php反序列化会执行任意代码,因其unserialize()还原对象时自动触发__wakeup()、__destruct()等魔术方法,若其中调用用户可控变量(如$this->callback),攻击者可构造恶意字符串控制执行流。

PHP反序列化为什么会执行任意代码
因为 unserialize() 在还原对象时,会自动触发 __wakeup()、__destruct() 等魔术方法,而这些方法里如果调用了用户可控的变量或函数(比如 $this->callback),攻击者就能通过构造恶意序列化字符串控制执行流。
常见错误现象:网站没明显功能点,但日志里出现 system()、exec() 调用痕迹;或者页面报错里意外泄露了 __wakeup() called 这类信息。
典型使用场景:
- 从 cookie、session、缓存(如 Redis)中读取并直接
unserialize()数据 - API 接口接收 base64 编码的序列化字符串,未校验来源就还原
- 旧版 Laravel / ThinkPHP 的 session 处理逻辑(尤其 PHP 7.0–7.2 时期)
怎么判断项目里有没有危险的 unserialize()
不能只搜函数名——很多项目会封装一层,比如叫 safe_unserialize() 或走配置开关。重点看三点:
立即学习“PHP免费学习笔记(深入)”;
- 所有接收外部输入(
$_GET、$_COOKIE、file_get_contents()读缓存)的地方,是否最终流向了unserialize() - 是否用了已知有 POP 链的组件(如旧版
monolog、phpunit、guzzle),哪怕没直接调unserialize(),也可能被间接触发 - 是否开启
unserialize_callback_func且回调函数接受不可信类名(例如直接拼接$class . 'Handler')
一个快速检查命令:grep -r "unserialize\|__wakeup\|__destruct" --include="*.php" .,再人工确认上下文是否涉及外部输入。
unserialize() 替代方案不是加个白名单就安全
很多人加个 allowed_classes 参数就以为万事大吉,但 PHP 7.4+ 才支持数组形式白名单,低版本只认布尔值;而且白名单挡不住原生类利用(如 stdClass、Exception 配合特定扩展)。
真正可用的缓解方式:
- 彻底不用
unserialize()—— 改用json_decode()+ 手动构造对象,或igbinary_unserialize()(需服务端统一启用且不暴露反序列化入口) - 必须保留时,强制设置
allowed_classes => false(PHP 7.4+),并确保没有自定义unserialize_callback_func - 对输入做双层校验:先验签名(如 HMAC-SHA256),再解密/解码,最后才考虑是否反序列化
示例风险写法:unserialize($_COOKIE['data']);安全写法:json_decode(sodium_crypto_secretbox_open($raw, $nonce, $key), true)。
为什么有些漏洞扫描器扫不出真实反序列化链
因为 POP 链依赖具体环境:装了什么扩展(soap、gd)、用了什么第三方库、甚至 PHP 版本小版本号(7.2.28 和 7.2.33 的 SoapClient 行为就有差异)都会影响链是否生效。
所以别只信扫描结果:
- 本地搭同版本环境,用
phpggc生成 payload 测试关键入口点 - 关注 error_log 或 php-fpm slowlog,看是否有
__destruct被调用但没抛异常的静默执行 - 检查
phpinfo()页面是否暴露了危险扩展(如phar、soap),它们是常见 POP 链载体
最麻烦的情况是:业务代码本身没问题,但某个 Composer 包的 autoload.php 里注册了带 __wakeup 的类,而你根本没意识到它会被触发。











