Java 反序列化漏洞
1. 基本概念
什么是序列化(Serialization)?
序列化就是把程序中内存里的对象状态(字段值、类型信息等)转换成一种可存储或传输的格式(比如二进制流、字符串),以便保存到文件、通过网络传输、或跨进程/跨机器传递。
举个直观的比喻:对象在内存中是“活的”结构,序列化就是把它“拍照+压缩”,变成可以发给别人的文件或数据包。
什么是反序列化(Deserialization)?
反序列化就是把序列化后的二进制/字符串数据解析回对象的过程:读取数据,根据类型信息重建对象并加载到内存中。
2. Java 中序列化/反序列化的常见实现
-
一个类要被 Java 原生序列化,需要实现
java.io.Serializable接口(这是个标识接口,没有方法)。 -
序列化写出:使用
ObjectOutputStream的writeObject(Object)方法,把对象写到FileOutputStream、Socket等输出流。 -
反序列化读取:使用
ObjectInputStream的readObject()方法,从输入流读取并重建对象。
常见代码模板:
// 序列化(写)
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 运行时会自动执行类中某些方法(例如 readObject、readResolve、finalize 等,或其他可触发的回调),如果这些方法中包含不安全的代码(例如执行系统命令 Runtime.getRuntime().exec(...)),那么当反序列化一个被攻击者构造的对象时,这些恶意代码会被执行,从而导致任意命令执行(RCE)。
重点:只要反序列化的是不受信任或未验证的外部数据,就存在风险。
此外:实际攻击常常通过“gadget chain”(利用第三方库的可控链)来触发链中某处的危险代码,而不是目标类本身必须写 Runtime.exec。例如 commons-collections 等库曾被广泛利用。
4. 典型示例说明
-
有一个
Person类:实现了Serializable,并重写了readObject(ObjectInputStream ois)方法。 -
在
readObject内部作者写了类似Runtime.getRuntime().exec("calc")或exec("ipconfig")这样的代码(恶意或演示用)。 -
客户端把这个
Person对象序列化并发送给服务器;服务器在接收数据时调用readObject()进行反序列化。 -
反序列化会执行
Person.readObject,从而执行Runtime.exec,打开计算器或输出 IP 等——这就是 RCE 漏洞的直观演示。
关键点复盘:反序列化不是“单纯把数据复原”,而是在重建对象时会触发类的代码逻辑。攻击者可以构造序列化数据,诱导服务器反序列化并执行恶意代码。
5. 漏洞利用流程(高层步骤)
-
攻击者构造一个或多级的序列化对象(如果目标类本身有危险回调,可直接序列化该类;常见更复杂的是构造 gadget chain,利用第三方库中若干类串联触发危险方法)。
-
将序列化后的二进制数据通过协议(HTTP、Socket 等)发送到目标服务器的反序列化接口。
-
目标服务器执行
ObjectInputStream.readObject()(或等价逻辑),触发回调函数并执行攻击者的代码。 -
攻击者借此获得命令执行、读取文件、建立反连等能力。
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(...)、ProcessBuilder、Class.forName(动态加载)、反射Method.invoke等可执行外部或动态调用的代码。 -
第三方库中的“gadget”类:
commons-collections、commons-beanutils、spring、hibernate等历史上均出现过可被串联利用的 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.exec、ProcessBuilder 等;CI 中添加 SAST 规则检测。
7) 其他保护(把握好可用性)
-
如果不能避免反序列化,考虑在一个受限制的沙箱进程中做反序列化并对结果进行严格验证。
-
使用最新 JDK 的
Serialization Filtering功能并配置严格策略。
10. 真实世界中常见的利用链
攻击者通常不会指望目标项目恰好有一个 Person 类的 readObject 执行 Runtime.exec。更常见的是:
-
目标依赖包含某些库(例如早年的
commons-collections),攻击者构造一串对象(gadget chain),通过反射、恶意属性或回调来最终触发执行命令。 -
利用链长度与复杂性不一,但最终目标通常是触发
Runtime.exec、动态加载类、或反射调用敏感方法。
因此在审计时除了自定义类,还要审查依赖库与其版本。
更多推荐



所有评论(0)