PHP反序列化漏洞攻防实战:从魔术方法到POP链构造与WAF绕过

发布时间:2026/8/2 7:20:53
PHP反序列化漏洞攻防实战:从魔术方法到POP链构造与WAF绕过 1. 项目概述从靶场到实战理解反序列化漏洞的攻防脉络最近在带新人入门Web安全发现很多朋友在CTFshow的Web入门系列里一到反序列化这块就容易卡壳尤其是Web265到Web270这几关。这几关的设计非常典型几乎覆盖了从最基础的PHP反序列化触发到利用魔术方法构造POP链再到绕过常见WAF和过滤的完整路径。我自己当年也是从这些题目里摸爬滚打过来的深知如果只是对着网上的Writeup抄一遍payload而不去理解背后的“为什么”那下次遇到变种还是两眼一抹黑。所以今天我就以这几道题为蓝本结合我这些年挖洞和审计的经验带大家把反序列化漏洞从原理到利用再到防御彻底掰开揉碎了讲清楚。反序列化漏洞简单说就是把一串“死”的数据重新“复活”成一个内存中的对象。这个过程本身没问题问题出在“复活”时程序会自动执行对象中的一些特殊方法在PHP里就是魔术方法在Java里就是readObject。攻击者通过精心构造这串数据就能让程序在反序列化时执行我们预设的恶意代码。CTFshow的这六道题正是沿着这个逻辑层层递进从最简单的__wakeup()绕过到利用__destruct()或__toString()作为跳板再到无参数函数RCE和字符串逃逸这种高级技巧。搞懂它们你不仅能在CTF里拿分更能理解真实世界中诸如Fastjson、Shiro等著名反序列化漏洞的底层逻辑。2. 核心原理深度拆解对象如何被“唤醒”与“操控”要利用漏洞必须先理解原理。很多人觉得反序列化原理枯燥其实我们可以把它想象成“乐高说明书”和“乐高模型”。2.1 序列化与反序列化的本质假设你拼好了一个复杂的乐高战舰现在想寄给朋友。直接寄成品容易损坏你会怎么做你会按照一定规则把它拆成零件并写一份详细的“搭建说明书”记录每个零件属性的类型、值以及拼接顺序数据结构。这个过程就是序列化serialize生成的那份“说明书”就是序列化字符串。你的朋友收到后按照“说明书”把零件重新拼成战舰这就是反序列化unserialize。在PHP中一个简单的类序列化后是这样的class User { public $username admin; private $password 123456; } $obj new User(); echo serialize($obj); // 输出O:4:User:2:{s:8:username;s:5:admin;s:14:\000User\000password;s:6:123456;}解读一下O表示对象4是类名长度User2表示有2个属性。接着是属性列表s:8:username表示字符串类型、长度8、键名username后面跟着它的值s:5:admin。注意私有属性password它的键名被编码为\000User\000password这是PHP内部对类私有变量的一种表示方式。2.2 危险的“自动执行”魔术方法反序列化的危险就藏在这份“说明书”被读取和执行的过程中。PHP提供了一系列魔术方法Magic Methods它们会在对象生命周期的特定节点被自动调用就像乐高模型里预设的“电动组件”。__wakeup(): 当对象被反序列化从字符串“唤醒”时自动调用。常用于重新建立数据库连接等初始化操作。__destruct(): 当对象被销毁如脚本执行结束时自动调用。常用于关闭文件句柄、清理资源。__toString(): 当对象被当作字符串处理如echo $obj时自动调用。__get(),__set(): 当访问或设置不可访问的属性时调用。__call(): 当调用不可访问的方法时调用。攻击者的核心思路就是构造一个恶意的序列化字符串当它被反序列化成一个对象时利用这些自动调用的魔术方法作为“跳板”最终执行任意代码或命令。关键理解漏洞的触发点unserialize()往往不可控比如从用户输入接收但我们可以控制传入的数据。我们的目标是让反序列化产生的对象在其生命周期内通过一连串的魔术方法调用即POP链走到一个能执行危险函数如eval(),system()的地方。2.3 POP链将多个“跳板”连成攻击路径单一魔术方法可能无法直接完成攻击。POPProperty-Oriented Programming链就是通过精心设置对象的属性让一个魔术方法调用另一个对象的方法或访问其属性从而触发另一个魔术方法像多米诺骨牌一样最终达到恶意目的。例如A类的__destruct()方法里调用了$this-abc-run()。如果我们将$this-abc设置为B类的一个对象而B类的run()方法里又调用了eval($this-cmd)那么一条从__destruct()到eval()的POP链就形成了。在序列化字符串中我们需要精确地描述这种对象间的引用关系。3. 靶场实战解析从Web265到Web270的闯关思路下面我们进入CTFshow的实战关卡。我会重点讲思路和原理payload你可以自己尝试构造这才是提升的关键。3.1 Web265__wakeup()的绕过与属性数量修改这通常是反序列化的第一课。题目源码通常会有一个类它的__wakeup()方法会把我们想要的属性比如$admin重置或删除阻止我们直接赋值。class ctfShowUser{ public $usernamexxxxxx; public $passwordxxxxxx; public $isVipfalse; public function __wakeup(){ $this-isVip false; // 关键唤醒时重置isVip } public function __destruct(){ if($this-isVip){ // 输出flag的关键代码 } } }我们的目标是让__destruct()中的判断成立。但__wakeup()会在反序列化后立刻执行把isVip改回false。怎么办这里涉及PHP CVE-2016-7124的一个特性当序列化字符串中表示对象属性数量的值大于真实的属性数量时__wakeup()方法将不会被执行。这是早期非常经典的一种绕过方式。原始序列化字符串可能是O:11:ctfShowUser:3:{...}数字3表示3个属性。我们将其改为O:11:ctfShowUser:4:{...}数字4大于3同时确保payload中仍然只定义那3个属性。这样在反序列化时__wakeup()被跳过我们预设的$isViptrue就得以保留从而在__destruct()中触发flag。实操心得这种绕过方式在PHP版本 5.6.25 或 7.0.10 中有效。现在的CTF题目和真实环境为了兼容性可能仍会模拟此场景。遇到__wakeup()阻拦属性数量修改是第一尝试方向。3.2 Web266-Web267利用__destruct()或__toString()构造POP链这两关开始引入多个类需要构造简单的POP链。题目可能给出两个类class ctfShowUser{ public $class; public function __destruct(){ $this-class-getFlag(); // 关键调用$class对象的getFlag方法 } } class backDoor{ public $code; public function getFlag(){ eval($this-code); // 危险函数 } }攻击思路非常清晰目标是执行backDoor类中的eval($this-code)。入口点在ctfShowUser的__destruct()它调用了$this-class-getFlag()。因此我们需要让ctfShowUser对象的$class属性成为backDoor类的一个对象。同时设置backDoor对象的$code属性为我们想执行的PHP代码例如system(cat /flag);。构造的序列化字符串结构如下O:11:ctfShowUser:1:{s:5:class;O:8:backDoor:1:{s:4:code;s:18:system(cat /flag);;}}这表示一个ctfShowUser对象其class属性是另一个backDoor对象该backDoor对象的code属性是恶意代码字符串。3.3 Web268无参数函数RCE与字符串逃逸进阶这一关难度提升通常会有严格的过滤比如禁用括号()、分号;或者对序列化字符串进行替换操作导致长度变化。这里会用到两种高级技巧技巧一无参数函数读取文件当不能带参数时如何读取文件可以利用一些返回数组或字符串的函数进行“套娃”。session_id(): 可以设置并获取当前会话ID。通过session_start()后我们可以把PHPSESSID设置为一个文件名然后session_id()就能返回它。但如何包含文件需要配合include或highlight_file这通常又需要参数。此时可以看技巧二。getcwd(): 获取当前目录。scandir(): 列出目录文件返回数组。array_reverse(),next(),current()等数组操作函数可以在没有数字索引的情况下遍历数组最终定位到flag.php文件。readfile(),highlight_file(): 最终输出文件内容。如何把文件名传给它们可能需要利用implode()、array_pop()等从数组中提取。技巧二字符串逃逸序列化字符逃逸这是反序列化中非常精妙的一种攻击。当源码对序列化字符串进行了字符串替换并且替换前后字符串长度发生变化时就可能引发逃逸。原理PHP反序列化过程是“一个字符一个字符”解析的。它根据序列化字符串中的格式定义如s:5:value来精确读取后续的5个字符作为值。如果我们通过过滤函数人为地改变了字符串的长度但格式定义中的长度值没有变就会导致解析错位。案例假设源码有str_replace(bad, good, $serialized_data)。我们把属性值设置为badbadbad序列化格式为s:9:badbadbad。经过替换后内容变为goodgoodgood长度变成了12。但格式头s:9仍然告诉解析器只读9个字符。这会导致解析器把goodgoodg当作当前属性的值而剩下的ood会被当作后续的序列化格式的一部分去解析从而可能“逃逸”出原本的属性值边界去构造我们想要的对象属性。利用这通常用于两种目的1构造虚假属性逃逸出的字符可以闭合当前对象并开始定义新的、源码中不存在的属性。2修改变量值通过精确计算逃逸长度可以改变后续属性的值。在Web268这类题目中往往需要你将无参数RCE和字符串逃逸结合。例如通过逃逸修改某个属性使其成为一个包含文件名的数组然后利用无参数函数链遍历数组并读取文件。3.4 Web269-Web270综合利用与WAF绕过最后两关通常是前面所有技巧的综合并增加一些简单的WAF过滤比如过滤了system、exec、passthru等明显的关键词。过滤了反引号、$等特殊字符。限制了代码长度。绕过方法函数替换不能用system可以用shell_exec、proc_open、popen。甚至可以用pcntl_exec需安装扩展。字符串构造如果过滤了cat、flag等关键词可以用拼接、反转、取反、异或等方式动态生成字符串。拼接$_‘c‘.‘a‘.‘t‘;或$a‘c‘;$b‘a‘;$c‘t‘;$_$a.$b.$c;取反~运算符。(~%8C%86%8C%8B%9A%92)在PHP中经过URL解码后取反就是system。这是非常常用的绕过方式因为WAF通常匹配明文而取反后是一堆乱码。异或通过两个字符串异或得到目标字符串。利用已有类原生类利用PHP内置了很多类比如SplFileObject可以用来读取文件GlobIterator可以用来遍历目录SimpleXMLElement可以配合XXE。当题目限制了自定义类或者开启了phar反序列化时这些内置类就是我们的武器。phar://协议反序列化这是另一个重量级考点。phar文件本质上是一个PHP归档文件但其元数据metadata部分会被以序列化的形式存储。当使用phar://协议去访问一个文件时如include(‘phar://test.phar‘)其元数据会被自动反序列化。这意味着如果存在文件操作函数且我们能控制其部分参数我们就能通过上传一个恶意构造的phar文件图片马即可然后利用phar://协议包含它从而触发反序列化将文件上传点转化为反序列化漏洞触发点。4. 漏洞挖掘与防御从CTF到真实世界通过CTF练习我们掌握了攻击手法。但作为安全从业者更重要的是知道如何发现和修复它。4.1 漏洞挖掘思路白盒/黑盒白盒审计代码审计寻找入口点全局搜索unserialize()、json_decode()配合object_hook、yaml.loadPython、ObjectInputStream.readObject()Java等函数。追踪数据流确认传入unserialize()的参数是否用户完全可控来自$_GET、$_POST、$_COOKIE等。分析可利用的类检查代码中定义的类尤其是那些包含了魔术方法PHP或实现了Serializable接口Java的类。绘制类与方法之间的调用关系图寻找可能的POP链。寻找“危险终点”在调用链的末端寻找eval()、exec()、file_put_contents()、unlink()等危险函数。黑盒测试识别序列化数据观察请求中是否存在Base64编码的、格式类似O:4:、a:2:的长字符串或者JSON格式中带有__type、class等可能指示类型的字段。测试点Cookie、Session、表单隐藏字段、API参数、文件上传后的元数据处理等。使用工具辅助可以尝试使用ysoserialJava、PHPGGCPHP等工具生成针对通用库如ThinkPHP、Laravel、Fastjson的payload进行模糊测试。注意仅在授权测试的环境中使用4.2 防御方案与实践修复反序列化漏洞核心原则是不要反序列化不可信的数据。首选方案使用安全的数据交换格式彻底放弃序列化。对于需要持久化或传输的数据使用JSON、XML等纯数据格式。它们只描述数据不包含可执行逻辑。在PHP中用json_encode()/json_decode()在Java中用Jackson、Gson库注意配置不解析任意类。严格校验与白名单如果必须用序列化签名/验签对序列化字符串进行HMAC签名反序列化前先验证签名确保数据未被篡改。白名单校验在反序列化前通过钩子函数或回调限制允许反序列化的类名。例如// PHP 7 的 allowed_classes 参数 $data unserialize($input, [‘allowed_classes‘ [‘MySafeClass1‘, ‘MySafeClass2‘]]);// Java中使用ObjectInputFilter ObjectInputFilter filter ObjectInputFilter.Config.createFilter(com.safe.*;!*); ObjectInputFilter.Config.setSerialFilter(filter);代码层加固避免在魔术方法中放入关键逻辑尤其是__wakeup()、__destruct()、__toString()尽量不要在这里执行文件操作、数据库查询、命令执行等。关键方法设为final防止POP链通过子类重写方法进行连接。使用__sleep()控制序列化字段只序列化必要的、安全的属性。依赖库安全及时升级框架和组件修复已知的反序列化漏洞如Fastjson、Shiro、Jackson的历史漏洞。定期使用SCA软件成分分析工具扫描项目依赖。5. 常见问题与排查技巧实录在实际测试和教学中我总结了一些新手最容易踩的坑5.1 Payload构造后不生效检查属性修饰符PHP中private和protected属性在序列化字符串中的格式与public不同多了类名和空字符的包装\x00。如果你在代码里看到private $cmd在构造payload时属性名必须是\x00类名\x00cmd。直接写s:3:“cmd”会失败。一个技巧是先写一个本地脚本创建对象并赋值然后serialize()出来复制这个标准的格式。检查字符转义序列化字符串中的引号、反斜杠都需要转义。特别是在URL传递时需要先urlencode()。在Burp Suite里直接修改时要注意长度字段s:xx必须与实际字符串的字节长度完全一致否则解析会失败。注意PHP版本差异__wakeup()绕过、protected属性格式等在PHP不同版本间有细微差别。测试环境尽量与目标环境保持一致。5.2 明明有POP链为什么执行不到魔术方法调用条件未满足比如__toString()需要对象被当作字符串使用如echo,str_replace等。检查你的链子是否每个“跳板”都被正确触发。可以在本地环境打印日志或使用__debugInfo()魔术方法辅助调试。属性访问权限问题__get()只在访问不存在或不可见的属性时触发。如果你直接访问一个public属性它不会走__get()。代码逻辑短路某些方法里可能有if判断提前return或者抛出异常导致链子中断。需要仔细审计代码逻辑。5.3 面对过滤和WAF无从下手逐步测试先提交一个最简单的合法序列化字符串看是否被拦截。然后逐步添加特征确定WAF过滤的具体规则是黑名单关键词正则匹配长度限制。多备几种绕过手法取反、异或、编码Base64、Hex、字符串拼接、利用PHP动态函数特性$func ‘sys‘.‘tem‘; $func(‘whoami‘);。利用环境特性phpinfo()函数是宝库查看disable_functions列表看看哪些函数没被禁用。查看已加载的扩展也许有FFI、COM这类“冷门”但强大的组件可用。5.4 真实漏洞利用与CTF的区别信息获取CTF通常给源码。真实环境需要信息收集猜框架、猜组件版本通过报错信息、静态资源文件如package.json、composer.json等寻找线索。POP链构造CTF的链子往往很短。真实世界利用著名框架的漏洞链子可能非常长如Shiro的CB链、Fastjson的JNDI链需要借助ysoserial、PHPGGC等工具生成。利用场景CTF往往是直接RCE拿flag。真实攻击可能是用来打内网横向移动、写入Webshell、窃取数据库凭证等目标更具多样性。反序列化漏洞的学习曲线比较陡峭但它是Web安全知识体系里承上启下的一环往上连接着代码审计往下贯通着高级利用。把CTFshow Web265-Web270这六关吃透反复练习直到你能在不看任何提示的情况下独立构造出每一关的payload并且能清晰说出每一步的原理那么你就已经打下了非常扎实的基础。接下来去尝试一些更复杂的综合靶场或者开始审计一些开源项目的代码你会发现自己打开了一个新世界的大门。