Jackson反序列化漏洞简介(一):Jackson基本的工作原理

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

本文是第一篇,讲讲jackson的基础知识。

一、Jackson的基本用法

直接贴代码吧,很简单的int和String操作,序列化反序列化,非常舒服。

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

public static void main(String args[]) throws IOException {
Person p = new Person();
p.age = 10;
p.name = "Alice";

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
System.out.println(json);
// {"age":10,"name":"Alice"}
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
// Person.age=10, Person.name=Alice
}
}

class Person {
public int age;
public String name;

@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s", age, name);
}
}

恩,看起来没有任何问题,这怎么会有漏洞呢?没错,这么写没有漏洞。。。

如果是我这样的玩家,使用基础的用法,不用骚操作,写上面的代码,是无法被攻击的。 但json规范里有很多额外自定义的东西,这些会引入额外的解析规则。

二、DefaultTyping的配置

jackson提供一个设置,叫enableDefaultTyping,有4个值。

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
public enum DefaultTyping {
/**
* This value means that only properties that have
* {@link java.lang.Object} as declared type (including
* generic types without explicit type) will use default
* typing.
*/
JAVA_LANG_OBJECT,

/**
* Value that means that default typing will be used for
* properties with declared type of {@link java.lang.Object}
* or an abstract type (abstract class or interface).
* Note that this does <b>not</b> include array types.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
OBJECT_AND_NON_CONCRETE,

/**
* Value that means that default typing will be used for
* all types covered by {@link #OBJECT_AND_NON_CONCRETE}
* plus all array types for them.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
NON_CONCRETE_AND_ARRAYS,

/**
* Value that means that default typing will be used for
* all non-final types, with exception of small number of
* "natural" types (String, Boolean, Integer, Double), which
* can be correctly inferred from JSON; as well as for
* all arrays of non-final types.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
NON_FINAL
}

下文println的输出内容已经作为注释被写到了代码里。

1、JAVA_LANG_OBJECT

JAVA_LANG_OBJECT 当类里的属性声明为一个Object时,会对该属性进行序列化和反序列化,并且明确规定类名。(当然,这个Object本身也得是一个可被序列化/反序列化的类)

例如下面的代码,我们给Person里添加一个Object object。

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

public static void main(String args[]) throws IOException {
Person p = new Person();
p.age = 10;
p.name = "Alice";
p.object = new Dna();
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);
String json = mapper.writeValueAsString(p);
System.out.println(json);
// {"age":10,"name":"Alice","object":["com.leadroyal.example.Dna",{"length":100}]}
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
// Person.age=10, Person.name=Alice, com.leadroyal.example.Dna@7c16905e
}
}

class Person {
public int age;
public String name;
public Object object;

@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object);
}
}

class Dna {
public int length = 100;
}

神奇的是,输出的String和我们平时看到的json长得不一样了,额外多出来com.leadroyal.example.Dna 的信息。 这就是 EnableDefaultTyping ,在序列化的String里,包含类原本的一些信息,将来反的时候会进行还原。

2、OBJECT_AND_NON_CONCRETE

OBJECT_AND_NON_CONCRETE 。除了上文 提到的特征,当类里有Interface 、AbstractClass 时,对其进行序列化和反序列化。(当然,这些类本身需要是合法的、可以被序列化/反序列化的对象)。

例如下面的代码,这次我们添加名为 Sex 的interface,发现它被正确序列化、反序列化了,就是这个选项控制的。

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

public static void main(String args[]) throws IOException {
Person p = new Person();
p.age = 10;
p.name = "Alice";
p.object = new Dna();
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
String json = mapper.writeValueAsString(p);
System.out.println(json);
// {"age":10,"name":"Alice","object":["com.leadroyal.example.Dna",{"length":100}],"sex":["com.leadroyal.example.MySex",{"sex":0}]}
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
// Person.age=10, Person.name=Alice, com.leadroyal.example.Dna@2a5ca609, com.leadroyal.example.MySex@20e2cbe0


}
}

class Person {
public int age;
public String name;
public Object object;
public Sex sex;

@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s, %s", age, name, object == null ? "null" : object, sex == null ? "null" : sex);
}
}

class Dna {
public int length = 100;
}

class MySex implements Sex {
int sex;

@Override
public int getSex() {
return sex;
}

@Override
public void setSex(int sex) {
this.sex = sex;
}
}

interface Sex {
public void setSex(int sex);

public int getSex();
}

3、NON_CONCRETE_AND_ARRAYS

NON_CONCRETE_AND_ARRAYS ,除了上文提到的特征,还支持上文全部类型的Array类型。

例如下面的代码,我们的Object里存放Dna的数组。

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
57
58
59
public class Hello {

public static void main(String args[]) throws IOException {
Person p = new Person();
p.age = 10;
p.name = "Alice";
Dna[] dnas = new Dna[2];
dnas[0] = new Dna();
dnas[1] = new Dna();
p.object = dnas;
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS);
String json = mapper.writeValueAsString(p);
System.out.println(json);
// {"age":10,"name":"Alice","object":["[Lcom.leadroyal.example.Dna;",[{"length":100},{"length":100}]],"sex":["com.leadroyal.example.MySex",{"sex":0}]}
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
// Person.age=10, Person.name=Alice, [Lcom.leadroyal.example.Dna;@475530b9, com.leadroyal.example.MySex@1d057a39


}
}

class Person {
public int age;
public String name;
public Object object;
public Sex sex;

@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s, %s", age, name, object == null ? "null" : object, sex == null ? "null" : sex);
}
}

class Dna {
public int length = 100;
}

class MySex implements Sex {
int sex;

@Override
public int getSex() {
return sex;
}

@Override
public void setSex(int sex) {
this.sex = sex;
}
}

interface Sex {
public void setSex(int sex);

public int getSex();
}

4、NON_FINAL

NON_FINAL ,包括上文提到的所有特征,而且包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的的属性信息都需要被序列化和反序列化。

例如下面的代码,添加了类型为Dna的变量,非Object也非虚,但也可以被序列化出来。

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
57
58
59
60
61
public class Hello {

public static void main(String args[]) throws IOException {
Person p = new Person();
p.age = 10;
p.name = "Alice";
p.object = new Dna();
p.sex = new MySex();
p.dna = new Dna();
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
String json = mapper.writeValueAsString(p);
System.out.println(json);
// ["com.leadroyal.example.Person",{"age":10,"name":"Alice","object":["com.leadroyal.example.Dna",{"length":100}],"sex":["com.leadroyal.example.MySex",{"sex":0}],"dna":["com.leadroyal.example.Dna",{"length":100}]}]
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
// Person.age=10, Person.name=Alice, com.leadroyal.example.Dna@2a5ca609, com.leadroyal.example.MySex@20e2cbe0, com.leadroyal.example.Dna@68be2bc2


}
}

class Person {
public int age;
public String name;
public Object object;
public Sex sex;
public Dna dna;

@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s, %s, %s", age, name,
object == null ? "null" : object,
sex == null ? "null" : sex,
dna == null ? "null" : dna);
}
}

class Dna {
public int length = 100;
}

class MySex implements Sex {
int sex;

@Override
public int getSex() {
return sex;
}

@Override
public void setSex(int sex) {
this.sex = sex;
}
}

interface Sex {
public void setSex(int sex);

public int getSex();
}

默认的、无参的enableDefaultTyping是第二个等级,OBJECT_AND_NON_CONCRETE。

总结一下,如果开启了这个功能,就可以指定被反序列化的那个类!

三、序列化时的逻辑

这里是简要的逻辑,并没有太认真地去阅读代码,各位读者将就着看,不一定对。 在序列化时候,要求field可以访问到,先走getter,如果没有的话就去找field,如果还没有,就跳过这个field。 在没有开启defaultTyping的情况下,所有的field都要是基本数据类型、数组、集合类型和由常见类型组成的类等常见的数据,一旦属性里包含了不常见的东西,就直接抛异常了。 例如这个是合法的,可以被序列化和反序列化的。

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

class Dna {
public int length = 100;
}

例如这个是非法的,因为包含了一个不常见的类型。

1
2
3
4
5
6
7
8
9
10
class Person {
public int age;
public String name;
public Dna dna;
public FileInputStream stream;
}

class Dna {
public int length = 100;
}

在开启了 dafaultTyping的情况下,同样会对所有field都做一遍序列化,但区别在于,对不常见的类型反序列化时,需要主动指明类名,所以结构会不一样。也就是说,开启和关闭 defaultTyping 二者是相互无法解开对方的字符串的,因为协议不一样。 就拿上面这个例子,开启前后序列化出来的内容如下:

1
2
3
4
5
6
7
{
"age": 10,
"name": "Alice",
"dna": {
"length": 100
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
[
"com.leadroyal.example.Person",
{
"age": 10,
"name": "Alice",
"dna": [
"com.leadroyal.example.Dna",
{
"length": 100
}
]
}
]

四、反序列化时的逻辑

这个代码我是读了很多次的,也调试过,可以稍微看下,但也有不准确的地方。

首先对于不开启defaultTyping的,根据json里的key-value中的key,去找对应变量的setter,找不到的话就找对应变量,还找不到的话,如果没有设置 ignore unknown,就抛异常了。因为控制的都是基本类型,所以攻击者无法发动攻击,是一种保守、安全的配置。

如果开启了defaultTyping,就不一样了,这里我们先讲两种格式,第一种格式是上面的,类名后面加一堆key-value,去反序列化指定的类;第二种是类名后面跟一个参数,但必须是基本类型,官方解释是方便使用,一般推荐String,这样就可以从String反序列化这个类了。

好,下面进入debug时间,首先样例代码就是上面那段,我们断点下在readValue上,去反序列化自动生成的那段。然后在构造方法、setter上面同样下断点,这样就可以看到栈回溯了。

第一次,断在了Person的构造函数上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<init>:31, Person (com.leadroyal.example)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
call:119, AnnotatedConstructor (com.fasterxml.jackson.databind.introspect)
createUsingDefault:243, StdValueInstantiator (com.fasterxml.jackson.databind.deser.std)
vanillaDeserialize:249, BeanDeserializer (com.fasterxml.jackson.databind.deser)
deserialize:125, BeanDeserializer (com.fasterxml.jackson.databind.deser)
_deserialize:110, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl)
deserializeTypedFromObject:58, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl)
deserializeWithType:1021, BeanDeserializerBase (com.fasterxml.jackson.databind.deser)
deserialize:63, TypeWrappedDeserializer (com.fasterxml.jackson.databind.deser.impl)
_readMapAndClose:3807, ObjectMapper (com.fasterxml.jackson.databind)
readValue:2797, ObjectMapper (com.fasterxml.jackson.databind)
main:20, Hello (com.leadroyal.example)



来三张图,大概就是通过反射,调用了构造函数,这我这个testcase下,只会调用默认的构造函数。但看代码里,似乎有多参数的构造和一个参数的构造也是可以。

点下继续,下一次断在了setAge上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setAge:35, Person (com.leadroyal.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
deserializeAndSet:97, MethodProperty (com.fasterxml.jackson.databind.deser.impl)
vanillaDeserialize:260, BeanDeserializer (com.fasterxml.jackson.databind.deser)
deserialize:125, BeanDeserializer (com.fasterxml.jackson.databind.deser)
_deserialize:110, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl)
deserializeTypedFromObject:58, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl)
deserializeWithType:1021, BeanDeserializerBase (com.fasterxml.jackson.databind.deser)
deserialize:63, TypeWrappedDeserializer (com.fasterxml.jackson.databind.deser.impl)
_readMapAndClose:3807, ObjectMapper (com.fasterxml.jackson.databind)
readValue:2797, ObjectMapper (com.fasterxml.jackson.databind)
main:20, Hello (com.leadroyal.example)


大概就是通过反射,找到了setter方法,然后去调用赋值。

之后setName和setDna这个一模一样,后者是复杂对象,也是经过了同样的、反序列化逻辑,是嵌套的。

在没有setter方法时,应该会尝试去主动赋值,例如我们把age的setter删掉,会走下图中的代码,使用反射去赋值。

由于篇幅有限,大概就是这个样子,还是自己调试时候感受比较深刻。

这时候注意一下,前面提到,jackson代码里是可以调用到仅带有一个参数的构造方法的,当初设计这个功能可能是为了使用带String的构造方法,从而生成该对象的,也可以用其他的、仅有一个参数但是是基本类型或者数组。

再这里对各个creator做了初始化,共有9个允许的构造方法。

在寻找完毕后,可以看到我们有3个允许的构造方法。

其中,无参的构造方法是这里初始化的,之后放入第一个格子里。


有参的在这里被初始化,根据参数类型去放到相应的位置。

之前提到参数个数为1,也是这里判定的,ctor就是那个creator,将来放到9个格子里中的某一个。

五、总结

没什么好总结的吧,基本操作,多构建一些poc测测就知道了。

请期待第二篇文章~~~~~~~