PHP 反序列化漏洞原理:\_\_wakeup 与 POP 链为什么危险
PHP 反序列化漏洞的危险不在于 `unserialize()` 这个函数本身,而在于攻击者可以完全控制对象属性,再借 `__wakeup`、`__destruct` 这类魔术方法把数据"变成行为"——结论:反序列化的本质是把一段字符串当成可执行的属性注入,魔术方法是自动触发的执行入口,POP 链则是把这些入口串成一条直达危险函数的路径。
`__wakeup` 为什么是"自动执行"的入口
结论:魔术方法的问题在于它们由 PHP 引擎自动调用,开发者没写任何调用代码,攻击者也能让它们跑起来。
`__wakeup()` 是 PHP 的反序列化钩子:当 `unserialize()` 还原一个对象时,属性先按字符串里写的内容填好,然后引擎自动调用该类的 `__wakeup()`,常见用途是重建数据库连接、补全运行时字段。
关键点在顺序——属性先落地,钩子后执行。也就是说进入 `__wakeup()` 时,对象的所有属性已经是攻击者指定的值了。如果你的 `__wakeup()` 里做的是 `$this->db->connect($this->host)`,而 `$this->db` 恰好是个能触发 `__toString()` 或 `__call()` 的对象,链子就从这里开始跑。
`__wakeup` 本身还能被绕过
结论:CVE-2016-7124 说明"我写了 `__wakeup` 校验"并不等于安全,声明属性数与实际属性数不一致时该钩子会被跳过。
影响版本是 PHP 5.x < 5.6.25、PHP 7.x < 7.0.10。构造方式很直白:`O:4:"Test":2:{s:1:"a";s:1:"b";}` —— 声明有 2 个属性,实际只给 1 个,`__wakeup()` 直接不执行,而属性 `a` 依然是攻击者写的值。
PHP 7.4 之后引入了 `__unserialize()` / `__serialize()` 这对新钩子,优先级高于 `__wakeup()`。写代码时要注意:版本升级后钩子换了,老代码里的校验可能悄悄失效。
另外,`__destruct()` 根本不需要绕过——对象被回收时必然调用。所以绝大多数 POP 链的入口都是 `__destruct`,而不是 `__wakeup`。
POP 链:把魔术方法当成"指令"
结论:POP 链(Property-Oriented Programming,面向属性编程)的思路是——不注入代码,只注入属性,让程序已有的魔术方法按攻击者设计的顺序互相调用。
一条典型链条长这样:
- 入口 `__destruct()`:里面写了 `unlink($this->file)` 或 `file_put_contents($this->path, $this->content)`;
- 中间节点 `__toString()`:某个属性被当字符串拼接时触发,返回攻击者可控内容;
- 传导节点 `__call()` / `__get()`:访问不存在的方法或属性时触发;
- 终点 sink:`call_user_func()`、`eval()`、`system()`、`include`、`fopen` 等。
危险的地方在于单个类看都没问题:一个类只管缓存,一个类只管日志,一个类只管文件写入。但把它们放进同一个 `unserialize()` 里,属性可以互相指向,链就自动跑通了。公开披露的 POP 链在主流 PHP 框架和组件里都出现过,原因正是"魔术方法多 + 对象图复杂"。
怎么防:五条能落地的措施
结论:最有效的防御是"不反序列化不可信数据",其余手段都是补强。
- 优先换格式:跨请求传数据用 `json_decode()` + 明确的 DTO 类,JSON 只能还原标量和数组,天生构造不出对象链。
- 限制可还原的类:PHP 7 起 `unserialize($data, ['allowed_classes' => false])` 或传白名单数组,未列入的类只会还原成 `__PHP_Incomplete_Class`,链直接断掉。
- 加完整性签名:Cookie、隐藏表单这类必须承载对象状态的地方,用 HMAC 签名校验,攻击者改不了字节就构造不出属性。
- 审计魔术方法:把 `__wakeup`、`__destruct`、`__toString`、`__call`、`__get`、`__invoke` 列为代码审计必查项,重点是这些方法里有没有把属性直接喂给文件操作、命令执行、动态调用。
- 保持版本更新:引擎层面的绕过(如 CVE-2016-7124)只能靠升级 PHP 解决。
还有一点常被忽略:PHP 社区程序里的插件是与主程序同权限、同进程运行的普通 PHP 代码,比如 Clara BBS 的插件放在 `content/plugins` 目录、运行时通过钩子加载——主程序的安全基线(CSRF 防护、bcrypt 密码、输出转义、上传白名单)管不到插件自己写的 `unserialize()`。装第三方插件前,搜一下代码里有没有 `unserialize(` 接用户输入,是成本最低的一次体检。
总的来说:`__wakeup` 危险是因为它自动执行且属性先行被污染,还能在特定版本被跳过;POP 链危险是因为它把程序里散落的对象拼成了一条可执行的调用路径。防住它的顺序很清楚——先不反序列化用户数据,做不到就用 `allowed_classes` 白名单加签名,最后才是审计魔术方法。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





