在 PHP 面试中,序列化与反序列化是一个高频考点,不仅因为它是日常开发中常用的数据存储与传输手段,更因为它背后隐藏着严重的安全风险——对象注入漏洞。许多真实世界的 RCE(远程代码执行)漏洞都源于此。本文将从面试角度出发,系统讲解 PHP 序列化机制、反序列化漏洞原理、常见利用链,以及防御策略。
一、什么是 PHP 序列化与反序列化
PHP 序列化(Serialization)是将变量转换为可存储或传输的字符串的过程,反序列化(Unserialization)则是将字符串还原为原始变量的过程。核心函数为:
serialize($value):返回表示值的字节流字符串。unserialize($string):从字节流恢复 PHP 值。
示例:
class User {
public $name = 'Alice';
private $age = 25;
protected $email = 'alice@example.com';
}
$user = new User();
echo serialize($user);
// 输出:O:4:"User":3:{s:4:"name";s:5:"Alice";s:7:"\0User\0age";i:25;s:8:"\0*\0email";s:17:"alice@example.com";}
序列化字符串格式说明:
O:4:"User":3:{...}表示对象,类名长度 4,属性数量 3。s:4:"name"表示字符串属性名,长度为 4。i:25表示整数。- 私有属性名包含
\0类名\0,保护属性包含\0*\0。
反序列化时,PHP 会根据字符串中的类名尝试实例化对应类,并恢复属性值。如果类不存在,则生成 __PHP_Incomplete_Class 对象。
二、反序列化漏洞的本质:魔术方法触发
反序列化本身不会执行代码,但当反序列化后的对象被销毁、被当作字符串使用、或参与某些操作时,PHP 会自动调用一些魔术方法。攻击者通过控制序列化字符串中的类名和属性值,可以触发这些魔术方法,进而执行恶意代码或敏感操作。
常见的危险魔术方法:
| 魔术方法 | 触发时机 |
|---|---|
__wakeup() |
反序列化时立即调用 |
__destruct() |
对象被销毁时调用 |
__toString() |
对象被当作字符串时调用 |
__call() |
调用不存在的方法时调用 |
__get() |
读取不存在的属性时调用 |
__set() |
给不存在的属性赋值时调用 |
__invoke() |
对象被当作函数调用时调用 |
如果这些方法中使用了用户可控的数据(如文件操作、命令执行、数据库查询),就可能形成漏洞。
三、经典利用链(POP Chain)
POP(Property-Oriented Programming)链是指从反序列化入口开始,通过一系列类的魔术方法调用,最终达到危险操作(如 system()、eval()、文件写入)的链条。攻击者需要找到一条从 __wakeup 或 __destruct 到危险函数的调用路径。
示例:一个简单的 POP 链
class A {
public $cmd;
public function __destruct() {
// 危险操作:执行命令
system($this->cmd);
}
}
// 攻击者构造的序列化字符串
$payload = 'O:1:"A":1:{s:3:"cmd";s:2:"id";}';
unserialize($payload); // 执行 id 命令
实际应用中,POP 链可能涉及多个类,例如:
- 类
X的__destruct调用$this->obj->close()。 - 类
Y的__call处理close(),并调用$this->file->write()。 - 类
Z的write()将内容写入文件,内容来自可控属性。
攻击者通过精心构造属性值,串联起这条链。
常见入口点
- 用户输入直接传入
unserialize()。 - Cookie、Session、缓存、数据库字段中存储的序列化数据被反序列化。
- 第三方库(如 Guzzle、Laravel、ThinkPHP)中的反序列化点。
四、真实漏洞案例
- PHP 反序列化漏洞(CVE-2016-7124):当序列化字符串中对象属性数量大于实际属性数量时,
__wakeup()会被绕过。影响 PHP 5.6.25 之前、7.0.10 之前版本。 - Laravel 反序列化 RCE:通过
PendingBroadcast类构造 POP 链,最终调用call_user_func执行任意代码。 - ThinkPHP 5.1.x 反序列化:利用
Windows类与Pivot类组合,写入 Webshell。
这些案例说明,反序列化漏洞不仅限于用户直接输入,框架内部的不安全反序列化同样危险。
五、防御策略
1. 避免反序列化不可信数据
最根本的原则:永远不要反序列化用户可控的数据。如果必须传递复杂数据,使用 JSON、XML 等更安全的格式,并严格校验。
2. 使用 json_decode 替代 unserialize
JSON 不支持对象,不会触发魔术方法,天然免疫对象注入。对于纯数据结构,优先使用 JSON。
3. 签名与加密
如果必须使用序列化,对序列化字符串进行 HMAC 签名,确保数据未被篡改。例如:
$data = serialize($object);
$signature = hash_hmac('sha256', $data, $secretKey);
// 存储 $data 和 $signature
// 读取时验证签名
if (!hash_equals($signature, hash_hmac('sha256', $data, $secretKey))) {
die('Invalid data');
}
$object = unserialize($data);
4. 限制 unserialize 的类
PHP 7.0+ 支持 unserialize($data, ['allowed_classes' => ['SafeClass']]),只允许反序列化白名单中的类。如果不需要对象,设置为 false。
$data = unserialize($input, ['allowed_classes' => false]);
5. 禁用危险魔术方法中的用户输入
审查代码中所有 __wakeup、__destruct 等方法,确保不直接使用用户可控数据执行敏感操作。对属性进行类型和范围校验。
6. 及时更新框架与 PHP 版本
许多反序列化漏洞在后续版本中被修复。保持 PHP 和框架最新版本,减少已知漏洞风险。
7. 使用安全扫描工具
静态分析工具(如 RIPS、PHPStan)和动态扫描器可以帮助发现潜在的反序列化入口和 POP 链。
六、面试常见问题
-
__wakeup和__destruct在反序列化中的区别?__wakeup在反序列化时立即调用,__destruct在对象销毁时调用。两者都可能被利用,但触发时机不同。
-
如何绕过
__wakeup?- 在 PHP 5.6.25 之前和 7.0.10 之前,当序列化字符串中属性数量大于实际属性数量时,
__wakeup不会执行。例如O:4:"User":3:{...}改为O:4:"User":4:{...}。
- 在 PHP 5.6.25 之前和 7.0.10 之前,当序列化字符串中属性数量大于实际属性数量时,
-
allowed_classes参数有什么作用?- 限制反序列化时允许实例化的类,防止攻击者构造任意类的对象。
-
POP 链的起点和终点通常是什么?
- 起点是
__wakeup或__destruct等魔术方法,终点是system、eval、file_put_contents等危险函数。
- 起点是
-
如何安全地存储对象?
- 使用 JSON 或数据库关系映射,避免直接序列化对象。如果必须,使用签名和加密。
七、总结
PHP 反序列化漏洞是面试中的重点,也是实际渗透测试中的常见突破口。其核心在于:反序列化会触发魔术方法,而魔术方法中的用户可控数据可能导致代码执行。防御的关键是不信任任何输入,优先使用 JSON,限制 allowed_classes,并对序列化数据签名。理解 POP 链的构造原理,有助于在代码审计中快速定位风险。
在面试中,如果能结合真实案例(如 Laravel、ThinkPHP)说明利用链,并给出具体的防御代码,将大大加分。记住:安全不是功能,而是底线。
未经允许不得转载:任鹏个人博客 » PHP 面试精讲:PHP 序列化与反序列化的安全风险及对象注入防御

