PHP反序列化漏洞的核心攻击手法就是利用"POP链"(Property Oriented Programming Chain),即攻击者通过精心构造的序列化数据,串联起多个类中的魔术方法(__wakeup、__destruct、__toString等),最终触发危险函数执行。要拦截POP链攻击,最直接有效的方法是在反序列化入口处做白名单过滤、禁用危险函数、重写unserialize逻辑,以及在WAF层面做正则匹配拦截。下面我把每一种方法的原理、实现方式和注意事项全部讲透。
一、先搞清楚POP链到底是怎么回事
PHP的unserialize()函数会把字符串还原成对象。如果攻击者能控制传入的序列化字符串,就可以实例化任意类。单独实例化一个类通常没什么危害,但如果这个类的魔术方法里调用了其他危险操作,再串联下一个类的魔术方法,一环扣一环,最终就能执行system()、eval()、file_put_contents()等高危函数。这条串联路径就是POP链。
举个最经典的例子:ThinkPHP5的反序列化漏洞。攻击者构造一个包含Request类的序列化字符串,触发__toString方法,进而调用到Response类的__destruct,最终执行shell命令。整个链条不需要攻击者直接写恶意代码,只需要利用框架自带的类就够了。
二、方法一:白名单机制拦截未知类
PHP 7.0之后引入了allowed_classes参数,可以在反序列化时限定只允许哪些类被实例化。这是最基础也是最推荐的防护手段。
// 只允许反序列化指定的安全类 $data = unserialize($input, ['allowed_classes' => ['SafeClassA', 'SafeClassB']]); // 完全禁止反序列化对象,只允许数组 $data = unserialize($input, ['allowed_classes' => false]);
如果你的业务场景根本不需要反序列化对象,直接设置allowed_classes为false,只解析数组,能从根源上杜绝POP链攻击。但要注意,很多老项目依赖反序列化对象传递数据,这种情况下需要逐一排查哪些类是业务必需的,加入白名单。
三、方法二:重写unserialize函数做深度过滤
allowed_classes只能控制类名,但无法过滤类内部属性的值。更硬核的做法是完全不用原生unserialize,自己写一个安全的反序列化函数,对输入数据做逐层解析和校验。
function safe_unserialize($str) {
// 1. 先做基础格式校验
if (!preg_match('/^[aO]:\d+:"/', $str)) {
return false;
}
// 2. 提取类名
preg_match('/^([aO]):(\d+):"([^"]*)"/', $str, $matches);
$type = $matches[1];
$className = $matches[3];
// 3. 白名单校验类名
$allowed = ['User', 'Config', 'Session'];
if (!in_array($className, $allowed)) {
return false;
}
// 4. 递归解析属性,过滤危险值
$result = parse_properties($str);
return $result;
}这种方式的好处是你可以在解析每个属性时检查值的类型和内容,比如发现某个属性值是一个包含"system"或"eval"的字符串,直接拒绝。虽然实现成本高一些,但安全性是最强的。
四、方法三:禁用和限制危险魔术方法
POP链的触发依赖魔术方法。如果能在运行时禁用或监控这些方法的调用,就能打断攻击链。具体做法有两种:
第一种是在php.ini中禁用危险函数,从源头减少可利用的函数:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,putenv
第二种是利用PHP的ticks机制或者自定义错误处理器,监控__wakeup、__destruct等方法的调用频率和上下文。虽然不能完全阻止,但可以作为告警手段,一旦发现短时间内大量触发魔术方法就报警。
五、方法四:WAF层面的正则拦截
在Web应用防火墙层面拦截POP链攻击是非常实用的手段。攻击者构造的序列化字符串有明显的特征模式,可以用正则表达式匹配并拦截。
// 常见POP链特征正则
$patterns = [
'/O:\d+:"\w+":\d+:{/', // 对象序列化特征
'/s:\d+:"__wakeup"/', // wakeup方法
'/s:\d+:"__destruct"/', // destruct方法
'/s:\d+:"__toString"/', // toString方法
'/s:\d+:"\x00\x00/', // 私有属性前缀
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $input)) {
// 拦截并记录日志
log_attack($input);
http_response_code(403);
exit;
}
}需要注意的是,正则拦截容易被绕过。攻击者可以用编码、分割、嵌套等方式混淆序列化字符串。所以WAF拦截要作为辅助手段,不能作为唯一防线。建议结合多层正则和语义分析,提高命中率。
六、方法五:运行时应用自我保护(RASP)
RASP(Runtime Application Self-Protection)是一种嵌入在应用内部的安全机制。它可以在代码执行层面实时监控,当发现unserialize调用且传入的数据包含可疑类名或属性时,直接阻断。
实现思路是通过PHP扩展或者在关键函数调用处做Hook。比如用uopz扩展或者借助PHP的override_function机制:
// 使用override_function重写unserialize
override_function('unserialize', '$data', '
if (is_string($data) && strpos($data, "O:") !== false) {
// 提取类名做校验
preg_match("/O:\\d+:\"([^\"]+)\"/", $data, $m);
if (!in_array($m[1], $whitelist)) {
trigger_error("Blocked unserialize of class: " . $m[1], E_USER_WARNING);
return false;
}
}
return unserialize($data);
');这种方式的优势是在应用内部就完成了防护,不依赖外部组件,而且可以根据业务动态调整白名单。缺点是需要安装扩展,对现有代码有侵入性。
七、方法六:序列化数据签名验证
如果你的应用必须使用序列化传输数据,那就给序列化数据加上签名。发送方在序列化后用密钥生成HMAC,接收方验证签名通过后才反序列化。这样即使攻击者构造了恶意序列化字符串,没有密钥也无法通过验证。
// 发送方
$data = serialize($object);
$signature = hash_hmac('sha256', $data, $secret_key);
$payload = base64_encode($signature . '|' . $data);
// 接收方
list($sig, $data) = explode('|', base64_decode($payload), 2);
$expected = hash_hmac('sha256', $data, $secret_key);
if (!hash_equals($expected, $sig)) {
die('Invalid signature');
}
$object = unserialize($data);这种方法特别适合内部服务之间的数据通信,安全性非常高。但要注意密钥的保管和定期轮换。
八、实战中的综合防护策略
单一手段都有局限,真正安全的方案一定是多层防御叠加。我建议的优先级是:
第一层:代码层面,尽量避免使用unserialize处理用户输入。如果必须用,设置allowed_classes为最小白名单。
第二层:函数层面,禁用不必要的危险函数,减少攻击面。
第三层:应用层面,加入序列化签名验证或自定义安全反序列化函数。
第四层:网络层面,WAF配置正则规则拦截明显的攻击特征。
第五层:监控层面,对反序列化操作做日志记录和异常告警,发现攻击尝试及时响应。
九、常见POP链利用场景和对应拦截要点
不同框架和CMS的POP链构造方式不同,但核心思路一致。ThinkPHP系列主要利用Request和Response类;Laravel早期版本利用PendingBroadcast和FakeQueue;WordPress利用WP_Widget类。拦截时要针对具体框架的类名做白名单配置,而不是泛泛地过滤所有O:开头的内容。同时要关注框架升级,新版本通常会修复已知的POP链入口。
十、总结
PHP反序列化POP链攻击是Web安全中非常经典且持续存在的威胁。拦截的核心思路就是三个字:控入口、限类名、验数据。从allowed_classes白名单到自定义安全反序列化函数,从WAF正则到RASP运行时防护,从函数禁用到签名验证,每一层都在缩小攻击面。没有银弹,但组合拳打下来,攻击者的成本会高到放弃。做安全防护不是一劳永逸的事,持续关注漏洞情报、定期审计代码、及时更新框架,才是长久之计。
