SPEL表达式注入

369 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

前言

在前几天的网刃杯中,出了一道SPEL注入的Java题,在复现后其实对于原理和内部流程还是不够了解,所以现在浅学一下。

配置

github.com/LandGrey/Sp…,导入maven依赖

为了后期debug调试简便些,可以修改一下控制器

修改后运行,访问[localhost:9091/article?id=55](http://localhost:9091/article?id={5*5}](http://localhost:9091/article?id=%7B5*5%7D),出现结果25则为配置成功

影响版本

SpringBoot 1.1.0-1.1.12 SpringBoot 1.2.0-1.2.7 SpringBoot 1.3.0

利用条件是使用了springboot的默认错误页(Whitelabel Error Page),漏洞点在:org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration

触发原因

漏洞的触发点在SpringBoot的自定义错误页面,功能是页面返回错误,并提供详细信息,信息中包括错误status("status"->500)、时间戳("timestamp"->"Fri Dec.....")、错误信息("error"->"Internal Server Error")、和用户输入的参数("message"->"abcd"),这些参数在模板文件中以类似于以下形式存在:”Error 1234 status{status}---{timestamp}---error{error}---{message}“。

后端原理

在用户传入数据后,后端会判断${和它之后的}位置,若数据中存在${},则会将他去掉,即:若${55},在经过判断后变为55

去掉${}后的值会通过org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration类的resolvePlaceholder传入SpEL引擎,SpEL引擎将将直接对payload进行解析,造成了最终的rce

漏洞复现

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

引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}
​
其中属性名称引用还可以用符号$ 如:${someProperty}

除此以外在SpEL中,使用T()运算符会调用类作用域的方法和常量。例如,在SpEL中使用Java的Math类,我们可以像下面的示例这样使用T()运算符:

#{T(java.lang.Math)}
T()运算符的结果会返回一个java.lang.Math类对象。

由字符串格式转换成 0x**,即 java 字节形式,方便代码执行:

payload = 'calc'
res = ''
for i in payload:
    res += hex(ord(i)) + ','print(res.rstrip(','))
​
#${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}

传参后弹出calc,成功执行

流程分析

先启动debug,再打断点,再传参

断点

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

分析

首先在193行,会将map的值传入,context的rootObject中,之后以this.templatethis.resolver为参数调用replacePlaceholders方法。这里的this.template为渲染界面的源代码

<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,会调用parseStringValue,value为上边的this.template

第一次递归

跟进parseStringValue

①:StringBuilderstrVal转为字符串,并赋值给result(strVal就是之前this.template的值)

②:判断this.placeholderPrefix的首次出现位置,这里placeholderPrefix="${",所以判断后在索引为157的位置找到了第一个${

③:findPlaceholderEndIndex判断157后边的第一个}位置

④:结果为168

⑤:substring截取157和168的中间值,即:placeholder=timestamp

⑥:递归调用,又调用了parseStringValue,此时第一个参数placeholder=timestamp

递归后,strVal的值变为timestamp,所以在indexOf判断时,由于没出现${,所以变为了-1,跳过了while循环,直接执行下边的return result.toString();

本次递归结束后,回到145行,继续向下执行进入resolvePlaceholder

跟进resolvePlaceholder,先通过parseExpression解析timestamp,再将context中timestamp的值赋给value,再通过retrun输出

执行完毕后timestamp的值赋给propVal,此时propVal的值不为空,所以直接跳到了下边的if判断,之后就是在162行处,再次递归调用,判断值Sun May 01 12:32:14 CST 2022中是否有,跟之前递归一样如果第一个参数中没有{},跟之前递归一样如果第一个参数中没有{,则直接return第一个参数的值,因此这次就不再跟进了。

之后就是进行replace替换,将原来的${timestamp}处的值替换成了 Sun May 01 12:32:14 CST 2022,最后return result.toString();返回

至此第一次递归结束

第二次递归

第二次递归后startIndex和endIndex的值分别变成了232,239,这是因为在此前将${timestamp}进行了替换,所以这个地方就没有,于是就需要向下寻找当找到{},于是就需要向下寻找当找到`{error}时,发现了${`,并且索引为232;239也同理,再之后的流程跟之前一样不分析了

第三次递归

第三次是${status},也同理

第四次递归

第四次就是我们用户传入的message了,再跟进分析下(147行前就不解释了)

直接进入147行的resolvePlaceholder,但这里由于还没有去掉${},所以这里并没有执行calc

继续往下看propVal有值,所以跳到162行处,又进行了递归(去除${})

这次递归在经过substring处理后,就去除了${}

最后进入resolvePlaceholder,成功执行T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))

网刃杯利用方式

分析完SPEL注入链后,发现我们一直用的都是${},但是网刃杯中用到的是#{},所以又回头看了一下

TemplateParserContext中,自定义了expressionPrefix的值—#{,所以本题才需要用#{}