前言

Java 序列化是将对象状态转换为字节流的过程,反序列化则是从字节流重建对象。这两个过程是 Java RMI、JMX、JNDI 等分布式通信的基石,也是 Java 安全史上最严重的一类漏洞的根源。

2015 年 FoxGlove Security 公开的 WebLogic 反序列化漏洞(CVE-2015-4852)标志着 Java 反序列化攻击进入公众视野。此后,ysoserial 工具集的出现将这一攻击模式系统化,Spring、Shiro、Fastjson、Jackson 等主流框架无一幸免。

理解 Java 序列化的底层实现——ObjectOutputStreamObjectInputStream 的内部调用链——是理解反序列化漏洞利用(Gadget Chain)的前提。本文从 JDK 源码级别追踪完整的序列化与反序列化流程,覆盖 writeObject/readObjectExternalizabletransient/static 行为、serialVersionUIDwriteReplace/readResolve 以及 ObjectInputValidation 等所有关键机制。


一、序列化机制深度追踪

1.1 基本用法

1
2
3
4
5
public static void serialize(Object o) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("bin.ser"));
oos.writeObject(o);
oos.close();
}

1.2 writeObject 方法入口

ObjectOutputStream.writeObject() 是所有序列化的入口,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final void writeObject(Object var1) throws IOException {
if (this.enableOverride) { // 是否自定义序列化方式
this.writeObjectOverride(var1); // 调用 var1 的自定义序列化方法
// 这是一个钩子方法,允许子类覆盖默认的序列化行为
} else {
// 默认序列化逻辑
try {
this.writeObject0(var1, false);
} catch (IOException var3) {
if (this.depth == 0) {
// 如果深度为0(最外层序列化),调用 writeFatalException
this.writeFatalException(var3);
// 这个方法将异常信息写入输出流,标记序列化失败
}
throw var3;
}
}
}

关键分支:如果 enableOverride = true,走自定义子类的 writeObjectOverride;否则走标准流程 writeObject0

1.3 writeObjectOverride 钩子方法

1
2
protected void writeObjectOverride(Object var1) throws IOException {
}

ObjectOutputStream 中这是一个空方法,留给子类覆盖。这是第一种自定义序列化的方式——继承 ObjectOutputStream 并重写 writeObjectOverride

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
class SignedOutputStream extends ObjectOutputStream {
public SignedOutputStream(OutputStream out) throws IOException {
this.enableOverride = true; // 这里启用覆盖
super(out);
}

@Override
protected void writeObjectOverride(Object obj) throws IOException {
// 1. 先序列化对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream temp = new ObjectOutputStream(baos);
temp.writeObject(obj);
byte[] data = baos.toByteArray();

// 2. 添加签名
byte[] signature = sign(data);

// 3. 写入:数据 + 签名
writeInt(data.length);
write(data);
write(signature);
}
}

// 使用时必须用自定义流
SignedOutputStream sos = new SignedOutputStream(...);
sos.writeObject(anyObject); // 调用 writeObjectOverride

1.4 writeObject0 方法——核心调度

之前说的第二种自定义序列化方法就在这个方法里面。当 obj(被序列化的对象)自身定义了 private void writeObject(ObjectOutputStream out) 方法时,writeObject0 内部的调用链最终会通过反射调用它。

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
boolean var3 = this.bout.setBlockDataMode(false);
++this.depth;

try {
if ((var1 = this.subs.lookup(var1)) != null) {
// ... 处理替换对象
}

while (true) {
ObjectStreamClass var7 = ObjectStreamClass.lookup(var6, true);
Class var8;

if (!var7.hasWriteReplaceMethod()
|| (var1 = var7.invokeWriteReplace(var1)) == null
|| (var8 = var1.getClass()) == var6) {

if (var1 instanceof String) {
this.writeString((String) var1, var2);
return;
} else if (var6.isArray()) {
this.writeArray(var1, var7, var2);
return;
} else {
if (var1 instanceof Enum) {
this.writeEnum((Enum) var1, var7, var2);
} else {
if (!(var1 instanceof Serializable)) {
if (extendedDebugInfo) {
throw new NotSerializableException(
var6.getName() + "\n" + this.debugInfoStack.toString());
}
throw new NotSerializableException(var6.getName());
}

this.writeOrdinaryObject(var1, var7, var2);
}
return;
}
}
// ... writeReplace 循环
}
}

这段代码展示了 writeObject0 的分发逻辑:

检查条件 处理分支
var1 instanceof String writeString()
var6.isArray() writeArray()
var1 instanceof Enum writeEnum()
!(var1 instanceof Serializable) NotSerializableException
普通 Serializable 对象 writeOrdinaryObject()

writeReplace 检查: 在进入类型分支之前,先检查 hasWriteReplaceMethod()。如果对象定义了 writeReplace() 方法,JDK 会先调用它获取替换对象,然后用替换对象重新走一遍流程。这是序列化层面的对象替换机制。

1.5 writeOrdinaryObject —— 写入普通对象

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
private void writeOrdinaryObject(Object var1, ObjectStreamClass var2, boolean var3)
throws IOException {
if (extendedDebugInfo) {
this.debugInfoStack.push(
(this.depth == 1 ? "root " : "")
+ "object (class \"" + var1.getClass().getName()
+ "\", " + var1.toString() + ")");
}

try {
var2.checkSerialize();
this.bout.writeByte(115); // TC_OBJECT 标记
this.writeClassDesc(var2, false); // 写入类描述符
this.handles.assign(var3 ? null : var1);
if (var2.isExternalizable() && !var2.isProxy()) {
this.writeExternalData((Externalizable) var1);
} else {
this.writeSerialData(var1, var2);
}
} finally {
if (extendedDebugInfo) {
this.debugInfoStack.pop();
}
}
}

这里出现第二个分支:Externalizable 对象走 writeExternalData(),普通 Serializable 对象走 writeSerialData()

1.6 writeSerialData —— 反射调用 writeObject

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 writeSerialData(Object var1, ObjectStreamClass var2) throws IOException {
ObjectStreamClass.ClassDataSlot[] var3 = var2.getClassDataLayout();

for (int var4 = 0; var4 < var3.length; ++var4) {
ObjectStreamClass var5 = var3[var4].desc;
if (var5.hasWriteObjectMethod()) {
PutFieldImpl var6 = this.curPut;
this.curPut = null;
SerialCallbackContext var7 = this.curContext;
if (extendedDebugInfo) {
this.debugInfoStack.push(
"custom writeObject data (class \"" + var5.getName() + "\")");
}

try {
this.curContext = new SerialCallbackContext(var1, var5);
this.bout.setBlockDataMode(true);
var5.invokeWriteObject(var1, this); // ★ 反射调用
this.bout.setBlockDataMode(false);
this.bout.writeByte(120); // TC_ENDBLOCKDATA
} finally {
this.curContext.setUsed();
this.curContext = var7;
if (extendedDebugInfo) {
this.debugInfoStack.pop();
}
}
this.curPut = var6;
} else {
this.defaultWriteFields(var1, var5);
}
}
}

hasWriteObjectMethod() 检查类是否定义了 private void writeObject(ObjectOutputStream) 方法。如果有,则通过反射调用。

1.7 invokeWriteObject —— 反射入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void invokeWriteObject(Object var1, ObjectOutputStream var2)
throws IOException, UnsupportedOperationException {
this.requireInitialized();
if (this.writeObjectMethod != null) {
try {
this.writeObjectMethod.invoke(var1, var2);
// 反射调用,var2 就是原始的 ObjectOutputStream 对象
} catch (InvocationTargetException var5) {
Throwable var4 = var5.getTargetException();
if (var4 instanceof IOException) {
throw (IOException) var4;
}
throwMiscException(var4);
} catch (IllegalAccessException var6) {
throw new InternalError(var6);
}
} else {
throw new UnsupportedOperationException();
}
}

这就是第二种自定义序列化方式的核心——JDK 通过反射调用被序列化对象的 writeObject(ObjectOutputStream) 方法。

1.8 调试验证

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

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class test implements Serializable {
private String name;
private transient String password;

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 先序列化默认字段
out.writeObject(password + "12345"); // 手动处理敏感字段
}

public static void main(String[] args) throws Exception {
test t = new test();
t.name = "admin";
t.password = "123456";
serialize(t);
}

public static void serialize(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("bin.ser"));
oos.writeObject(o);
oos.close();
}
}

var5.invokeWriteObject(var1, this) 处下断点,可以验证确实走到了自定义的 writeObject 方法。


二、反序列化机制深度追踪

2.1 基本示例(带 writeObject / readObject)

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

import java.io.*;

public class test implements Serializable {
private String name;
private transient String password;

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(password + 12345); // 写入 "a12345612345"
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("调用自定义 readObject 方法");
in.defaultReadObject(); // 反序列化非 transient 字段

// 读取额外数据
String user_pss = (String) in.readObject();
System.out.println("从文件读取的密码字符串: " + user_pss);

// 替换处理
this.password = user_pss.replaceAll("\\d+$", "ccdd");
System.out.println("替换后密码: " + this.password);
}

@Override
public String toString() {
return "test{name='" + name + "', password='" + password + "'}";
}

public static void main(String[] args) throws Exception {
test t = new test();
t.name = "admin";
t.password = "a123456";

System.out.println("原始对象: " + t);
System.out.println("序列化前密码: " + t.password);

serialize(t);

// 反序列化,获取新对象
test deserializedObj = deserialize();

System.out.println("\n反序列化后的对象: " + deserializedObj);
System.out.println("原对象t的密码: " + t.password);
System.out.println("新对象deserializedObj的密码: " + deserializedObj.password);
}

public static void serialize(Object o) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("bin.ser"));
oos.writeObject(o);
oos.close();
System.out.println("序列化完成");
}

public static test deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("bin.ser"));
test obj = (test) ois.readObject(); // 接收返回值
ois.close();
return obj; // 返回反序列化的对象
}
}

2.2 readObject 方法入口

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
public final Object readObject() throws IOException, ClassNotFoundException {
if (this.enableOverride) {
return this.readObjectOverride();
} else {
int var1 = this.passHandle;

Object var4;
try {
Object var2 = this.readObject0(false);
this.handles.markDependency(var1, this.passHandle);
ClassNotFoundException var3 = this.handles.lookupException(this.passHandle);
if (var3 != null) {
throw var3;
}

if (this.depth == 0) {
this.vlist.doCallbacks(); // ★ 触发 validation 回调
}

var4 = var2;
} finally {
this.passHandle = var1;
if (this.closed && this.depth == 0) {
this.clear();
}
}

return var4;
}
}

注意 this.vlist.doCallbacks() — 当 depth == 0(最外层反序列化完成)时,会触发所有注册的 ObjectInputValidation 回调。

2.3 readObject0 —— 分发入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Object readObject0(boolean var1) throws IOException {
boolean var2 = this.bin.getBlockDataMode();
// ...
byte var8;
while ((var8 = this.bin.peekByte()) == 121) {
this.bin.readByte();
this.handleReset();
}

++this.depth;
try {
switch (var8) {
case 115: // TC_OBJECT
Object var13 = this.checkResolve(this.readOrdinaryObject(var1));
return var13;
// ... 其他 case
}
} finally {
--this.depth;
this.bin.setBlockDataMode(var2);
}
}

checkResolve() 会调用 readResolve() 方法(如果对象定义了的话),这是反序列化层面的对象替换。

2.4 readOrdinaryObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Object readOrdinaryObject(boolean var1) throws IOException {
if (this.bin.readByte() != 115) {
throw new InternalError();
} else {
ObjectStreamClass var2 = this.readClassDesc(false);
var2.checkDeserialize();
Class var3 = var2.forClass();
if (var3 != String.class && var3 != Class.class
&& var3 != ObjectStreamClass.class) {
// ...
if (var2.isExternalizable()) {
this.readExternalData((Externalizable) var4, var2);
} else {
this.readSerialData(var4, var2);
}
// ...
return var4;
} else {
throw new InvalidClassException("invalid class descriptor");
}
}
}

2.5 readSerialData —— 反射调用 readObject

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
private void readSerialData(Object var1, ObjectStreamClass var2) throws IOException {
ObjectStreamClass.ClassDataSlot[] var3 = var2.getClassDataLayout();

for (int var4 = 0; var4 < var3.length; ++var4) {
ObjectStreamClass var5 = var3[var4].desc;
if (var3[var4].hasData) {
if (var1 != null && this.handles.lookupException(this.passHandle) == null) {
if (var5.hasReadObjectMethod()) {
SerialCallbackContext var6 = this.curContext;
if (var6 != null) {
var6.check();
}

try {
this.curContext = new SerialCallbackContext(var1, var5);
this.bin.setBlockDataMode(true);
var5.invokeReadObject(var1, this);
// var5 此时就是被反序列化的对象
}
// ...
}
}
}
}
}

hasReadObjectMethod() 检查类是否定义了 private void readObject(ObjectInputStream) 方法。如果有,通过反射调用。

2.6 invokeReadObject —— 反射入口

1
2
3
4
5
6
7
8
9
10
void invokeReadObject(Object var1, ObjectInputStream var2)
throws IOException, UnsupportedOperationException {
this.requireInitialized();
if (this.readObjectMethod != null) {
try {
this.readObjectMethod.invoke(var1, var2);
}
// ...
}
}

三、自定义序列化方式汇总

方式 实现 适用场景
继承 ObjectOutputStream 重写 writeObjectOverride() 在流层面添加加密、签名等逻辑
实现 writeObject / readObject private void writeObject(ObjectOutputStream) / private void readObject(ObjectInputStream) 自定义字段序列化逻辑、加密敏感字段
实现 Externalizable writeExternal(ObjectOutput) / readExternal(ObjectInput) 完全控制序列化格式
writeReplace / readResolve 对象替换 单例保护、代理模式
ObjectInputValidation validateObject() 反序列化后校验对象合法性

3.1 Externalizable 接口

ExternalizableSerializable 的子接口,提供两个抽象方法让用户完全控制序列化逻辑:

1
2
3
4
5
6
public interface Externalizable extends Serializable {
// 将要序列化的对象属性通过 var1.writeXxx() 写入到序列化流中
void writeExternal(ObjectOutput var1) throws IOException;
// 将要反序列化的对象属性通过 var1.readXxx() 读出来
void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
}

继承关系:

1
2
3
4
5
java.io 包
├── ObjectOutput (接口)
├── ObjectInput (接口)
├── ObjectOutputStream (实现 ObjectOutput)
└── ObjectInputStream (实现 ObjectInput)

示例:

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
public class Person implements Externalizable {

private static final long serialVersionUID = -7424420983806112577L;
private String name;
private int age;

/*
* 实现了 Externalizable 这个接口需要提供无参构造,
* 在反序列化时会检测
*/
public Person() {
System.out.println("Person: empty");
}

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

@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("person writeExternal...");
out.writeObject(name);
out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
System.out.println("person readExternal...");
name = (String) in.readObject();
age = in.readInt();
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

测试类:

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
public class ExternalizableMain {
private static final String FILE_PATH = "../person.bin";

public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("zhangsan", 15);
System.out.println(person.toString());
serializable(person, FILE_PATH);
System.out.println("============反序列化=============");
person = (Person) deserializable(FILE_PATH);
System.out.println(person.toString());
}

private static void serializable(Object o, String path) throws IOException {
FileOutputStream boas = new FileOutputStream(path);
ObjectOutputStream oos = new ObjectOutputStream(boas);
oos.writeObject(o);
oos.close();
boas.close();
}

private static Object deserializable(String path) throws IOException, ClassNotFoundException {
ObjectInputStream bis = new ObjectInputStream(new FileInputStream(path));
Object obj = bis.readObject();
return obj;
}
}

ObjectOutputStream 在序列化一个对象时,会检查该对象是否实现了 Externalizable 接口。如果实现了,序列化机制就会采用 Externalizable 的规则来处理,而非默认的 Serializable 接口的自动序列化机制。

Externalizable vs Serializable 关键区别:

特性 Serializable Externalizable
序列化方式 自动(通过反射) 手动实现 writeExternal/readExternal
无参构造 不需要 必须提供 public 无参构造
性能 使用反射,较慢 手动控制,更快
灵活性 有限(通过 transient 控制) 完全自定义

四、序列化特性与注意事项

4.1 父类未实现 Serializable 时的行为

父类没有实现 Serializable 接口,子类实现了,则在子类反序列化时,父类的无参构造方法会被调用

父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Animal {
private String color;

public Animal() { // 没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}

public Animal(String color) {
this.color = color;
System.out.println("调用 Animal 有 color 参数的构造");
}

@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}

子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}

public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}

@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' + super.toString() + '\'' +
'}';
}
}

测试类:

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
public class SuperMain {
private static final String FILE_PATH = "./super.bin";

public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}

private static void serializeAnimal() throws Exception {
BlackCat black = new BlackCat("black", "我是黑猫");
System.out.println("序列化前:" + black.toString());
System.out.println("=================开始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(black);
oos.flush();
oos.close();
}

private static void deserializeAnimal() throws Exception {
System.out.println("=================开始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
BlackCat black = (BlackCat) ois.readObject();
ois.close();
System.out.println(black);
}
}

输出结果:

1
2
3
4
5
6
7
调用 Animal 有 color 参数的构造
调用黑猫有 color 参数的构造
序列化前:BlackCat{name='我是黑猫'Animal{color='black'}'}
=================开始序列化================
=================开始反序列化================
调用 Animal 无参构造
BlackCat{name='我是黑猫'Animal{color='null'}'}

结论:

  • 父类 Animal 的无参构造方法被调用(因为没有 Serializable,反序列化时通过构造器初始化)
  • 父类的字段(如 color)因未序列化,反序列化后为 null
  • 子类的字段(如 name)被正常恢复
  • 整个过程仅触发父类的无参构造,子类的构造方法完全被绕过

安全提示: 对于未实现 Serializable 的父类,其构造方法在反序列化时会被执行。这在某些利用场景中可以被触发。

4.2 静态成员变量不可被序列化

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
import java.io.*;

public class StaticSerializationTest implements Serializable {
private static final long serialVersionUID = 1L;

// 实例变量(会被序列化)
private int instanceVar = 10;

// 静态变量(不会被序列化)
public static String staticVar = "Initial Static Value";

public static void main(String[] args) {
// 序列化前打印静态变量
System.out.println("Before Serialization:");
System.out.println("staticVar = " + staticVar); // 输出: Initial Static Value

// 序列化对象
serializeObject();

// 修改静态变量的值(模拟反序列化后的场景)
staticVar = "Modified Static Value";

// 反序列化后打印静态变量
deserializeObject();
}

private static void serializeObject() {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
StaticSerializationTest obj = new StaticSerializationTest();
oos.writeObject(obj);
System.out.println("Object serialized.");
} catch (IOException e) {
e.printStackTrace();
}
}

private static void deserializeObject() {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"))) {
StaticSerializationTest obj = (StaticSerializationTest) ois.readObject();
System.out.println("\nAfter Deserialization:");
System.out.println("instanceVar = " + obj.instanceVar); // 输出: 10
System.out.println("staticVar = " + staticVar); // 输出: Modified Static Value
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

输出证明了静态变量不会被序列化: 反序列化后 staticVar 的值仍然是修改后的 "Modified Static Value",而不是序列化前的 "Initial Static Value"

serialVersionUID 虽然也是 static final 字段,但它是一个特殊情况——JDK 序列化机制会特殊处理它,将其写入序列化流中用于版本校验。

4.3 transient 变量

transient 变量的值不会被序列化保存,反序列化后始终恢复为默认值(引用类型为 null,基本类型为 0/false)。这是 Java 中最基础的序列化控制手段。

如果需要手动处理 transient 字段,可以在 writeObject / readObject 中自定义读写逻辑(如 1.8 和 2.1 节的示例所示)。


五、高级特性

5.1 serialVersionUID

serialVersionUID 是序列化版本控制的核心机制:

1
private static final long serialVersionUID = 1L;
场景 行为
类中没有声明 serialVersionUID JDK 根据类的结构(方法、字段等)自动计算一个 hash 值
序列化和反序列化时的 UID 不一致 抛出 InvalidClassException
声明为 1L(固定值) 只要字段兼容,不同版本的 class 可以互相反序列化

兼容性规则(有相同 serialVersionUID 时):

变化 兼容性
添加新字段 兼容,新字段为默认值
删除字段 兼容,多余数据被忽略
修改字段类型 不兼容,抛出异常
类从非 Serializable 变为 Serializable 不兼容
修改继承关系 不兼容

安全视角: 攻击者通常不关心 serialVersionUID 兼容性,因为 ysoserial 等工具生成的 payload 自身携带类描述符,不依赖目标环境的类定义。但在需要利用目标已有类(如 TemplatesImpl)时,确保 UID 匹配是 payload 成功的前提。

5.2 writeReplace —— 序列化时的对象替换

writeReplace() 方法在序列化时被调用,允许对象在序列化之前替换自身。返回值可以是任意对象,JDK 会对替换后的对象重新走序列化流程。

1
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

典型用法——单例模式保护:

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
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return INSTANCE;
}

// 序列化时不写入 Singleton,而是写入 SingletonProxy
private Object writeReplace() throws ObjectStreamException {
return new SingletonProxy();
}

// 内部代理类
private static class SingletonProxy implements Serializable {
private static final long serialVersionUID = 1L;

// 反序列化时返回真正的单例
private Object readResolve() throws ObjectStreamException {
return Singleton.INSTANCE;
}
}
}

序列化流程:Singleton –writeReplace–> SingletonProxy –序列化–> 字节流

5.3 readResolve —— 反序列化时的对象替换

readResolve() 在反序列化完成时被调用,允许用另一个对象替换反序列化出的对象。

1
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

readObject0 中可以看到调用位置:

1
2
3
case 115:   // TC_OBJECT
Object var13 = this.checkResolve(this.readOrdinaryObject(var1));
return var13;

checkResolve() 内部会反射调用 readResolve()

另一个单例保护示例:

1
2
3
4
public enum SingletonEnum {
INSTANCE;
// 枚举的序列化由 JVM 保证单例,不需要额外处理
}

对于非枚举单例,readResolve 是防止反序列化破坏单例的标准手段:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();

private Singleton() {}

public static Singleton getInstance() { return INSTANCE; }

// 没有这个方法的话,每次反序列化都会创建一个新实例
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}

反序列化流程:字节流 –反序列化–> 新 Singleton 对象 –readResolve–> 返回 INSTANCE

安全视角: readResolve 在 Gadget Chain 中也有利用价值。某些类在 readResolve 中执行危险操作(如 JNDI 查找),可以被用作 sink。但更常见的是,readResolve 返回的对象类型可能与反序列化的对象不同,这在绕过某些类型检查时有用。

5.4 ObjectInputValidation —— 反序列化后校验

ObjectInputValidation 接口允许对象在完全反序列化后(整个对象图构建完毕后)执行验证逻辑:

1
2
3
public interface ObjectInputValidation {
void validateObject() throws InvalidObjectException;
}

readObject 方法中可以看到触发时机:

1
2
3
if (this.depth == 0) {
this.vlist.doCallbacks(); // 触发所有注册的 validation 回调
}

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ValidatedObject implements Serializable, ObjectInputValidation {
private int age;

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// 注册验证回调——在所有对象都反序列化完成后才执行
in.registerValidation(this, 0);
}

@Override
public void validateObject() throws InvalidObjectException {
if (age < 0 || age > 150) {
throw new InvalidObjectException("Invalid age: " + age);
}
}
}
机制 触发时机 用途
readObject 当前对象反序列化时 自定义反序列化逻辑
readResolve 当前对象反序列化完成后 对象替换(单例保护)
validateObject 整个对象图反序列化完成后 跨对象校验

六、安全视角:为什么反序列化会出漏洞

6.1 漏洞本质

Java 反序列化漏洞的本质是:攻击者控制了反序列化的输入数据,而反序列化过程中会自动调用被反序列化对象的 readObjectreadExternalreadResolvevalidateObjectfinalize 等方法。 如果这些方法执行了危险操作,攻击者就可以通过构造恶意序列化数据来触发。

这条攻击链在业界被称为 Gadget Chain——一串相互衔接的方法调用,从反序列化入口点到最终的恶意操作。

6.2 经典 Gadget Chain 模式

1
2
3
反序列化入口 (readObject)
→ 中间跳板 (HashMap.hashCode / BadAttributeValueExpException.toString / ...)
→ 最终 Sink (Runtime.exec / TemplatesImpl.newTransformer / JNDI lookup / ...)

6.3 常见 Sink

Sink 效果 典型利用链
Runtime.getRuntime().exec() 命令执行 CommonsCollections 系列
TemplatesImpl.getOutputProperties() 加载自定义字节码实现 RCE CommonsCollections、CommonsBeanutils
JNDI lookup (如 InitialContext.lookup) JNDI 注入 → 远程类加载 各种链的最终 sink
ProcessBuilder.start() 命令执行 Spring 系列链
Method.invoke() 反射调用任意方法 中间跳板或终端

6.4 ysoserial 工具

ysoserial 是 Java 反序列化漏洞利用的里程碑工具,它收录了 CommonsCollections、CommonsBeanutils、Spring、Groovy 等几十条 Gadget Chain,每条链都自动化生成可用的序列化 payload。

使用示例:

1
2
# 生成 CommonsCollections1 的 payload
java -jar ysoserial.jar CommonsCollections1 "calc.exe" > payload.ser

6.5 与本文的关系

本文追踪的 writeObject/readObject/readResolve/validateObject 等方法,正是 Gadget Chain 中各个”跳板”的连接点。每个实现 Serializable 且定义了 readObject 方法的类,都是一个潜在的入口。

以经典的 CommonsCollections 链为例:

  • 入口: AnnotationInvocationHandler.readObject() → 触发 memberValues.entrySet()
  • 跳板: LazyMap.get()Transformer.transform()
  • 终点: InvokerTransformer.transform()Runtime.exec()

每个环节都依赖 Java 反序列化机制自动调用 readObject → 触发 hashCode/equals → 一路链到危险操作的特性。


七、防御手段

7.1 JEP 290:序列化过滤(Java 9+)

Java 9 引入了 JEP 290,提供 ObjectInputFilter 机制在反序列化之前进行过滤:

1
2
3
4
5
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.safe.*;!*"
);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
ois.setObjectInputFilter(filter);

过滤规则:

  • 白名单:com.example.safe.*
  • 黑名单:!org.apache.commons.collections.**
  • 限制:maxdepth=10;maxrefs=100;maxbytes=1000000;maxarray=100000

7.2 传统防御方法

方法 描述 效果
不使用 Java 序列化 改用 JSON / Protobuf 等非二进制协议 从根本上消除攻击面
升级 JDK Java 8u121+ 内置 RMI 反序列化限制 有限防护
JEP 290 Filter 类白名单/黑名单 推荐方案
运行时 Agent 如 SerialKiller、NotSoSerial 兼容旧版本 JDK
依赖管理 移除无用的第三方库 减少 Gadget 可用类

7.3 JEP 415:上下文过滤器(Java 17+)

Java 17 进一步引入 JEP 415,允许通过系统属性或配置文件全局配置反序列化过滤器,不需要修改代码:

1
2
# 在 conf/security/java.security 中配置
jdk.serialFilter=com.example.safe.*;!*

八、关键要点总结

概念 要点
writeObject 入口 writeObject0writeOrdinaryObjectwriteSerialData → 反射调用用户 writeObject
readObject 入口 readObject0readOrdinaryObjectreadSerialData → 反射调用用户 readObject
Externalizable 完全自定义序列化,需要 public 无参构造
父类非 Serializable 反序列化时调用父类无参构造,父类字段为 null
static 字段 不参与序列化
transient 字段 不参与序列化,反序列化后为默认值
serialVersionUID 版本校验,不一致则抛异常
writeReplace 序列化前替换对象
readResolve 反序列化后替换对象,用于保护单例
validateObject 整个对象图反序列化完成后校验

参考