jackson反序列化原理学习

GTL-JU Lv3

一、前言

在之前的buu月赛中,遇到了一道使用jackson依赖打内存马的题,由于并没有学习过jackson反序列化漏洞,这里对这个漏洞进行学习,以便于后面的题目复现。

二、关于jackson

Java生态圈中有很多处理JSON和XML格式化的类库, 常见的解析器:Jsonlib,Gson,fastjson,Jackson。Jackson是其中比较著名的一个,可以将Java对象序列化为XML或JSON格式的字符串,以及将XML或JSON格式的字符串反序列化为Java对象。并且jacksun的使用比较简单,速度快,且不依赖除JDK外的其他库。

使用的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.9</version>
</dependency>

三、jackson序列化与反序列化

在jackson中提供了ObjectMapper.writeValueAsString()ObjectMapper.readValue()两个方法来进行序列化和反序列化,通过ObjectMapper.writeValueAsString()方法将java对象序列化为json格式数据,通过ObjectMapper.readValue()方法将json格式数据反序列化成java对象。

下面我们通过一个demo来演示:

定义一个Product类:

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
public class Product {
private String name;
private double price;
private String description;

public Product(String name, double price, String description) {
this.name = name;
this.price = price;
this.description = description;
}


public Product(){}


// Getters and Setters for the class properties
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", description='" + description + '\'' +
'}';
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class test {
public static void main(String[] args) throws Exception {
Product product=new Product("苹果",10,"优质");
ObjectMapper objectMapper =new ObjectMapper();
String w = objectMapper.writeValueAsString(product);
System.out.println("这里是序列化:");
System.out.println(w);
Product r=objectMapper.readValue(w, Product.class);
System.out.println("这里是反序列化");
System.out.println(r);


}
}

运行结果:

image-20231026153730007

四、jackson中的多态问题

我们这里先了解一些java中的多态:

多态是同一个行为具有多个不同表现形式或形态的能力。

我们可以理解为同一个接口,使用不同的实例而执行不同操作。

img

这里以菜鸟教程的一个demo来更好的说明:

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
public class Test {
public static void main(String[] args) {
show(new Cat()); // 以 Cat 对象调用 show 方法
show(new Dog()); // 以 Dog 对象调用 show 方法

Animal a = new Cat(); // 向上转型
a.eat(); // 调用的是 Cat 的 eat
Cat c = (Cat)a; // 向下转型
c.work(); // 调用的是 Cat 的 work
}

public static void show(Animal a) {
a.eat();
// 类型判断
if (a instanceof Cat) { // 猫做的事情
Cat c = (Cat)a;
c.work();
} else if (a instanceof Dog) { // 狗做的事情
Dog c = (Dog)a;
c.work();
}
}
}

abstract class Animal {
abstract void eat();
}

class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void work() {
System.out.println("抓老鼠");
}
}

class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void work() {
System.out.println("看家");
}
}

可以看到我们在主函数中都调用了show方法,但是输出的结果却不一样

这也就是我们说的同一个接口,使用不同的实例而执行不同操作。

五、JacksonPolymorphicDeserialization机制解决jackson`的多态问题

我们上面说了多态就是使用同一个接口,使用不同的实例实现不同的操作

那么现在就出现了一个问题,我们如果在对多态类的某一个子类实例在进行序列化后反序列化,那么反序列化出来的实例是我们想要的那个子类的实例吗,会不会反序列化出其他子类的实例。

然后jackson就是通过JacksonPolymorphicDeserialization机制这个机制来解决这个问题的。

JacksonPolymorphicDeserialization在反序列化某个类对象的过程中,如果这个类的对象不是具体的类型,例如object,接口或者抽象类,那么我们就可以在json字符串中指定其具体类型。这样jackson就可以生成具体类型的实例。

JacksonPolymorphicDeserialization机制就是将具体的子类信息绑定在序列化的内容中,从而在后续进行反序列化的时候直接得到目标子类对象。

JacksonPolymorphicDeserialization机制有两种方法来解决这个问题:DefaultTyping@JsonTypeInfo注解。

DefaultTyping

jackson提供一个enableDefaultTyping设置

image-20231026202037268

包含四个值

image-20231026202104524

在默认情况下,也就是enableDefaultTyping在无参数的情况下,默认选择OBJECT_AND_NON_CONCRETE

我们下面结合我们上面的demo详细分析一下:

JAVA_LANG_OBJECT

我们这里添加一个类:

1
2
3
public class hacker {
public String skill = "hacker";
}

修改 Product类添加一个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
public class Product {
private String name;
private double price;
private String description;
public Object object;

public Product(String name, double price, String description,Object object) {
this.name = name;
this.price = price;
this.description = description;
this.object=object;
}


public Product(){}


// Getters and Setters for the class properties
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", description='" + description + '\'' +
", object=" + object +
'}';
}
}

修改测试类,添加enableDefaultTyping()并设置为JAVA_LANG_OBJECT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class test {
public static void main(String[] args) throws Exception {
Product product=new Product("苹果",10,"优质",new hacker());
ObjectMapper objectMapper =new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);
String w = objectMapper.writeValueAsString(product);
System.out.println("这里是序列化:");
System.out.println(w);
Product r=objectMapper.readValue(w, Product.class);
System.out.println("这里是反序列化");
System.out.println(r);


}
}

运行测试:

1
2
3
4
这里是序列化:
{"name":"苹果","price":10.0,"description":"优质","object":["blog.hacker",{"skill":"hacker"}]}
这里是反序列化
Product{name='苹果', price=10.0, description='优质', object=blog.hacker@5dfcfece}

注释掉enableDefaultTyping()

1
2
3
4
这里是序列化:
{"name":"苹果","price":10.0,"description":"优质","object":{"skill":"hacker"}}
这里是反序列化
Product{name='苹果', price=10.0, description='优质', object={skill=hacker}}

通过上面是否添加enableDefaultTyping()并设置JAVA_LANG_OBJECT对比可以看到,添加后会在序列化的时候多输出hacke的类名,并且在反序列化的时候,添加了enableDefaultTyping()的只会输出hacker类对象。那么这里就是说对Object同时进行了序列化和反序列化操作。

OBJECT_AND_NON_CONCRETE

OBJECT_AND_NON_CONCRETE除了JAVA_LANG_OBJECT的可以对Object对象进行序列化和反序列化,当类里面有接口,抽象类的时候,对其进行序列化和反序列化。而且enableDefaultTyping()默认无参数的情况下的设置就是这个选项。

我们这里通过实现一个接口来进行测试:

1
2
3
4
5
public interface productjie {
public void setprice(int price);
public int getprice();
}

写一个类继承这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class myproductjie implements productjie {
int price;



@Override
public void setprice(int price) {
this.price=price;

}

@Override
public int getprice() {
return price;
}
}

修改一下product类:

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 Product {
private String name;
private double price;
private String description;
public Object object;
public productjie productjie;


public Product(String name, double price, String description,Object object,productjie productjie) {
this.name = name;
this.price = price;
this.description = description;
this.object=object;
this.productjie=productjie;
}


public Product(){}


// Getters and Setters for the class properties
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", description='" + description + '\'' +
", object=" + object +
", productjie=" + productjie +
'}';
}
}

修改测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class test {
public static void main(String[] args) throws Exception {
Product product=new Product("苹果",10,"优质",new hacker(),new myproductjie());
ObjectMapper objectMapper =new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
String w = objectMapper.writeValueAsString(product);
System.out.println("这里是序列化:");
System.out.println(w);
Product r=objectMapper.readValue(w, Product.class);
System.out.println("这里是反序列化");
System.out.println(r);


}
}

运行结果:

1
2
3
4
这里是序列化:
{"name":"苹果","price":10.0,"description":"优质","object":["blog.hacker",{"skill":"hacker"}],"productjie":["blog.myproductjie",{"price":0}]}
这里是反序列化
Product{name='苹果', price=10.0, description='优质', object=blog.hacker@6cc4c815, productjie=blog.myproductjie@3a82f6ef}

我们这里可以看到我们定义的接口属性被成功序列化和反序列化了

那我们如果还是用上面那个JAVA_LANG_OBJECT设置值来测试一下:

image-20231028170023175

我们可以看到我们替换为JAVA_LANG_OBJECT的时候在进行反序列化的时候产生了报错。

抛出了一个异常这个异常的根本原因是 Jackson 在反序列化时遇到了一个抽象类型或接口类型,而无法确定如何实例化它,因为抽象类型不能直接实例化。

NON_CONCRETE_AND_ARRAYS

NON_CONCRETE_AND_ARRAYS除了上面的Object,接口,抽象类型可以被序列化和反序列化,还支持Array类型

这里直接将测试代码中的hacker对象修改为数组类型的就行l

修个测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class test {
public static void main(String[] args) throws Exception {
hacker[] hacker = new hacker[2];
hacker[0]=new hacker();
hacker[1]=new hacker();
Product product=new Product("苹果",10,"优质",hacker,new myproductjie());
ObjectMapper objectMapper =new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS);
String w = objectMapper.writeValueAsString(product);
System.out.println("这里是序列化:");
System.out.println(w);
Product r=objectMapper.readValue(w, Product.class);
System.out.println("这里是反序列化");
System.out.println(r);


}
}

运行结果:

1
2
3
4
这里是序列化:
{"name":"苹果","price":10.0,"description":"优质","object":["[Lblog.hacker;",[{"skill":"hacker"},{"skill":"hacker"}]],"productjie":["blog.myproductjie",{"price":0}]}
这里是反序列化
Product{name='苹果', price=10.0, description='优质', object=[Lblog.hacker;@20e2cbe0, productjie=blog.myproductjie@68be2bc2}

同样的我们将这个设置值换成前面的尝试运行:

这里更换为JAVA_LANG_OBJECT测试

image-20231028171147939

但是这里经过测试发现 OBJECT_AND_NON_CONCRETE也可也正常序列化和反序列化数组

image-20231028171424097

NON_FINAL

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

这里就修改一些test代码的中的属性设置值就可以了

image-20231028172015428

@JsonTypeInfo注解

@JsonTypeInfo 注解是 Jackson 库中的一个重要注解,用于在序列化和反序列化 JSON 数据时指定类型信息。它允许你为类的字段或属性添加类型信息,以便 Jackson 可以正确地识别和处理多态类型的数据。

使用 JsonTypeInfo.Id 枚举: 可以指定 use 属性为 JsonTypeInfo.Id 枚举值之一,以确定类型信息的使用方式。常见的枚举值包括:

JsonTypeInfo.Id.NONE

  • JsonTypeInfo.Id.CLASS:使用类名作为类型信息。

  • JsonTypeInfo.Id.MINIMAL_CLASS:使用类名的简短版本。

  • JsonTypeInfo.Id.NAME:使用自定义的类型名称。

  • JsonTypeInfo.Id.CUSTOM自定义处理器处理

JsonTypeInfo.Id.NONE

这里简化一下代码方便修改测试:

在object属性上面添加@JsonTypeInfo注解,并指定为JsonTypeInfo.Id.NONE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class product {
public int price;
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", price, name, object == null ? "null" : object);
}
}

test类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class test {
public static void main(String[] args) throws IOException {
Product product = new Product();
product.price=10;
product.name="苹果";
product.object=new hacker();
ObjectMapper mapper = new ObjectMapper();

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

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

}

运行结果:

1
2
{"price":10,"name":"苹果","object":{"skill":"hacker"}}
Person.age=10, Person.name=苹果, {skill=hacker}

我们这里把注解给注释掉:

再次运行

image-20231028175109573

可以看到两个结果是一样的,所以JsonTypeInfo.Id.NONE时没有任何处理的,和没有使用注解的效果是一样的。

JsonTypeInfo.Id.CLASS

修改注解的值为JsonTypeInfo.Id.CLASS

1
2
{"price":10,"name":"苹果","object":{"@class":"blog.hacker","skill":"hacker"}}
Person.age=10, Person.name=苹果, blog.hacker@4b9e13df

可以看到与上面的输出相比多了"@class":"blog.hacker",包含了具体的类的信息。可以对指定的类进行序列化和反序列化,我们根据输出结果可以看到成功对object属性的hacker类进行了序列化和反序列化。

JsonTypeInfo.Id.MINIMAL_CLASS

1
2
{"price":10,"name":"苹果","object":{"@c":"blog.hacker","skill":"hacker"}}
Person.age=10, Person.name=苹果, blog.hacker@475530b9

与上面的输出做对比可以看到基本产不多但是@class换成了@c,官方描述缩短了相关类名,但是真实效果和JsonTypeInfo.Id.CLASS

是一样的,能够成功对我们指定的类型进行序列化和反序列化。

JsonTypeInfo.Id.NAME

image-20231028180747834

可以看到在序列化输出的json对象中多了一个@type,但是具体的包名,类名却没有,这导致在后面反序列化的时候抛出了异常,无法正常反序列化。那么根据这个结果我们可以知道这个值是不能在反序列化利用的。

JsonTypeInfo.Id.CUSTOM

image-20231028181918781

这个值在进行序列化的时候就抛出了异常。因为这个值是提供给用户自定义的,没有办法直接使用,需要手动写一个解析器才能配合使用这个值,直接调用就会抛出异常。

那这里其实根据我们前面的测试,,当@JsonTypeInfo注解设置为如下值之一并且修饰的是Object类型的属性时,我们可以使用JsonTypeInfo.Id.CLASSJsonTypeInfo.Id.MINIMAL_CLASS可以利用来触发jackson反序列化漏洞。

六、jackson反序列化中类属性方法的调用

在上面我们学习了jackson是如何通过JacksonPolymorphicDeserialization机制下的多态的序列化和反序列化

然后我们这里来看一下jackson在JacksonPolymorphicDeserialization机制下类属性方法的调用。

DefaultTyping场景下:

这里对Product代码稍微做一个修改,每个类方法属性都加上一个输出语句,便于我们判断是否调用

Product类:

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
62
63
64
65
66
67
68
69
70
package blog;

public class Product {
private String name;
private double price;
private String description;
public Object object;
public productjie productjie;


public Product(String name, double price, String description,Object object,productjie productjie) {
this.name = name;
this.price = price;
this.description = description;
this.object=object;
this.productjie=productjie;
System.out.println("这里是有参构造");
}


public Product(){

System.out.println("这里是无参构造");
}


// Getters and Setters for the class properties
public String getName() {
System.out.println("这里是getname方法");
return name;

}

public void setName(String name) {
this.name = name;
System.out.println("这里调用了setname方法");
}

public double getPrice() {
System.out.println("这里调用了getprice方法");
return price;
}

public void setPrice(double price) {
System.out.println("这里调用了setprice方法");
this.price = price;
}

public String getDescription() {
System.out.println("这里调用了getdescripition方法");
return description;
}

public void setDescription(String description) {
System.out.println("这里调用了setdescripition方法");

this.description = description;
}

@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", description='" + description + '\'' +
", object=" + object +
", productjie=" + productjie +
'}';
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

import com.fasterxml.jackson.databind.ObjectMapper;


public class test {
public static void main(String[] args) throws Exception {
hacker[] hacker = new hacker[2];
hacker[0]=new hacker();
hacker[1]=new hacker();
Product product=new Product("苹果",10,"优质",new hacker(),new myproductjie());
ObjectMapper objectMapper =new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS);
String w = objectMapper.writeValueAsString(product);
System.out.println("这里是序列化:");
System.out.println(w);
Product r=objectMapper.readValue(w, Product.class);
System.out.println("这里是反序列化");
System.out.println(r);


}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
这里是有参构造
这里是getname方法
这里调用了getprice方法
这里调用了getdescripition方法
这里是序列化:
{"name":"苹果","price":10.0,"description":"优质","object":["blog.hacker",{"skill":"hacker"}],"productjie":["blog.myproductjie",{"price":0}]}
这里是无参构造
这里调用了setname方法
这里调用了setprice方法
这里调用了setdescripition方法
这里是反序列化
Product{name='苹果', price=10.0, description='优质', object=blog.hacker@6cc4c815, productjie=blog.myproductjie@3a82f6ef}

根据上面运行结果的输出,我们可以看到在序列化的时候调用了构造函数和get方法

但是在反序列化的时候调用了构造方法和set方法

我们继续分析在使用注解的情况下:

@JsonTypeInfo注解场景下:

修改Product代码在productjie属性上面添加注解属性

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
62
63
64
65
66
67
68
69
70
71
72
73
74
package blog;

import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class Product {
private String name;
private double price;
private String description;
public Object object;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public productjie productjie;


public Product(String name, double price, String description,Object object,productjie productjie) {
this.name = name;
this.price = price;
this.description = description;
this.object=object;
this.productjie=productjie;
System.out.println("这里是有参构造");
}


public Product(){

System.out.println("这里是无参构造");
}


// Getters and Setters for the class properties
public String getName() {
System.out.println("这里是getname方法");
return name;

}

public void setName(String name) {
this.name = name;
System.out.println("这里调用了setname方法");
}

public double getPrice() {
System.out.println("这里调用了getprice方法");
return price;
}

public void setPrice(double price) {
System.out.println("这里调用了setprice方法");
this.price = price;
}

public String getDescription() {
System.out.println("这里调用了getdescripition方法");
return description;
}

public void setDescription(String description) {
System.out.println("这里调用了setdescripition方法");

this.description = description;
}

@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", description='" + description + '\'' +
", object=" + object +
", productjie=" + productjie +
'}';
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class test {
public static void main(String[] args) throws Exception {
Product product=new Product("苹果",10,"优质",new hacker(),new myproductjie());
ObjectMapper objectMapper =new ObjectMapper();
String w = objectMapper.writeValueAsString(product);
System.out.println("这里是序列化:");
System.out.println(w);
Product r=objectMapper.readValue(w, Product.class);
System.out.println("这里是反序列化");
System.out.println(r);


}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
这里是有参构造
这里是getname方法
这里调用了getprice方法
这里调用了getdescripition方法
这里是序列化:
{"name":"苹果","price":10.0,"description":"优质","object":{"skill":"hacker"},"productjie":{"@class":"blog.myproductjie","price":0}}
这里是无参构造
这里调用了setname方法
这里调用了setprice方法
这里调用了setdescripition方法
这里是反序列化
Product{name='苹果', price=10.0, description='优质', object={skill=hacker}, productjie=blog.myproductjie@6767c1fc}

我们看到这里调用的类属性方法是和上面在DefaultTyping场景下的结果是一样的。

动态调试

那么根据上面的测试,jackson在进行反序列化的过程中首先是通过构造函数生成实列,然后调用调用set方法设置实例的属性值,我们这里打断点动态调试一下这个过程

image-20231028185954048

前面一部分代码都是读取json数据对象

然后调用deserialize方法对json对象进行反序列化操作

我们这里跟进去分析一下:

image-20231028190131938

首先通过p.isExpectedStartObjectToken()检查是否是在处理一个json对象

然后调用vanillaDeserialize进行处理

跟进这个方法

image-20231028191511962

1
final Object bean = _valueInstantiator.createUsingDefault(ctxt);

首先这里是先通过createUsingDefault这个方法来调用指定类的无参构造函数来生成类实例

那么到这里我们也就知道了无参构造函数在jackson反序列化的调用过程

跟进这个方法

image-20231028191805282

这里调用了call方法

跟进

image-20231028191910654

进入call方法我们可以看到调用了newInstace()方法对_constructor进行实例化,这里的_constructor我们根据调试信息可以看到就是我们定义的product类

那么根据上面的调试分析我们可以知道实例化的过程

然后跳出createUsingDefault方法进行向下分析代码

image-20231028192743317

我们这里简单分析一些代码

if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)): 这个条件检查 JSON 标记(token)是否是字段名。在 JSON 中,字段名是一个字符串,例如 "name": "John" 中的 "name" 就是字段名。

String propName = p.getCurrentName();: 如果 JSON 标记是字段名,那么这一行代码会获取当前字段名,并将其存储在 propName 变量中。

然后可以看到是一个dowhile循环用于处理对象的各个属性。在每次迭代中,它会获取下一个字段名,并继续处理,直到没有更多字段名为止。

image-20231028193012521

如果找到了与字段名匹配的属性,那么它会进入这个条件块。在这个块中,它尝试使用属性的 deserializeAndSet 方法来将 JSON 数据中的值设置到 Java 对象的属性上。

我们这里跟进到这个deserializeAndSet方法

image-20231028193106651

可以看到这里也调用了deserialize方法

跟进到这个deserialize方法:

image-20231028193708798

可以看到这里有两个处理逻辑

第一个逻辑用于处理 JSON 中属性值为 null 的情况,将其映射为 Java 对象的属性值为 null。

第二个if判断

首先,它检查 _valueTypeDeserializer 是否为非空,即是否存在类型信息。如果存在类型信息,说明该属性值包含了类型信息。

如果存在类型信息,它会调用 _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer) 来执行反序列化操作。这个方法传递了当前的 JSON 解析器 p、反序列化上下文 ctxt 以及类型信息 _valueTypeDeserializer

如果 _valueTypeDeserializer 为空,说明属性值不包含类型信息,它将简单地调用 _valueDeserializer.deserialize(p, ctxt) 来执行属性值的反序列化。这将根据属性值的实际类型执行反序列化。

image-20231028194113889

我们的第一个属性是name,所以可以看到这里调用的是StringDeserializerl来获取name属性的值

image-20231028194312205

当获取到属性值后回到deserializeAndSet方法调用属性的setter方法来对实例的属性值进行设置。

image-20231028194416544

后面就还是进行dowhile设置 后面price和description的值。

但是hacker是一个包含类名的数组

image-20231028205254249

可以看到这里的类型不为空,也就是_valueTypeDeserializer的值不为空

然后这里调用了_valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);

image-20231028205552322

跟进去这个处理函数

一直跟进到调用_deserialize方法

image-20231028205819239

1
tring typeId = _locateTypeId(p, ctxt);

在这里可以看到调用_locateTypeId()函数获取到了类型为:blog.hacker

image-20231028210008938

然后这里根据取到的类型_findDeserializer方法来寻找对应的反序列化器

image-20231028210326672

我们跟进去获取到了反序列化器并返回

image-20231028210635389

这里其实就是获取到了反序列化器

我们这里跟进去看一下

image-20231029143031810

我们这次处理的序列化对象Object是有类型信息的,所以这里会调用_valueDeserializer.deserializeWithType进行处理

跟进到deserializeWithType方法

image-20231029143803433

这里就是jackson用于处理多态对象的地方,typeDeserializer会检查JSON数据中的类型信息,然后选择正确的Java类进行反序列化。这允许在JSON中表示多态数据结构,以便将其准确地还原为Java对象。

我们继续跟进

image-20231029143946516

然后继续调用 _deserialize进行反序列化处理

image-20231029144113333

具体的反序列化还是调用这个deserialize方法,然后将反序列化的值返回

继续跟进到这个deserialize方法

image-20231029144253448

可以看到又重新跳回了deserialize调用了vanillaDeserialize方法来解析数组内的内容

image-20231029144347375

其中调用createUsingDefault()函数的时候会调用到hakcer类的无参构造函数来新建hacker类对象

image-20231029144529401

然后再次调用deserializeAndSet()函数获取该属性值并设置到该实例中:

image-20231029144709046

image-20231029144725701

然后通过反射获取到hacker类的属性名,然后调用set方法将值设置给hacker对象

因为我在product类里面也写了一个object的set方法

image-20231029144904617

所以会在来一次调用这个set方法

image-20231029145008454

image-20231029144945486

那么其实上面对于object类型的反序列化首先通过deserializeWithType方法去获取到合适的反序列化器,然后再次调用deserialize方法去进行json对象的反序列化,调用无参构造函数,然然后就是通过反射机制调用该属性的setter方法进行设置。

那么根据上面的分析,我们大概可以总结一下jackson的反序列化过程:先通过无参构造类函数生成目标类的实例,然后根据属性值是否是数组,也就是是否带有类名,如果目标是一个不带有类名的就直接调用deserializeAndSet方法调用set方法将属性值设置,如果是带有类名的,也就是类似于我们上面的数组类型,会先调用deserializeWithType去寻找合适的发序列化器,进行反序列化,然后再构造对象,调用deserializeAndSet方法。

七、jackson反序列化漏洞

那么根据我们上面的分析,在jackson的反序列化中,若是调用了enableDefaultTyping()函数或使用@JsonTypeInfo注解指定反序列化得到的类的属性为JsonTypeInfo.Id.CLASSJsonTypeInfo.Id.MINIMAL_CLASS就会调用该属性类的构造函数和set方法。那么现在出现了一个问题,如果我们在构造函数或者set方法中写入一些危险的操作的方法或者函数,那么当反序列化调用构造函数和set方法时就会造成危险利用。这就是我们这次要学习的漏洞jackson反序列化漏洞。

根据上面的分析我们可以得到jackson反序列化的利用前提条件:(满足其一就可)

  • 调用了ObjectMapper.enableDefaultTyping()函数;
    对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS或者JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    分析到这里,我们可以知道这个漏洞的原因就是因为这个jackson在解决多态问题使用的JacksonPolymorphicDeserialization机制存在的问题,导致在反序列化时会调用构造函数和set方法。

    我们这里通过前面的测试代码,在set方法或者构造函数中添加一个弹出计算器的操作来演示这个操作:

    我们这里修改myproductjie这个继承接口的类就行,我在他的一个set方法中添加一个弹出计算器的命令:

    myproductjie类:

    public class myproductjie implements productjie { int price; public myproductjie(){ System.out.println("这里调用了myproductjie的无参构造方法"); } @Override public void setprice(int price) throws IOException { Runtime.getRuntime().exec("calc"); this.price=price; System.out.println("这里是setprice"); } @Override public int getprice() { return price; }

}

1
2
3

productjie

public interface productjie {
public void setprice(int price) throws IOException;
public int getprice();
}

1
2
3
4
5
6
7
8
9
10
11
12
13

运行测试:

![image-20231029173820717](https://jublog.oss-cn-beijing.aliyuncs.com/image/image-20231029173820717.png)

可以看到成功弹出了计算器。

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

那我们直接这里修改:

hacker类:

package blog1;

public class hacker {
public String cmd;

public hacker(){

}
public hacker(String cmd){
this.cmd=cmd;

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

}

product类:

import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class Product {
public int price;
public String name;
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Object object;
public Product() {
System.out.println(“Product构造函数”);
}

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

}

测试类:
public class test {
public static void main(String[] args) throws IOException {
Product product = new Product();
product.price=10;
product.name=”苹果”;
product.object=new hacker(“calc”);
ObjectMapper mapper = new ObjectMapper();

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

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

}

运行测试:

![image-20231029183331719](https://jublog.oss-cn-beijing.aliyuncs.com/image/image-20231029183331719.png)

也弹出来计算器。

# 八、总结

上面我们分析和调试了jackson反序列化漏洞产生的原因,也就是因为jackson解决多态问题反序列化的JacksonPolymorphicDeserialization机制存在问题,当调用`enableDefaultTyping()`函数或使用`@JsonTypeInfo`注解指定反序列化得到的类的属性为`JsonTypeInfo.Id.CLASS`或`JsonTypeInfo.Id.MINIMAL_CLASS`就会调用该属性类的构造函数和set方法,导致我们可以反序列化利用。

但是我们上面的测试都是自己写了一个利用类,在真实环境中,是不可能留有一个可以执行命令的后门的,所以我们这里只是简单了一下jackson反序列化的原理和流程。后门我们将分析与其相关的几个cve和利用链。
  • 标题: jackson反序列化原理学习
  • 作者: GTL-JU
  • 创建于: 2023-10-29 18:43:20
  • 更新于: 2023-10-30 10:08:37
  • 链接: https://gtl-ju.github.io/2023/10/29/jackson反序列化/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。