PHP 面试精讲:PHP 序列化与反序列化的安全风险及对象注入防御

在 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 链可能涉及多个类,例如:

  1. X__destruct 调用 $this->obj->close()
  2. Y__call 处理 close(),并调用 $this->file->write()
  3. Zwrite() 将内容写入文件,内容来自可控属性。

攻击者通过精心构造属性值,串联起这条链。

常见入口点

  • 用户输入直接传入 unserialize()
  • Cookie、Session、缓存、数据库字段中存储的序列化数据被反序列化。
  • 第三方库(如 Guzzle、Laravel、ThinkPHP)中的反序列化点。

四、真实漏洞案例

  1. PHP 反序列化漏洞(CVE-2016-7124):当序列化字符串中对象属性数量大于实际属性数量时,__wakeup() 会被绕过。影响 PHP 5.6.25 之前、7.0.10 之前版本。
  2. Laravel 反序列化 RCE:通过 PendingBroadcast 类构造 POP 链,最终调用 call_user_func 执行任意代码。
  3. 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 链。

六、面试常见问题

  1. __wakeup__destruct 在反序列化中的区别?

    • __wakeup 在反序列化时立即调用,__destruct 在对象销毁时调用。两者都可能被利用,但触发时机不同。
  2. 如何绕过 __wakeup

    • 在 PHP 5.6.25 之前和 7.0.10 之前,当序列化字符串中属性数量大于实际属性数量时,__wakeup 不会执行。例如 O:4:"User":3:{...} 改为 O:4:"User":4:{...}
  3. allowed_classes 参数有什么作用?

    • 限制反序列化时允许实例化的类,防止攻击者构造任意类的对象。
  4. POP 链的起点和终点通常是什么?

    • 起点是 __wakeup__destruct 等魔术方法,终点是 systemevalfile_put_contents 等危险函数。
  5. 如何安全地存储对象?

    • 使用 JSON 或数据库关系映射,避免直接序列化对象。如果必须,使用签名和加密。

七、总结

PHP 反序列化漏洞是面试中的重点,也是实际渗透测试中的常见突破口。其核心在于:反序列化会触发魔术方法,而魔术方法中的用户可控数据可能导致代码执行。防御的关键是不信任任何输入,优先使用 JSON,限制 allowed_classes,并对序列化数据签名。理解 POP 链的构造原理,有助于在代码审计中快速定位风险。

在面试中,如果能结合真实案例(如 Laravel、ThinkPHP)说明利用链,并给出具体的防御代码,将大大加分。记住:安全不是功能,而是底线。

未经允许不得转载:任鹏个人博客 » PHP 面试精讲:PHP 序列化与反序列化的安全风险及对象注入防御

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏