前言
Java 序列化是将对象状态转换为字节流的过程,反序列化则是从字节流重建对象。这两个过程是 Java RMI、JMX、JNDI 等分布式通信的基石,也是 Java 安全史上最严重的一类漏洞的根源。
2015 年 FoxGlove Security 公开的 WebLogic 反序列化漏洞(CVE-2015-4852)标志着 Java 反序列化攻击进入公众视野。此后,ysoserial 工具集的出现将这一攻击模式系统化,Spring、Shiro、Fastjson、Jackson 等主流框架无一幸免。
理解 Java 序列化的底层实现——ObjectOutputStream 和 ObjectInputStream 的内部调用链——是理解反序列化漏洞利用(Gadget Chain)的前提。本文从 JDK 源码级别追踪完整的序列化与反序列化流程,覆盖 writeObject/readObject、Externalizable、transient/static 行为、serialVersionUID、writeReplace/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); } else { try { this.writeObject0(var1, false); } catch (IOException var3) { if (this.depth == 0) { 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 { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream temp = new ObjectOutputStream(baos); temp.writeObject(obj); byte[] data = baos.toByteArray();
byte[] signature = sign(data);
writeInt(data.length); write(data); write(signature); } }
SignedOutputStream sos = new SignedOutputStream(...); sos.writeObject(anyObject);
|
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; } } } }
|
这段代码展示了 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); 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); } 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); } 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); }
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { System.out.println("调用自定义 readObject 方法"); in.defaultReadObject();
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(); }
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: Object var13 = this.checkResolve(this.readOrdinaryObject(var1)); return var13; } } 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); } } } } } }
|
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 接口
Externalizable 是 Serializable 的子接口,提供两个抽象方法让用户完全控制序列化逻辑:
1 2 3 4 5 6
| public interface Externalizable extends Serializable { void writeExternal(ObjectOutput var1) throws IOException; 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;
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);
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); System.out.println("staticVar = " + staticVar); } 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; }
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: Object var13 = this.checkResolve(this.readOrdinaryObject(var1)); return var13;
|
checkResolve() 内部会反射调用 readResolve()。
另一个单例保护示例:
1 2 3 4
| public enum SingletonEnum { INSTANCE; }
|
对于非枚举单例,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 返回的对象类型可能与反序列化的对象不同,这在绕过某些类型检查时有用。
ObjectInputValidation 接口允许对象在完全反序列化后(整个对象图构建完毕后)执行验证逻辑:
1 2 3
| public interface ObjectInputValidation { void validateObject() throws InvalidObjectException; }
|
在 readObject 方法中可以看到触发时机:
1 2 3
| if (this.depth == 0) { this.vlist.doCallbacks(); }
|
使用方式:
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 反序列化漏洞的本质是:攻击者控制了反序列化的输入数据,而反序列化过程中会自动调用被反序列化对象的 readObject、readExternal、readResolve、validateObject、finalize 等方法。 如果这些方法执行了危险操作,攻击者就可以通过构造恶意序列化数据来触发。
这条攻击链在业界被称为 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
| 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 |
入口 writeObject0 → writeOrdinaryObject → writeSerialData → 反射调用用户 writeObject |
readObject |
入口 readObject0 → readOrdinaryObject → readSerialData → 反射调用用户 readObject |
Externalizable |
完全自定义序列化,需要 public 无参构造 |
| 父类非 Serializable |
反序列化时调用父类无参构造,父类字段为 null |
static 字段 |
不参与序列化 |
transient 字段 |
不参与序列化,反序列化后为默认值 |
serialVersionUID |
版本校验,不一致则抛异常 |
writeReplace |
序列化前替换对象 |
readResolve |
反序列化后替换对象,用于保护单例 |
validateObject |
整个对象图反序列化完成后校验 |
参考