CVE-2017-8046

https://github.com/vulhub/vulhub/tree/master/spring/CVE-2017-8046

https://github.com/vulhub/vulhub/tree/master/base/spring/spring-rest-data/2.6.6

https://github.com/spring-projects/spring-data-rest/tree/2.6.6.RELEASE

https://mp.weixin.qq.com/s/uTiWDsPKEjTkN6z9QNLtSA

https://github.com/spring-projects/spring-data-rest/compare/2.6.6.RELEASE...2.6.7.RELEASE

https://github.com/spring-projects/spring-data-rest/compare/2.6.6.RELEASE...2.6.9.RELEASE

概述

Spring Data Rest 远程命令执行漏洞(CVE-2017-8046)

Spring Data REST是一个构建在Spring Data之上,为了帮助开发者更加容易地开发REST风格的Web服务。在REST API的Patch方法中(实现RFC6902),path的值被传入setValue,导致执行了SpEL表达式,触发远程命令执行漏洞。

受影响的版本

  • 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.6.9

分析

分析基于 2.6.6 版本,配合 vulhub 示例的版本。

org/springframework/data/rest/webmvc/config/JsonPatchHandler.java:76

此处进行处理 patch 请求,可以看到先判断是不是 patch 请求,然后有一个 isJsonPatchRequest(判断请求头类型是不是application/json-patch+json),然后进入applyPatch

org/springframework/data/rest/webmvc/config/JsonPatchHandler.java:90

org/springframework/data/rest/webmvc/config/JsonPatchHandler.java:109

applyPatch 进入到 getPatchOperations,source 是上面传过来的请求体,在getPatchOperations中mapper``.readTree(source)是调用的 Jackson 的方法,将请求体内容转换为 JsonNode,然后使用JsonPatchPatchConverter的 convert 方法去进行处理数据。

org/springframework/data/rest/webmvc/json/patch/JsonPatchPatchConverter.java:50

convert 代码如下,可以看到对 op 、 path 和 from 进行处理,根据 op 类型不同,创建不同的 Operation 对象

public Patch convert(JsonNode jsonNode) {

		if (!(jsonNode instanceof ArrayNode)) {
			throw new IllegalArgumentException("JsonNode must be an instance of ArrayNode");
		}

		ArrayNode opNodes = (ArrayNode) jsonNode;
		List<PatchOperation> ops = new ArrayList<PatchOperation>(opNodes.size());

		for (Iterator<JsonNode> elements = opNodes.elements(); elements.hasNext();) {

			JsonNode opNode = elements.next();

			String opType = opNode.get("op").textValue();
			String path = opNode.get("path").textValue();

			JsonNode valueNode = opNode.get("value");
			Object value = 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")) {
				ops.add(new MoveOperation(path, from));
			} else {
				throw new PatchException("Unrecognized operation type: " + opType);
			}
		}

		return new Patch(ops);
	}

这里以RemoveOperation为例子,在创建时,传入 path 值,随即调用super``(``"remove"``, path)``;

在创建时会初始化 spelExpression 对象

pathToExpression 就是将 path 通过 pathToSpEL 处理字符串后,交给 spel 进行解析

	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;
			}
/*static final List<String> APPEND_CHARACTERS = Arrays.asList("-", "~")*/
			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;
	}

至此,相关的前置条件准备充分,然后就是如何执行 spel 表达式,回到applyPatch,getPatchOperations(source)之后会执行spply 方法,进去之后就是执行刚才解析的opration的 perform 方法,这里以RemoveOperation为例,就是去执行popValueAtPath,

修复分析

增加 verifyPath 方法校验路径是否合法,关键在于PropertyPath._from_(pathSource``, type)``;路径非法会抛出异常。

当调用 PropertyPath.from(pathSource, type) 时,Spring Data 会尝试解析 pathSource 字符串,以创建一个 PropertyPath 对象。在这个过程中,Spring Data 会对 pathSource 进行一些验证来确保它是合法的。

具体来说,Spring Data 在检测 pathSource 是否合法时,会执行以下步骤:

语法分析:Spring Data 首先会对 pathSource 字符串进行语法分析,以解析其中的属性路径。

类型检查:Spring Data 尝试根据提供的 type 参数,检查属性路径是否在该类型中是有效的。这是为了确保属性路径在给定的类型中是存在的。

属性路径验证:Spring Data 会验证属性路径中的每一部分是否合法,包括属性名称是否存在、是否可访问等。

防止注入攻击:Spring Data 会对属性路径中的每一部分进行合理的验证,以防止可能的注入攻击。

但是2.6.7 版本中只有PatchOperation#evaluateValueFromTarget和 AddOperation#evaluateValueFromTarget调用了这个检查,实际能够修复影响的只有Add 、 Replace 和 Test,Remove 是不受影响的。

2.6.9版本引入SpelPath类对路径进行统一处理,都经过verifyPath 校验,相应的操作方法也都转移到 SpelPath 类中,至此问题全部解决。