Java反序列化原理篇02-原生反序列化流程分析

0x00 前言

推荐先看一下笔者的上一篇文章:Java反序列化原理篇01-原生序列化流程分析

0x01 demo

给Person类写一个readObject方法,先注释该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Person implements Serializable {
public String name;
private int age;

private transient String hobby = "Handsome";

public Person(){}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

// private void readObject(ObjectInputStream inputStream) throws Exception {
// System.out.println("已触发自定义的readObject()");
// }

}

主类Main:

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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main {
public static void main(String[] args) throws Exception{
Person person = new Person("Pax", 666);
byte[] serialize = serialize(person);
Person deserialize = (Person) deserialize(serialize);
}

public static <T> byte[] serialize(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> T deserialize(byte[] ser) throws Exception {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ser));
return (T) in.readObject();
}
}

0x02 创建对象输入流

断点打在如下位置:

image-20250411163249535

跟进,该构造方法与对象输出流的构造方法类似:

image-20250411163426088

属性名 类型 作用
bin ObjectInputStream.BlockDataInputStream 与bout类似,存放反序列化的对象数据
handles HandleTable 一个哈希表,表示从对象到引用的映射
vlist ObjectInputStream.ValidationList 提供一个callback操作的验证集合
serialFilter ObjectInputFilter 获取序列化过滤器
enableOverride boolean 决定在反序列化时选用readObjectOverride方法还是readObject方法

0x03 readObject方法

流程到这里:

image-20250411164732038

先是enableOverride的判断。然后创建反序列化深度句柄outerHandle,用于在处理嵌套对象时,能够跟踪外层对象的句柄,以便正确地进行反序列化和管理对象之间的关系。这样可以确保在读取嵌套结构时,不会混淆不同层级的对象。

image-20250411164835396

跟进readObject0方法:

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
75
76
77
78
79
80
81
82
83
84
85
86
87
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}

byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}

depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();

case TC_REFERENCE:
return readHandle(unshared);

case TC_CLASS:
return readClass(unshared);

case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);

case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));

case TC_ARRAY:
return checkResolve(readArray(unshared));

case TC_ENUM:
return checkResolve(readEnum(unshared));

case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));

case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);

case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}

case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

流程走到while循环,该循环跳过所有的 TC_RESET 标记,并调用 handleReset 方法来处理重置操作。循环结束后,tc 变量保存了第一个非 TC_RESET 的字节

image-20250411165633794

depth 是一个计数器,用于跟踪当前反序列化调用的深度。

image-20250411165837153

接着就是一个switch分支,根据tc的值不同,调用对应的处理方法。重点看看tc = TC_OBJECT的逻辑:

image-20250411170312306

先进入readOrdinaryObject方法。首先会再次判断读到的标识是不是TC_OBJECT,如果不是,那么直接抛出InternalError错误:

image-20250411170553931

紧接着:

1
ObjectStreamClass desc = readClassDesc(false);

跟进。该方法正如快速文档所说:

读取并返回一个类描述符(可能为 null)。同时,将 passHandle 设置为这个类描述符的句柄。如果这个类描述符无法在本地虚拟机中找到对应的类,就会抛出一个 ClassNotFoundException。

image-20250411170859986

由于Person类是一个常规的Class,走到readNonProxyDesc方法,开头就是一个标识符判断:

image-20250411171729458

然后判断读取模式是什么,如果是unshared,那么从handles对象的映射中读取一个新的desc,如果不是unshared,那么从unsharedMarker中读取对应的对象。

image-20250411171842095

然后进入readClassDescriptor方法:

image-20250411171925978

该方法的返回值类型ObjectStreamClass,或者说desc,实际上就与Person.class有关,分装了Person类的相关信息。

跟进readNonProxy方法,获取类名,序列化版本号,是否为代理:

image-20250411173112565

接着,从字节流中读取每一个字段的信息。这些字段信息包括:TypeCode、fieldName、fieldType。

image-20250411173444978

读取结束以后会依次跳出readNonProxy、readClassDescriptor方法,在获得类信息后会返回readNonProxyDesc,接着走:

image-20250411173817626

关注这个判断:

1
if ((cl = resolveClass(readDesc)) == null) 

这里要拓展一下resolveClass方法和annotateClass方法:

annotateClass是提供给子类实现的方法,通常默认情况下这个方法什么也不做,与此类似的还有ObjectInputStream中的resolveClass方法。

实际上,ObjectInputStream中的resolveClass、resolveProxyClass、resolveObject这三个方法对应着ObjectOutputStream中定义的annotateClass、annotateProxyClass和replaceObject方法,如果ObjectOutputStream的子类重写了这的三个方法,那么要求ObjectInputStream的子类也必须重写这三个方法对应的resolve方法。

在这里,resolveClass方法会根据字节流中读取的类描述信息加载本地类,加载的时候用到的就是我们平时用的Class.forName()的方法,实际上反序列化漏洞根本的原因就是在这里加载了Runtime类,然后执行了exec()方法。

接着调用序列化筛选器:

1
filterCheck(cl, -1);

接着,会调用ObjectStreamClass中的initNonProxy方法:

1
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));

初始化完毕后会调用handles的finish方法完成引用Handle的赋值操作:

1
handles.finish(descHandle);

终于在经过了这么多方法的层层调用后,拿到了描述类信息desc。然后和序列化开始时类似,同样检测当前处理的对象是否是一个可反序列化的对象(checkDeserialize()),如果是,那么就从系统中读取当前Java对象所属类的描述信息(也叫做类元数据信息)。

image-20250411174820936

接着初始化一个Person类对象:

image-20250411174916466

然后经过几个简单的判断后会调用handles的finish方法完成引用Handle的赋值操作,最后将结果赋值给passHandle成员属性。

image-20250411175048001

下面就是反序列化对属性赋值的操作:

image-20250411175149577

跟进,如果反序列化对象的类存在readObject方法,则反射调用该方法,否则调用默认的defaultReadFields方法:

image-20250411175402444

defaultReadFields方法的赋值过程与序列化对应的赋值过程类似,不叙:

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
private void defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}

int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
bin.readFully(primVals, 0, primDataSize, false);
if (obj != null) {
desc.setPrimFieldValues(obj, primVals);
}

int objHandle = passHandle;
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}

最后调用vlist成员的doCallbacks来执行完成过后的回调逻辑,然后结束所有的序列化流程。

至此,结束。

0x04 参考

Java反序列化流程总结