Mi1k7ea

Jackson系列一——反序列化漏洞基本原理

阅读量

0x01 Jackson简介

Jackson是一个开源的Java序列化和反序列化工具,可以将Java对象序列化为XML或JSON格式的字符串,以及将XML或JSON格式的字符串反序列化为Java对象。

由于其使用简单,速度较快,且不依靠除JDK外的其他库,被众多用户所使用。

0x02 使用Jackson进行序列化和反序列化

Jackson提供了ObjectMapper.writeValueAsString()和ObjectMapper.readValue()两个方法来实现序列化和反序列化的功能。

我本地测试用到的jar包:jackson-annotations-2.7.9,jackson-core-2.7.9,jackson-databind-2.7.9

定义Person类:

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

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

JSTest.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class JSTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "mi1k7ea";
ObjectMapper mapper = new ObjectMapper();

String json = mapper.writeValueAsString(p);
System.out.println(json);

Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

输出:

1
2
{"age":6,"name":"mi1k7ea"}
Person.age=6, Person.name=mi1k7ea

0x03 多态问题的解决——JacksonPolymorphicDeserialization

简单地说,Java多态就是同一个接口使用不同的实例而执行不同的操作。

那么问题来了,如果对多态类的某一个子类实例在序列化后再进行反序列化时,如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例呢?——Jackson实现了JacksonPolymorphicDeserialization机制来解决这个问题。

JacksonPolymorphicDeserialization即Jackson多态类型的反序列化:在反序列化某个类对象的过程中,如果类的成员变量不是具体类型(non-concrete),比如Object、接口或抽象类,则可以在JSON字符串中指定其具体类型,Jackson将生成具体类型的实例。

简单地说,就是将具体的子类信息绑定在序列化的内容中以便于后续反序列化的时候直接得到目标子类对象,其实现有两种,即DefaultTyping和@JsonTypeInfo注解。

下面具体介绍一下。

DefaultTyping

Jackson提供一个enableDefaultTyping设置,其包含4个值,查看jackson-databind-2.7.9.jar!/com/fasterxml/jackson/databind/ObjectMapper.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
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
}

默认情况下,即无参数的enableDefaultTyping是第二个设置,OBJECT_AND_NON_CONCRETE。

下面分别对这几个选项进行说明。

JAVA_LANG_OBJECT

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

添加一个com.mi1k7ea.Hacker类:

1
2
3
4
5
package com.mi1k7ea;

public class Hacker {
public String skill = "Jackson";
}

修改Person类,添加Object类型属性:

1
2
3
4
5
6
7
8
9
10
public 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);
}
}

修改JSTest.java,添加enableDefaultTyping()并设置为JAVA_LANG_OBJECT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JSTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "mi1k7ea";
p.object = new Hacker();
ObjectMapper mapper = new ObjectMapper();
// 设置JAVA_LANG_OBJECT
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);

String json = mapper.writeValueAsString(p);
System.out.println(json);

Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

输出对比看到,通过enableDefaultTyping()设置设置JAVA_LANG_OBJECT后,会多输出Hacker类名,且在输出的Object属性时直接输出的是Hacker类对象,也就是说同时对Object属性对象进行了序列化和反序列化操作:

1
2
3
4
5
6
7
// 设置JAVA_LANG_OBJECT
{"age":6,"name":"mi1k7ea","object":["com.mi1k7ea.Hacker",{"skill":"Jackson"}]}
Person.age=6, Person.name=mi1k7ea, com.mi1k7ea.Hacker@7f9a81e8

// 未设置JAVA_LANG_OBJECT
{"age":6,"name":"mi1k7ea","object":{"skill":"Jackson"}}
Person.age=6, Person.name=mi1k7ea, {skill=Jackson}

OBJECT_AND_NON_CONCRETE

OBJECT_AND_NON_CONCRETE:除了前面提到的特征,当类里有Interface、AbstractClass类时,对其进行序列化和反序列化(当然这些类本身需要时合法的、可被序列化的对象)。此外,enableDefaultTyping()默认的无参数的设置就是此选项。

添加com.mi1k7ea.Sex接口类:

1
2
3
4
public interface Sex {
public void setSex(int sex);
public int getSex();
}

添加com.mi1k7ea.MySex类实现Sex接口类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MySex implements Sex {
int sex;

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

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

修改Person类:

1
2
3
4
5
6
7
8
9
10
11
public 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);
}
}

修改JSTest.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JSTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "mi1k7ea";
p.object = new Hacker();
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
// 设置OBJECT_AND_NON_CONCRETE
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
// 或直接无参调用,输出一样
//mapper.enableDefaultTyping();

String json = mapper.writeValueAsString(p);
System.out.println(json);

Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

输出,可以看到该Interface类属性被成功序列化和反序列化:

1
2
{"age":6,"name":"mi1k7ea","object":["com.mi1k7ea.Hacker",{"skill":"Jackson"}],"sex":["com.mi1k7ea.MySex",{"sex":0}]}
Person.age=6, Person.name=mi1k7ea, com.mi1k7ea.Hacker@62ee68d8, com.mi1k7ea.MySex@735b5592

NON_CONCRETE_AND_ARRAYS

NON_CONCRETE_AND_ARRAYS:除了前面提到的特征外,还支持Array类型。

下面直接修改JSTest.java,在Object属性中存在的是数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class JSTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "mi1k7ea";
Hacker[] hackers = new Hacker[2];
hackers[0] = new Hacker();
hackers[1] = new Hacker();
p.object = hackers;
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
// 设置NON_CONCRETE_AND_ARRAYS
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS);

String json = mapper.writeValueAsString(p);
System.out.println(json);

Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

输出看到,类名变成了”[L”+类名+”;”,序列化Object之后为数组形式,反序列化之后得到[Lcom.mi1k7ea.Hacker;类对象,说明对Array类型成功进行了序列化和反序列化:

1
2
{"age":6,"name":"mi1k7ea","object":["[Lcom.mi1k7ea.Hacker;",[{"skill":"Jackson"},{"skill":"Jackson"}]],"sex":["com.mi1k7ea.MySex",{"sex":0}]}
Person.age=6, Person.name=mi1k7ea, [Lcom.mi1k7ea.Hacker;@724af044, com.mi1k7ea.MySex@4678c730

NON_FINAL

NON_FINAL:除了前面的所有特征外,包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的属性信息都需要被序列化和反序列化。

修改Person类,添加Hacker属性:

1
2
3
4
5
6
7
8
9
10
11
12
public class Person {
public int age;
public String name;
public Object object;
public Sex sex;
public Hacker hacker;

@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, hacker == null ? "null" : hacker);
}
}

修改JSTest.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JSTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "mi1k7ea";
p.object = new Hacker();
p.sex = new MySex();
p.hacker = new Hacker();
ObjectMapper mapper = new ObjectMapper();
// 设置NON_FINAL
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

String json = mapper.writeValueAsString(p);
System.out.println(json);

Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

输出看到,成功对非final的hacker属性进行序列化和反序列化:

1
2
["Person",{"age":6,"name":"mi1k7ea","object":["com.mi1k7ea.Hacker",{"skill":"Jackson"}],"sex":["com.mi1k7ea.MySex",{"sex":0}],"hacker":["com.mi1k7ea.Hacker",{"skill":"Jackson"}]}]
Person.age=6, Person.name=mi1k7ea, com.mi1k7ea.Hacker@62ee68d8, com.mi1k7ea.MySex@735b5592, com.mi1k7ea.Hacker@58651fd0

小结

从前面的分析知道,DefaultTyping的几个设置选项是逐渐扩大适用范围的,如下表:

DefaultTyping类型 描述说明
JAVA_LANG_OBJECT 属性的类型为Object
OBJECT_AND_NON_CONCRETE 属性的类型为Object、Interface、AbstractClass
NON_CONCRETE_AND_ARRAYS 属性的类型为Object、Interface、AbstractClass、Array
NON_FINAL 所有除了声明为final之外的属性

@JsonTypeInfo注解

@JsonTypeInfo注解是Jackson多态类型绑定的一种方式,支持下面5种类型的取值:

1
2
3
4
5
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM)

下面我们逐个看下。

JsonTypeInfo.Id.NONE

JSTest.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JSTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "mi1k7ea";
p.object = new Hacker();
ObjectMapper mapper = new ObjectMapper();

String json = mapper.writeValueAsString(p);
System.out.println(json);

Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

Person类,给object属性添加@JsonTypeInfo注解,指定为JsonTypeInfo.Id.NONE:

1
2
3
4
5
6
7
8
9
10
11
public class Person {
public int age;
public String name;
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
public Object object;

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

输出看到,和没有设置值为JsonTypeInfo.Id.NONE的@JsonTypeInfo注解是一样的:

1
2
{"age":6,"name":"mi1k7ea","object":{"skill":"Jackson"}}
Person.age=6, Person.name=mi1k7ea, {skill=Jackson}

JsonTypeInfo.Id.CLASS

修改Person类中的object属性@JsonTypeInfo注解值为JsonTypeInfo.Id.CLASS。

输出看到,object属性中多了”@class”:”com.mi1k7ea.Hacker”,即含有具体的类的信息,同时反序列化出来的object属性Hacker类对象,即能够成功对指定类型进行序列化和反序列化:

1
2
{"age":6,"name":"mi1k7ea","object":{"@class":"com.mi1k7ea.Hacker","skill":"Jackson"}}
Person.age=6, Person.name=mi1k7ea, com.mi1k7ea.Hacker@1d057a39

也就是说,在Jackson反序列化的时候如果使用了JsonTypeInfo.Id.CLASS修饰的话,可以通过@class的方式指定相关类,并进行相关调用。

JsonTypeInfo.Id.MINIMAL_CLASS

修改Person类中的object属性@JsonTypeInfo注解值为JsonTypeInfo.Id.MINIMAL_CLASS。

输出看到,object属性中多了”@c”:”com.mi1k7ea.Hacker”,即使用@c替代料@class,官方描述中的意思是缩短了相关类名,实际效果和JsonTypeInfo.Id.CLASS类似,能够成功对指定类型进行序列化和反序列化,都可以用于指定相关类并进行相关的调用:

1
2
{"age":6,"name":"mi1k7ea","object":{"@c":"com.mi1k7ea.Hacker","skill":"Jackson"}}
Person.age=6, Person.name=mi1k7ea, com.mi1k7ea.Hacker@4c70fda8

JsonTypeInfo.Id.NAME

修改Person类中的object属性@JsonTypeInfo注解值为JsonTypeInfo.Id.NAME。

输出看到,object属性中多了”@type”:”Hacker”,但没有具体的包名在内的类名,因此在后面的反序列化的时候会报错,也就是说这个设置值是不能被反序列化利用的:

1
2
{"age":6,"name":"mi1k7ea","object":{"@type":"Hacker","skill":"Jackson"}}
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Could not resolve type id 'Hacker' into a subtype of [simple type, class java.lang.Object]: known type ids = [Object]

JsonTypeInfo.Id.CUSTOM

其实这个值时提供给用户自定义的意思,我们是没办法直接使用的,需要手动写一个解析器才能配合使用,直接运行会抛出异常:

1
Exception in thread "main" java.lang.IllegalStateException: Do not know how to construct standard type id resolver for idType: CUSTOM

小结

由前面测试发现,当@JsonTypeInfo注解设置为如下值之一并且修饰的是Object类型的属性时,可以利用来触发Jackson反序列化漏洞:

  • JsonTypeInfo.Id.CLASS
  • JsonTypeInfo.Id.MINIMAL_CLASS

0x04 反序列化中类属性方法的调用

这里只针对Jackson反序列化过程中存在的一些方法调用进行分析,并且只针对应用JacksonPolymorphicDeserialization机制的场景进行分析。

下面简单看下两个实现方式间是否有区别。

当使用DefaultTyping时

修改Person类:

1
2
3
4
5
6
7
8
9
10
public class Person {
public int age;
public String name;
public Sex sex;

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

在MySex类中的方法中添加输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MySex implements Sex {
int sex;
public MySex() {
System.out.println("MySex构造函数");
}

@Override
public int getSex() {
System.out.println("MySex.getSex");
return sex;
}

@Override
public void setSex(int sex) {
System.out.println("MySex.setSex");
this.sex = sex;
}
}

修改JSTest.java,只进行反序列化操作并调用无参数的enableDefaultTyping():

1
2
3
4
5
6
7
8
9
10
public class JSTest {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

String json = "{\"age\":6,\"name\":\"mi1k7ea\",\"sex\":[\"com.mi1k7ea.MySex\",{\"sex\":1}]}";
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

输出,看到调用了目标类的构造函数和setter方法:

1
2
3
MySex构造函数
MySex.setSex
Person.age=6, Person.name=mi1k7ea, com.mi1k7ea.MySex@153f5a29

当使用@JsonTypeInfo注解时

修改Person类,在sex属性前添加注解:

1
2
3
4
5
6
7
8
9
10
11
12
public class Person {
public int age;
public String name;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// 或 @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Sex sex;

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

修改JSTest.java,注释掉enableDefaultTyping():

1
2
3
4
5
6
7
8
9
10
public class JSTest {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
// mapper.enableDefaultTyping();

String json = "{\"age\":6,\"name\":\"mi1k7ea\",\"sex\":[\"com.mi1k7ea.MySex\",{\"sex\":1}]}";
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

输出看到,和使用DefaultTyping是一样的:

1
2
3
MySex构造函数
MySex.setSex
Person.age=6, Person.name=mi1k7ea, com.mi1k7ea.MySex@5ae9a829

调试分析

Jackson反序列化的过程其实就分为两步,第一步是通过构造函数生成实例,第二部是设置实例的属性值。

这里以第一个例子来进行Jackson反序列化过程的调试分析,在Person p2 = mapper.readValue(json, Person.class);处打上断点,同时在MySex类的构造函数、getter、setter方法中设置断点,然后开始调试。

另外,为了方便,给Person类加上个构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
public int age;
public String name;
// @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Sex sex;

public Person() {
System.out.println("Person构造函数");
}

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

BeanDeserializer.deserialize()函数中,调用了vanillaDeserialize()函数:

跟进去,BeanDeserializer.vanillaDeserialize()函数的实现比较简单,先调用createUsingDefault()函数来调用指定类的无参构造函数来生成类实例:

BeanDeserializer.vanillaDeserialize()函数调用完无参的类的构造函数生成实例Bean后,就开始进入do while循环来循环解析键值对中的属性值并调用deserializeAndSet()函数来解析并设置Bean的属性值:

跟进去FieldProperty.deserializeAndSet()函数中,发现调用了deserialize()函数:

跟进该SettableBeanProperty.deserialize()函数,可以看到有两个反序列化的代码逻辑,其中if判断语句会判断当前反序列化的内容是否携带类型,若是则调用deserializeWithType()函数解析,否则直接调用deserialize()函数解析:

这里第一个属性是age,属于Int类型,因此调用的是NumberDeserializers.deserialize()函数来获取age属性的值:

反序列化解析得到属性值之后,回到FieldProperty.deserializeAndSet()函数中,调用属性的setter方法来设置Bean的属性值:

接着就是回到BeanDeserializer.vanillaDeserialize()函数中的do while循环中继续解析name、sex等属性内容。

name和前面age一样的调用过程,sex属性值是包含类名的数组,因此直接调试到解析sex属性时看到,在SettableBeanProperty.deserialize()函数中进入到了调用deserializeWithType()函数解析的代码逻辑,因为此时_valueTypeDeserializer值不为null:

跟进AbstractDeserializer.deserializeWithType()函数中,进一步调用了AsArrayTypeDeserializer.deserializeTypedFromObject()函数来解析:

跟进去,看到调用_locateTypeId()函数获取到了typeId为”com.mi1k7ea.MySex”即已经获取到我们的类型名称,然后根据该typeId调用_findDeserializer()函数寻找对应的反序列化器:

跟进去,其中调用findContextualValueDeserializer()找到typeId类型对应的反序列化器,然后缓存到_deserializers这个Map变量中,然后返回该反序列化器:

接着程序回到数组类型解析的AsArrayTypeDeserializer._deserialize()函数中往下执行,用刚刚获取到的反序列化器来解析sex属性中数组内的具体类型实例:

然后会再次调用BeanDeserializer.deserialize()->BeanDeserializer.vanillaDeserialize()来解析数组内的内容,其中调用createUsingDefault()函数的时候会调用到MySex类的无参构造函数来新建MySex类对象:

获取到MySex类对象后,回到vanillaDeserialize()函数中扫描到该类对象的sex属性后,调用deserializeAndSet()函数获取该属性值并设置到该实例中:

跟进MethodProperty.deserializeAndSet()函数,先是获取sex属性值,其调用过程和前面一样;然后就是通过反射机制调用该属性的setter方法进行设置:

至此,整个函数调用过程大致过了一遍。使用@JsonTypeInfo注解的函数调用过程也是一样的。

简单梳理一遍,Jackson反序列化的过程为,先调用通过无参的构造函数生成目标类实例,接着是根据属性值是否是数组的形式即是否带类名来分别调用不同的函数来设置实例的属性值,其中会调用Object类型属性的构造函数和setter方法。

调用到MySex类的构造函数时的函数调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<init>:6, MySex (com.mi1k7ea)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, 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:142, AbstractDeserializer (com.fasterxml.jackson.databind.deser)
deserialize:493, SettableBeanProperty (com.fasterxml.jackson.databind.deser)
deserializeAndSet:101, FieldProperty (com.fasterxml.jackson.databind.deser.impl)
vanillaDeserialize:260, BeanDeserializer (com.fasterxml.jackson.databind.deser)
deserialize:125, BeanDeserializer (com.fasterxml.jackson.databind.deser)
_readMapAndClose:3807, ObjectMapper (com.fasterxml.jackson.databind)
readValue:2797, ObjectMapper (com.fasterxml.jackson.databind)
main:11, JSTest

调用到MySex类sex属性的setter方法时的函数调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setSex:17, MySex (com.mi1k7ea)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, 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:142, AbstractDeserializer (com.fasterxml.jackson.databind.deser)
deserialize:493, SettableBeanProperty (com.fasterxml.jackson.databind.deser)
deserializeAndSet:101, FieldProperty (com.fasterxml.jackson.databind.deser.impl)
vanillaDeserialize:260, BeanDeserializer (com.fasterxml.jackson.databind.deser)
deserialize:125, BeanDeserializer (com.fasterxml.jackson.databind.deser)
_readMapAndClose:3807, ObjectMapper (com.fasterxml.jackson.databind)
readValue:2797, ObjectMapper (com.fasterxml.jackson.databind)
main:11, JSTest

结论

在Jackson反序列化中,若调用了enableDefaultTyping()函数或使用@JsonTypeInfo注解指定反序列化得到的类的属性为JsonTypeInfo.Id.CLASS或JsonTypeInfo.Id.MINIMAL_CLASS,则会调用该属性的类的构造函数和setter方法。

0x05 Jackson反序列化漏洞

前提条件

满足下面三个条件之一即存在Jackson反序列化漏洞:

  • 调用了ObjectMapper.enableDefaultTyping()函数;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解;

漏洞原理

由之前的结论知道,当使用的JacksonPolymorphicDeserialization机制配置有问题时,Jackson反序列化就会调用属性所属类的构造函数和setter方法。

而如果该构造函数或setter方法存在危险操作,那么就存在Jackson反序列化漏洞。

漏洞场景及Demo

这里大致以要进行反序列化的类的属性所属的类的类型分为两种:

属性不为Object类时

当要进行反序列化的类的属性所属类的构造函数或setter方法本身存在漏洞时,这种场景存在Jackson反序列化漏洞。当然这种场景开发几乎不会这么写。

我们看个例子,直接修改MySex类的setSex()方法,在其中添加命令执行操作(除非程序员自己想留后门、不然不会出现这种写法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MySex implements Sex {
int sex;
public MySex() {
System.out.println("MySex构造函数");
}

@Override
public int getSex() {
System.out.println("MySex.getSex");
return sex;
}

@Override
public void setSex(int sex) {
System.out.println("MySex.setSex");
this.sex = sex;
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
}

Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
public int age;
public String name;
// @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Sex sex;

public Person() {
System.out.println("Person构造函数");
}

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

JSTest.java:

1
2
3
4
5
6
7
8
9
10
public class JSTest {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

String json = "{\"age\":6,\"name\":\"mi1k7ea\",\"sex\":[\"com.mi1k7ea.MySex\",{\"sex\":1}]}";
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

运行即可弹计算器:

属性为Object类时

当属性类型为Object时,因为Object类型是任意类型的父类,因此扩大了我们的攻击面,我们只需要寻找出在目标服务端环境中存在的且构造函数或setter方法存在漏洞代码的类即可进行攻击利用。

后面出现的Jackson反序列化的CVE漏洞、黑名单绕过等都是基于这个原理寻找各种符合条件的利用链而已。

这里我们假设目标服务端环境中存在其一个恶意类Evil,其setter方法存在任意代码执行漏洞,存在于com.evil包中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.evil;

public class Evil {
String cmd;

public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec(this.cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}

Person类,将sex属性改为object属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
public int age;
public String name;
// @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Object object;

public Person() {
System.out.println("Person构造函数");
}

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

JSTest.java:

1
2
3
4
5
6
7
8
9
10
11
12
public class JSTest {
public static void main(String[] args) throws Exception {


ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

String json = "{\"age\":6,\"name\":\"mi1k7ea\",\"object\":[\"com.evil.Evil\",{\"cmd\":\"calc\"}]}";
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}

运行即可触发:


OK,Jackson反序列化漏洞相关的一些基本概念都说到了,后面的几篇文章就分别对Jackson反序列化的几个CVE漏洞以及不同的利用链进行分析。


Copyright © Mi1k7ea | 本站总访问量