Dubbo漏洞 - 00 Java序列化机制

Java 序列化机制 — Dubbo 漏洞的根基

什么是序列化

序列化(Serialization):将 Java 对象转换为字节流,可以存储到文件或通过网络传输

反序列化(Deserialization):将字节流还原为 Java 对象

类比理解

序列化 = 把一个乐高模型拆成零件装进盒子,附上说明书

反序列化 = 按照说明书把零件重新组装成模型

为什么 Dubbo 需要序列化?

Dubbo 是 RPC 框架,Consumer 调用 Provider 的方法时,参数和返回值需要通过网络传输

传输前必须序列化为字节流,接收后反序列化还原

Java 原生序列化

核心接口:Serializable

1
2
3
4
5
6
7
8
9
10
11
// 一个类只要实现 Serializable 接口就可以被序列化
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;

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

Serializable 是一个标记接口(没有任何方法),仅仅告诉 JVM “这个类可以被序列化”

序列化过程

1
2
3
4
5
6
7
8
9
10
// 序列化:对象 → 字节流
User user = new User("Alice", 25);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));
oos.writeObject(user); // 将对象写入文件
oos.close();

// 反序列化:字节流 → 对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
User restored = (User) ois.readObject(); // 从文件还原对象
ois.close();

序列化数据格式

Java 序列化数据以固定的 magic number 开头:AC ED 00 05

可以用 SerializationDumper 工具查看二进制结构

1
2
3
4
5
6
7
8
$ java -jar SerializationDumper.jar -f user.ser
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className: User
serialVersionUID: 0x00 00 00 00 00 00 00 01
...

安全意义:看到 aced0005 开头的数据 = 这里有 Java 反序列化,可能有漏洞

危险的 readObject() 方法

自动调用机制

当调用 ObjectInputStream.readObject() 时,如果被反序列化的类定义了 private void readObject(ObjectInputStream in) 方法,JVM 会自动调用它

这是整个 Java 反序列化漏洞存在的根本原因

为什么危险?

1
2
3
4
5
6
7
8
9
10
public class EvilClass implements Serializable {
private String command;

// 这个方法在反序列化时会被 JVM 自动调用!
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject();
// 攻击者可以控制 command 的值
Runtime.getRuntime().exec(command); // 执行系统命令!
}
}

反序列化时,数据来自攻击者,代码在服务器执行

攻击者可以控制对象的字段值 → 影响 readObject() 中的逻辑 → 达成任意代码执行

完整 Demo:反序列化触发命令执行

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

public class DeserDemo {
// 恶意类
static class Malicious implements Serializable {
private String cmd;

public Malicious(String cmd) { this.cmd = cmd; }

private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject();
System.out.println("[!] readObject 被自动调用,执行命令: " + cmd);
Runtime.getRuntime().exec(cmd);
}
}

public static void main(String[] args) throws Exception {
// 1. 序列化恶意对象
Malicious evil = new Malicious("open -a Calculator"); // macOS
// Malicious evil = new Malicious("calc.exe"); // Windows
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectOutputStream(baos).writeObject(evil);
byte[] payload = baos.toByteArray();

System.out.println("[*] 序列化完成,payload 长度: " + payload.length);
System.out.println("[*] 开始反序列化...");

// 2. 反序列化 → 自动触发 readObject → 弹计算器
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload));
ois.readObject(); // 这一行就会触发命令执行!
}
}

现实中的攻击场景

攻击者不能直接控制类的代码

上面的 Demo 是为了说明原理,实际攻击中,攻击者不可能让服务器有一个 readObject() 直接执行命令的类

但是! 服务器 classpath 中已有的第三方库可能包含”有副作用”的 readObject() 方法

这就是 Gadget Chain(利用链)的概念 — 下一节详解

哪些地方会触发反序列化?

Java RMI

JMX

RPC 框架(Dubbo、Hessian) ← 我们的重点

HTTP 请求中的 Java 序列化数据

消息队列(ActiveMQ、RabbitMQ)

Session 持久化

其他序列化方式简介

Java 原生序列化只是众多方式之一,Dubbo 还支持:

方式 特点 安全风险
Java 原生 内置,功能完整,性能差 最高:完整 gadget chain 支持
Hessian2 Dubbo 默认,二进制紧凑 高:hashCode/toString 触发链
Kryo 高性能,需注册类 高:无内置安全过滤
FST 高性能 高:无内置安全过滤
Fastjson JSON 格式 中:autoType 漏洞
Protobuf 类型安全,跨语言 低:不支持任意类实例化

Hessian2 的反序列化机制和 Java 原生不同,将在后续笔记中详细分析

关键概念总结

概念 说明
序列化 对象 → 字节流
反序列化 字节流 → 对象
Serializable 标记接口,表示类可序列化
readObject() 反序列化时自动调用的魔法方法
aced0005 Java 序列化数据的 magic number
攻击本质 数据来自攻击者 + 代码在服务器自动执行

动手练习

编译运行上面的 DeserDemo,观察计算器弹出

xxd user.ser | head 查看序列化文件的二进制开头,确认 aced0005

思考:如果 readObject() 不直接执行命令,而是调用了某个看似无害的方法(如 hashCode()),攻击者还能利用吗?→ 这就是 Gadget Chain 的起点


上一章 目录 下一章
Dubbo漏洞 01-Gadget-Chain原理