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运行时防护,从函数禁用到签名验证,每一层都在缩小攻击面。没有银弹,但组合拳打下来,攻击者的成本会高到放弃。做安全防护不是一劳永逸的事,持续关注漏洞情报、定期审计代码、及时更新框架,才是长久之计。