1. 基本概念

什么是序列化(Serialization)?

序列化就是把程序中内存里的对象状态(字段值、类型信息等)转换成一种可存储或传输的格式(比如二进制流、字符串),以便保存到文件、通过网络传输、或跨进程/跨机器传递。

举个直观的比喻:对象在内存中是“活的”结构,序列化就是把它“拍照+压缩”,变成可以发给别人的文件或数据包。

什么是反序列化(Deserialization)?

反序列化就是把序列化后的二进制/字符串数据解析回对象的过程:读取数据,根据类型信息重建对象并加载到内存中。


2. Java 中序列化/反序列化的常见实现

  • 一个类要被 Java 原生序列化,需要实现 java.io.Serializable 接口(这是个标识接口,没有方法)。

  • 序列化写出:使用 ObjectOutputStreamwriteObject(Object) 方法,把对象写到 FileOutputStreamSocket 等输出流。

  • 反序列化读取:使用 ObjectInputStreamreadObject() 方法,从输入流读取并重建对象。

常见代码模板

// 序列化(写)
FileOutputStream fos = new FileOutputStream("/path/a.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(myObject);
oos.close();

// 反序列化(读)
FileInputStream fis = new FileInputStream("/path/a.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();

3. 为何会产生安全问题

核心原因:在反序列化过程中,Java 运行时会自动执行类中某些方法(例如 readObjectreadResolvefinalize 等,或其他可触发的回调),如果这些方法中包含不安全的代码(例如执行系统命令 Runtime.getRuntime().exec(...)),那么当反序列化一个被攻击者构造的对象时,这些恶意代码会被执行,从而导致任意命令执行(RCE)。

重点:只要反序列化的是不受信任或未验证的外部数据,就存在风险

此外:实际攻击常常通过“gadget chain”(利用第三方库的可控链)来触发链中某处的危险代码,而不是目标类本身必须写 Runtime.exec。例如 commons-collections 等库曾被广泛利用。


4. 典型示例说明

  1. 有一个 Person 类:实现了 Serializable,并重写了 readObject(ObjectInputStream ois) 方法。

  2. readObject 内部作者写了类似 Runtime.getRuntime().exec("calc")exec("ipconfig") 这样的代码(恶意或演示用)。

  3. 客户端把这个 Person 对象序列化并发送给服务器;服务器在接收数据时调用 readObject() 进行反序列化。

  4. 反序列化会执行 Person.readObject,从而执行 Runtime.exec,打开计算器或输出 IP 等——这就是 RCE 漏洞的直观演示。

关键点复盘:反序列化不是“单纯把数据复原”,而是在重建对象时会触发类的代码逻辑。攻击者可以构造序列化数据,诱导服务器反序列化并执行恶意代码。


5. 漏洞利用流程(高层步骤)

  1. 攻击者构造一个或多级的序列化对象(如果目标类本身有危险回调,可直接序列化该类;常见更复杂的是构造 gadget chain,利用第三方库中若干类串联触发危险方法)。

  2. 将序列化后的二进制数据通过协议(HTTP、Socket 等)发送到目标服务器的反序列化接口。

  3. 目标服务器执行 ObjectInputStream.readObject()(或等价逻辑),触发回调函数并执行攻击者的代码。

  4. 攻击者借此获得命令执行、读取文件、建立反连等能力。


6. 实验/复现

准备:一台做为服务器(监听 socket / 接受序列化数据并 readObject),一台做客户端(构造恶意对象并序列化后发送)。

服务器端伪代码

ServerSocket ss = new ServerSocket(9999);
Socket s = ss.accept();
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
Object obj = ois.readObject(); // 反序列化并触发 readObject
System.out.println(obj);

客户端伪代码(恶意)

Person p = new Person();
// Person 类的 readObject 会在反序列化时执行恶意命令
Socket s = new Socket(serverIp, 9999);
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
oos.writeObject(p);
oos.close();

观察:服务器端在调用 readObject() 时触发 Person.readObject 内的 Runtime.exec(...),例如弹出计算器或执行 ipconfig,说明漏洞存在。


7. 常见触发点与危险 API

  • 类里是否实现了 readObject(ObjectInputStream)readResolve()writeReplace()finalize() 等方法。

  • 是否存在诸如 Runtime.getRuntime().exec(...)ProcessBuilderClass.forName(动态加载)、反射 Method.invoke 等可执行外部或动态调用的代码。

  • 第三方库中的“gadget”类:commons-collectionscommons-beanutilsspringhibernate 等历史上均出现过可被串联利用的 gadget。审计时要重点查看依赖并评估是否存在可利用链。


8. 检测方法

动态检测(渗透)

  • 构造一个最小 payload(自写的 Person,重写 readObject 内执行一个明显可见动作),送到目标反序列化接口,看是否触发。注意合规与授权。

静态代码审计

  • 搜索 implements Serializable 的类,检查是否有 readObject / readResolve 等方法,并查看内部是否调用了危险 API。

  • 在项目依赖中查找历史上有 exploit 的库版本(比如早期被利用的 commons-collections 版本)。

监测与日志

  • 监控出现异常系统命令执行的日志、异常外联、或异常进程创建。


9. 防护建议

1) 最简单但最重要的原则

不要反序列化不信任的数据。 任何来自网络、用户输入或者外部系统的数据都视为不可信,不能直接 readObject

2) 使用白名单过滤(推荐)

Java 提供了 ObjectInputFilter(JDK 9+ 引入,JDK8 后续补丁也支持)用于限制反序列化的类、大小等,优先使用白名单(allowlist)方式来只允许可信类被反序列化。

示例(伪代码):

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.myapp.*;java.base/*;!*\");
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter);

3) 使用安全的数据交换格式

JSON、Protobuf、XML(注意 XML 也要防 XXE)等文本或结构化格式,配合安全的解析库,通常比 Java 原生序列化更安全且易审计。

4) 升级和修补依赖

定期升级第三方库,避免使用已知存在 gadget 的老版本依赖。

5) 最小权限、运行时限制

运行服务的进程应尽量限制权限(文件/网络/执行),避免进程本身有权限执行敏感命令或访问敏感资源。

6) 安全审计与检测规则

在代码审计过程中建立规则:禁止在 readObject 等回调中调用 Runtime.execProcessBuilder 等;CI 中添加 SAST 规则检测。

7) 其他保护(把握好可用性)

  • 如果不能避免反序列化,考虑在一个受限制的沙箱进程中做反序列化并对结果进行严格验证。

  • 使用最新 JDK 的 Serialization Filtering 功能并配置严格策略。


10. 真实世界中常见的利用链

攻击者通常不会指望目标项目恰好有一个 Person 类的 readObject 执行 Runtime.exec。更常见的是:

  1. 目标依赖包含某些库(例如早年的 commons-collections),攻击者构造一串对象(gadget chain),通过反射、恶意属性或回调来最终触发执行命令。

  2. 利用链长度与复杂性不一,但最终目标通常是触发 Runtime.exec、动态加载类、或反射调用敏感方法。

因此在审计时除了自定义类,还要审查依赖库与其版本。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐