PHP反序列化漏洞实战:从CVE-2016-7124绕过__wakeup到CTF解题

发布时间:2026/6/22 7:30:23
PHP反序列化漏洞实战:从CVE-2016-7124绕过__wakeup到CTF解题 1. 项目概述从一道CTF题看PHP反序列化的攻防博弈最近在带新人入门Web安全发现很多朋友对PHP反序列化漏洞的理解还停留在“知道有这么回事”的层面一到实战就无从下手。正好攻防世界CTFHub里那道经典的unserialize3题目完美地浓缩了这类漏洞的一个关键考点——如何绕过__wakeup魔术方法。这不仅仅是解一道题拿个flag那么简单它背后折射的是对PHP对象生命周期和序列化机制的深度理解。今天我就以这道题为蓝本手把手带你拆解整个漏洞利用过程从原理分析、代码审计到最终Payload构造让你不仅知其然更知其所以然。无论你是刚接触安全的新手还是想巩固反序列化知识的老兵这篇实战笔记都能给你带来直接的收获。我们最终的目标很明确绕过__wakeup的“防御”拿到藏在服务器里的flag。2. 漏洞原理深度解析序列化、反序列化与魔术方法要绕过__wakeup首先得彻底明白它在整个流程中扮演什么角色。这得从PHP序列化serialize和反序列化unserialize这两个基础操作说起。2.1 序列化与反序列化数据的“打包”与“解包”你可以把PHP的序列化想象成给一个复杂的、活生生的对象“拍一张快照”。这个对象可能有各种属性数据还定义了一系列方法行为。serialize()函数的作用就是把这个对象当前的状态主要是属性值转换成一个字符串格式的“字节流”。这个字符串包含了重建该对象所需的最少信息比如对象的类名、属性名和属性值。这样做的好处是方便存储比如存到数据库或文件里或传输比如通过网络发送。而unserialize()函数则相反它是个“复活”过程。它读取这个序列化字符串并根据其中的信息在内存中重新创建出一个和原来状态几乎一模一样的对象实例。注意这里说的是“几乎”因为序列化主要保存的是对象的数据属性而不是其逻辑方法的代码。方法的逻辑是由类的定义决定的。2.2 魔术方法对象生命周期的“事件监听器”PHP提供了一系列以双下划线__开头的魔术方法Magic Methods它们会在对象的特定生命周期节点被自动调用。在反序列化漏洞的利用中以下几个尤为关键__construct(): 构造函数在对象被创建new时调用。__destruct(): 析构函数在对象被销毁如脚本执行结束、被unset时调用。这是反序列化漏洞中最常见的“攻击入口”之一因为反序列化出来的对象在脚本结束时总会触发析构。__wakeup(): 在对象被unserialize()反序列化之后、自动调用之前执行。它的设计初衷是用于反序列化后重新初始化一些可能丢失的资源比如数据库连接、文件句柄。这里就是unserialize3题目的核心考点__wakeup方法的存在常常被开发者用作一种“安全措施”。他们可能会在__wakeup里重置对象状态、进行一些安全检查甚至直接销毁对象或退出流程从而阻断我们利用__destruct或__toString等后续魔术方法执行恶意代码的企图。2.3__wakeup绕过原理CVE-2016-7124那么__wakeup就真的无法逾越吗并非如此。PHP历史上存在一个著名的特性在特定版本下可以被利用来绕过__wakeup的调用。这个特性与序列化字符串中表示对象属性数量的值有关。一个标准的序列化字符串格式如下O:类名长度:类名:属性数量:{属性序列化...}例如一个TestClass类有一个属性$a值为1序列化后可能是O:10:TestClass:1:{s:1:a;i:1;}这里的1就表示这个对象有1个属性。绕过关键点在PHP 5.6.25之前和PHP 7.0.10之前的版本中如果我们在序列化字符串中将对象属性数量的值修改为比实际属性数量更大的数字那么在反序列化时__wakeup方法将不会被调用但对象依然会被成功反序列化并且后续的__destruct方法会正常执行。这就是我们绕过__wakeup的武器。对于上面的例子我们将字符串改为O:10:TestClass:2:{s:1:a;i:1;}注意属性数量从1改成了2但后面属性的定义并没有增加当这个字符串被unserialize()处理时PHP会尝试读取2个属性但实际数据只定义了1个这会导致解析异常。然而在受影响版本的PHP中这种异常恰好阻止了__wakeup的执行却不妨碍对象被创建以及最终__destruct的触发。注意这个绕过方法有严格的版本限制。在解题或实战中首要步骤就是判断目标PHP版本是否在受影响范围内。攻防世界的unserialize3题目环境通常就是搭建在存在此漏洞的PHP版本上为我们创造了条件。3. 靶场环境分析与代码审计思路明确了原理我们就要开始实战了。面对unserialize3这样的题目标准的解题流程是获取源码 - 代码审计 - 寻找漏洞点 - 构造Payload。3.1 获取题目源码CTF题目尤其是Web题经常通过留备份文件、.git泄露、注释提示等方式提供源码。对于unserialize3常见的方法是访问index.php的备份文件如index.php.bak、index.php~或者尝试www.zip、source.zip等压缩包。有时候直接查看网页源代码也能找到线索。这里我们假设通过常规扫描获取到了核心的PHP源码文件。3.2 核心漏洞代码审计假设我们拿到了如下简化后的源码class.php?php class xctf{ public $flag 111; public function __wakeup(){ exit(bad requests); } public function __destruct(){ // 我们假设在理想情况下这里会输出或操作$flag // 例如echo $this-flag; // 但题目可能把真正的flag放在服务器文件里这里只是示意 // 真正的目标可能是触发这里从而读取flag文件 if (isset($this-flag)) { // 一些关键操作... } } } ?以及一个入口文件index.php?php require_once(class.php); $str $_GET[code]; if (isset($str)) { $data unserialize($str); echo Welcome!; } else { highlight_file(__FILE__); } ?审计过程定位反序列化入口index.php中通过$_GET[code]获取参数并直接传递给unserialize()函数。这是一个明显的、用户输入可控的反序列化点。分析可利用的类代码中只定义了一个类xctf。寻找魔术方法__wakeup(): 该方法直接执行exit(bad requests)。这意味着只要__wakeup被调用程序会立即终止打印“bad requests”我们后续的任何企图都会落空。这是我们必须绕过的障碍。__destruct(): 析构函数。这里虽然看起来只是判断$flag是否存在但在真实的题目场景中__destruct内部可能包含文件读取、命令执行等关键代码或者是触发其他链式调用的起点。我们的目标就是让程序执行到这里。分析属性类有一个公共属性$flag。在反序列化时我们可以通过序列化字符串控制这个属性的值。解题思路链我们需要向code参数传递一个精心构造的序列化字符串。这个字符串要能成功反序列化出一个xctf对象。绕过__wakeup方法防止程序退出。让对象正常走到生命周期结束从而自动调用__destruct方法执行其中的关键代码在真实题目中这可能是获取flag的关键。3.3 确定利用链与攻击面对于这道题利用链非常直接可控输入($_GET[‘code’])-unserialize()-绕过__wakeup-对象销毁-触发__destruct。攻击面就在于我们能否控制序列化字符串使其在反序列化时触发漏洞。结合第2.3节的知识我们确定使用修改属性数量的方法来尝试绕过__wakeup。4. 手把手构造绕过Payload理论结合实践现在我们一步步构造出能绕过__wakeup的Payload。4.1 步骤一创建正常对象并序列化我们先写一个本地脚本模拟创建对象并生成标准的序列化字符串。?php class xctf{ public $flag 111; // 初始值不重要我们可以覆盖它 } $obj new xctf(); // 我们可以修改$flag的值比如指向一个假想的flag文件 // $obj-flag /path/to/real/flag; echo serialize($obj); ?运行这段代码会得到类似以下的输出O:4:xctf:1:{s:4:flag;s:3:111;}字符串解析O: 表示对象Object。4: 类名xctf的长度。xctf: 类名。1: 对象属性的数量本例中只有$flag一个属性。{s:4:flag;s:3:111;}: 这是属性的序列化。s:4:flag表示一个长度为4的字符串属性名flags:3:111表示一个长度为3的字符串属性值111。4.2 步骤二应用__wakeup绕过技巧根据CVE-2016-7124我们需要将属性数量1修改为一个大于实际属性数量的数字比如2或100。同时为了增加利用成功率我们可能还需要修改$flag属性的值。在真实题目中__destruct方法里可能会用$this-flag去做文件读取例如file_get_contents($this-flag)那么我们就需要将$flag的值设置为服务器上存储flag的真实路径这通常需要结合其他信息泄露或路径遍历漏洞来获取有时题目会直接给提示。假设我们通过信息收集知道flag文件在/flag。那么我们先构造一个属性值被修改的序列化字符串O:4:xctf:1:{s:4:flag;s:5:/flag;}然后应用绕过技巧将属性数量1改为2O:4:xctf:2:{s:4:flag;s:5:/flag;}这就是我们的核心Payload。4.3 步骤三进行URL编码与传输由于Payload需要通过GET请求的code参数传递而序列化字符串中包含花括号{}、引号等特殊字符在URL中可能会被错误解析或截断。因此我们需要对其进行URL编码。可以使用在线工具或编程语言函数如PHP的urlencode进行编码。上述Payload编码后大致如下O%3A4%3A%22xctf%22%3A2%3A%7Bs%3A4%3A%22flag%22%3Bs%3A5%3A%22%2Fflag%22%3B%7D4.4 步骤四发起攻击并获取结果在浏览器中访问靶场地址并附上我们的Payloadhttp://靶场地址/?codeO%3A4%3A%22xctf%22%3A2%3A%7Bs%3A4%3A%22flag%22%3Bs%3A5%3A%22%2Fflag%22%3B%7D如果一切顺利服务器接收到code参数。unserialize()函数开始解析我们提供的字符串。由于属性数量2大于实际定义的数量1在存在漏洞的PHP版本中__wakeup方法被跳过。一个xctf对象被成功创建其$flag属性值为/flag。脚本执行到末尾该对象被销毁触发__destruct()方法。在__destruct()方法中根据题目实际代码可能会读取/flag文件的内容并将其输出到页面或者作为响应的一部分返回。这样flag就出现在我们眼前了。5. 实战中的疑难排查与技巧进阶在实际操作中事情往往不会一帆风顺。下面分享几个我踩过的坑和对应的排查思路。5.1 常见问题排查表问题现象可能原因排查思路与解决方案页面返回“bad requests”__wakeup方法未被成功绕过1.确认PHP版本这是最常见的原因。靶场环境可能使用了已修复该漏洞的PHP版本5.6.25或7.0.10。尝试寻找其他入口或利用链。2.检查Payload格式仔细核对序列化字符串的语法确保花括号、分号、引号配对正确属性名长度与实际字符串长度严格一致。一个字符的错误都会导致解析失败从而可能走正常流程触发__wakeup。页面空白或报错非“bad requests”反序列化过程出错1.URL编码问题确保Payload已正确进行URL编码。可以先用urldecode函数验证一下编码后的字符串是否与原始Payload一致。2.属性名修饰符如果类属性是private或protected其序列化后的格式不同。私有属性会在属性名前加上%00类名%00保护属性前加%00*%00。需要根据源码中的属性定义来调整Payload。本题中$flag是public所以最简单。3.开启错误显示如果可能在本地测试时开启display_errors查看具体的PHP错误或警告信息。返回“Welcome!”但无flag__destruct逻辑未按预期执行1.分析__destruct真实逻辑我们之前审计的代码是简化的。真实题目的__destruct可能不是直接输出$flag而是进行其他操作比如调用其他对象的方法POP链的起点。需要更仔细地审计全部源码。2.$flag属性值不对可能flag文件的路径不是/flag而是./flag、flag.txt或位于其他目录。需要结合题目描述、注释、其他接口进行路径猜测或遍历。3.输出被过滤或重定向__destruct中的输出可能被ob_start缓存或者被后续代码覆盖。可以尝试将$flag的值设置为一个Web可访问的URL让服务器发起请求SSRF思路或者写入一个文件。Payload被WAF拦截存在安全防护1.混淆Payload对序列化字符串进行多次编码如Base64URL编码、添加无关字符利用PHP反序列化特性字符串长度后的冒号后可以有空格、拆分参数等。2.更改请求方式尝试将Payload放在POST Body中传递。3.寻找其他入口点也许code参数不是唯一的反序列化点。5.2 高级技巧与扩展思考利用__destruct与__toString构建POP链在更复杂的场景中一个类的__destruct可能会调用另一个对象的某个方法如果那个方法又触发了__toString或其他魔术方法就可能形成一条“属性导向编程POP”链。审计时需要全局搜索所有类的魔术方法寻找可以连接起来的“跳板”。字符串逃逸与字符数量利用这是另一种高级利用技巧。当序列化字符串在反序列化前经过了某些过滤函数如str_replace时可能会因为字符数量的变化导致序列化字符串的边界被“撑开”或“压缩”从而使得后续部分被解析为新的属性实现对象注入。这需要对序列化格式有极其精准的把握。Phar反序列化一种更隐蔽的反序列化入口。如果网站存在文件上传功能且可以上传Phar文件或能通过修改文件头将其他文件伪装成Phar并且有文件操作函数如file_get_contents、include等的参数可控就可能触发Phar包中元数据metadata的反序列化。这是一种将反序列化与文件上传结合的综合利用方式。关注PHP内置类一些PHP内置类如SimpleXMLElement、SoapClient、ArrayObject等的魔术方法在某些情况下可以被利用来发起SSRF、发起请求或进行其他操作。在找不到自定义类利用链时可以研究一下内置类。5.3 防御措施建议开发者视角既然我们作为攻击者研究了利用那么从防御者角度该如何避免此类漏洞呢首要原则不要反序列化不可信数据。这是最根本的。如果业务必须使用序列化考虑使用JSON等更安全的格式。升级PHP版本及时升级到已修复CVE-2016-7124的PHP版本。使用安全的白名单机制如果必须使用unserialize可以配合allowed_classes参数PHP 7.0将其设置为false或一个明确的可信类名数组只允许反序列化基础的、无害的类。对象签名与校验在序列化数据中加入签名HMAC在反序列化前先验证数据的完整性和来源合法性。避免在魔术方法中放入关键逻辑尤其是__wakeup、__destruct、__toString等尽量不要在这些方法中执行文件操作、数据库查询、命令执行等敏感操作。如果必须要严格检查对象属性的来源和有效性。代码审计与漏洞扫描定期对代码进行安全审计使用自动化工具扫描潜在的反序列化漏洞点。回过头看unserialize3这道题它像是一个精致的教学模型把PHP反序列化漏洞中最经典的一个绕过场景单独提炼出来让我们练习。通过它我们不仅学会了一个具体的绕过技巧CVE-2016-7124更重要的是建立起一套面对此类漏洞的通用分析方法找入口、审代码、寻链子、构载荷、试绕过。在实际的渗透测试或CTF比赛中情况会复杂得多可能需要综合运用信息收集、代码审计、链式构造等多种能力。但万变不离其宗对语言特性这里是PHP魔术方法和序列化协议的深刻理解永远是解开这些谜题最可靠的钥匙。下次当你再遇到unserialize时希望你能清晰地想起整个分析流程从容地拆解它。