引言

在之前的RMI篇中,我们观察到RMI通信的核心是对象的序列化与反序列化。反序列化漏洞在安全领域声名显赫,几乎每种语言都曾因此受累。那么,一个核心问题是:为什么反序列化操作如此危险?

简单来说,当程序需要将网络或磁盘上的数据“还原”成一个内存中的对象时,如果这个“还原”过程本身可以被操纵,去执行攻击者预期的逻辑,漏洞就产生了。

漏洞根源

为了理解漏洞的根源,我们首先要明白不同序列化方案的设计思路:

  1. **通用数据格式 (如 JSON/XML)**:

    • 目标:跨语言、跨平台通信。
    • 局限:通常只支持基本数据类型(字符串、数字、布尔等)。要传输一个“对象”,需要额外约定或使用扩展库(如Jackson/Fastjson)。
  2. **语言原生序列化 (如 Java Serializable)**:

    • 目标:完美地在网络间传输一个编程语言中的“对象”。
    • 特点:能将对象的完整状态(属性、类信息等)转化为字节流。接收方可以完整地“还原”出一个对象。
    • 风险:这个“还原”过程非常强大,而强大往往伴随着危险。

关键认知:“反序列化漏洞”是一个泛指,不同序列化库(Jackson, Fastjson)或不同语言(Java, PHP, Python)的漏洞成因和利用方式可能截然不同。本文我们聚焦于 Java 原生的 readObject 机制

Java与PHP的设计差异

为了理解Java反序列化的独特风险,一个极佳的方式是与PHP进行对比。许多人认为Java的 readObject 等同于PHP的 __wakeup,但这是一个常见的误解。

特性 PHP __wakeup Java readObject
核心职责 对象初始化 对象重建
执行时机 在PHP引擎自动完成反序列化,恢复所有属性之后执行。 反序列化过程的核心逻辑本身。它定义了如何从流中读取数据来还原对象。
开发者控制力 弱。你无法干预数据如何被解析成属性。 极强。你可以完全自定义从流中读取什么数据以及如何设置对象状态。
类比 新房交付后的“精装修”。房子结构(属性)已建好,你只是做些后期布置。 从零开始盖房子的“施工图”。图纸规定了如何打地基、砌墙、布线(即如何读取数据并构建对象)。

这个设计差异是导致Java反序列化漏洞多的根本原因!

以上是P牛的意思,但是我仍然认为不准确

ObjectInputStream.readObject()(反序列化入口)

能 完整控制 序列化协议的解析;决定如何读取流、如何解析类结构、对象如何创建、字段如何恢复;是整个序列化协议的 核心实现者

但: 用户不能自定义它(除非写子类 ObjectInputStream——但这种方式实际上非常有限,不能破坏协议结构)

用户类的 readObject()只能在 defaultReadObject() 恢复字段之后,进行“扩展读取”和“额外逻辑”。

不能更改底层序列化协议的解析方式;不能改变类的字段如何映射、对象如何创建;不能跳过 defaultReadObject() 并用自定义协议替代(完全不行)

PHP与Java的反序列化流程

1. PHP反序列化:以 __wakeup 为例

PHP的反序列化流程是“黑盒”的:

1
序列化数据 -> (PHP引擎自动解析) -> 对象属性已赋值 -> 调用 `__wakeup`(如果存在)

__wakeup 通常用于处理引擎无法自动完成的工作,最经典的例子是重新连接资源(如数据库连接)。

1
2
3
4
5
6
7
8
9
10
11
<?php
class Connection {
private $dsn, $username, $password;
private $link; // 数据库连接资源,无法被直接序列化

public function __wakeup() {
// 在反序列化后,属性已还原,但$link为null
// 所以需要调用connect方法重新建立连接
$this->connect();
}
}

结论:PHP反序列化漏洞很少由 __wakeup 直接引发,更多的是因为反序列化后我们能控制对象的属性,进而导致在 __destruct 析构函数或其他方法中被利用。

2. Java反序列化:灵活的 readObject

Java则将反序列化的巨大权力交给了开发者。一个类可以实现私有的 readObject 方法,完全掌控如何从 ObjectInputStream 中重建自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person implements Serializable {
public String name;
private String password;

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 1. 首先,执行默认的反序列化,恢复 name 和 password 字段
in.defaultReadObject();

// 2. 然后,开发者可以执行任何自定义逻辑!
// 例如:读取一个额外的消息
String message = (String) in.readObject();
System.out.println(message);

// 再例如:对密码进行“额外”的验证或日志记录(这里可能就是危险逻辑!)
if ("admin".equals(name)) {
System.out.println("Admin password is: " + password);
}
}
}

在这个例子中,readObject 不仅仅是在“还原”对象,它还在执行业务逻辑(打印消息、检查密码)。如果攻击者能够构造一个恶意的序列化流,让 name 为 “admin”,他就能在反序列化过程中直接窃取到密码

这种“在反序列化过程中执行业务逻辑”的能力,是Java反序列化漏洞的温床。 大量的Java库(如Apache Commons Collections, XStream等)在其类的 readObject 方法中实现了复杂且可能被利用的逻辑,形成了所谓的“利用链”(Gadget Chain)。

Python的反序列化

作为对比,Python的 pickle 反序列化机制更为“狂野”。它本质上是一个小型的字节码解释器,反序列化过程直接执行 pickle 字节码。这意味着攻击者几乎可以直接编码任意Python命令在目标机器上执行,无需依赖现有的类和方法(Gadget)。从危害性上讲,Python反序列化通常是最大的。

总结

  • PHP:反序列化是“自动化的属性还原 + 可选的初始化”。漏洞多在生命周期函数(如__destruct)中。
  • Python:反序列化是“一个虚拟机的执行”。漏洞是直接的代码执行。
  • Java:反序列化是“一个由 readObject 方法定义的、可能包含任意逻辑的对象重建过程”。这正是Java反序列化漏洞如此普遍和灵活的根源。