Web安全实战:绕过__wakeup漏洞攻防解析
1. 从一道CTF题说起为什么我的反序列化总被“叫醒”大家好我是老张一个在Web安全领域摸爬滚打了十来年的老兵。今天想和大家聊聊一个在PHP安全里既经典又有点“狡猾”的漏洞——__wakeup魔术方法的绕过。这事儿说起来还得从我带新人做CTFCapture The Flag夺旗赛训练时的一个场景讲起。当时我们正在做“攻防世界”平台上的一个经典题目叫unserialize3。题目页面很简单就给了几行PHP代码末尾还贴心地加了个?code的提示。代码大概是这样的class xctf { public $flag 111; public function __wakeup() { exit(bad requests); } } // 注意题目源码里这里少了个右大括号是个小陷阱但不影响核心逻辑很多新手朋友一看到这代码就懵了。他们知道__wakeup()会在反序列化时自动调用而这里一调用就直接exit退出了这还怎么玩这不就是个“死循环”吗反序列化就会触发__wakeup触发就结束根本拿不到后边的flag。我当时就笑了这恰恰是出题人的“良苦用心”啊。它逼着你去思考一个核心问题反序列化的过程真的是一个完全由PHP解释器控制的“黑盒”吗我们有没有可能通过“欺骗”这个解释器让它跳过某些预设的步骤这个场景非常典型它模拟了很多真实世界中的应用逻辑。比如一个购物车对象在反序列化比如从Session或缓存中恢复时__wakeup方法可能会去重新连接数据库、验证用户状态。如果这里存在逻辑缺陷攻击者就能绕过这些安全检查直接让对象进入一个“未初始化”的危险状态。所以理解并复现这个绕过不仅仅是解一道题更是理解一类真实威胁的钥匙。我们先来把最基础的步骤走一遍。要反序列化首先得有个序列化字符串。我们得自己构造一个xctf类的对象并序列化。很多在线PHP沙箱都能做这个事你也可以在本地搭个环境。代码很简单?php class xctf { public $flag 111; public function __wakeup() { exit(bad requests); } } $a new xctf(); echo serialize($a); ?运行后你会得到一个字符串O:4:xctf:1:{s:4:flag;s:3:111;}。这就是xctf类对象的“身份证”。把它传给?code参数果然页面返回了“bad requests”。__wakeup成功拦截了我们。那么突破口在哪里呢就在这个“身份证”的格式里。我们下一节就来好好拆解一下这张“身份证”看看里面到底藏了什么玄机。2. 拆解序列化字符串对象的“身份证”里写了什么上一节我们拿到了那个序列化字符串O:4:xctf:1:{s:4:flag;s:3:111;}。乍一看像天书但其实它的结构非常规整可以看作是PHP对象的“标准化身份证”。我们把它拆开揉碎了看O代表这是一个Object对象。如果是数组这里会是aarray字符串是sstring整数是iinteger。4表示接下来的类名长度是4个字符。xctf对象的类名。1这是整个序列化字符串中最关键的数字之一。它表示这个对象有1个属性也就是成员变量。{ ... }花括号内是这个对象的属性列表。s:4:flag第一个属性。s表示属性名是字符串4是长度flag是属性名本身。s:3:111第一个属性的值。同样s表示字符串3是长度111是值。所以这个字符串完整地告诉PHP解释器“嘿这里有一个对象它属于xctf类这个类有1个属性属性名叫flag它的值是111。”当我们把这个字符串通过?code传给题目时后台的PHP代码我们猜测大致会执行unserialize($_GET[code])。解释器收到这个字符串就开始按图索骥地“复活”这个对象识别出O:4:xctf知道要创建一个xctf类的对象。看到:1:知道这个对象应该有1个属性于是准备好1个属性的存储空间。解析{s:4:flag;s:3:111;}将属性flag的值设置为111。在对象完全“复活”之后按照PHP的规则自动调用该对象的__wakeup()魔术方法。__wakeup()方法执行输出bad requests并退出整个过程结束。问题就出在第2步和第4步之间。PHP解释器在“复活”对象时是严格依赖我们提供的这份“身份证”信息的。如果我们在这份信息上动点手脚比如谎报军情会发生什么这里就引出了那个著名的漏洞原理在PHP特定版本中尤其是涉及CVE-2016-7124当序列化字符串中声明的对象属性数量就是那个:1:大于对象实际拥有的属性数量时__wakeup()方法的执行会被跳过。为什么你可以这样理解解释器按照“身份证”说“有1个属性”去准备但后续的代码逻辑在核对时发现不对劲数量对不上这个反序列化过程就产生了一个“异常状态”。在这个异常状态下作为对象初始化一部分的__wakeup()就被忽略了。这就像一个装配流水线图纸上说有5个零件但你只提供了4个流水线在某个环节卡住报错原本该进行的“最终质检”__wakeup环节就被跳过了。所以我们的攻击思路就从“硬闯”变成了“智取”修改序列化字符串中的属性个数让它在反序列化时“出错”从而绕过__wakeup这个安全检查点。3. 实战绕过手把手修改你的第一个Payload理论懂了咱们就来真刀真枪地干。我们的目标很明确把O:4:xctf:1:{s:4:flag;s:3:111;}中的属性个数1改成一个比实际属性数大的数字。实际属性有几个就一个public $flag。所以我们只要把1改成2、3、100都行。通常我们用2这是最小的有效改动。手工修改Payload原始字符串O:4:xctf:1:{s:4:flag;s:3:111;}修改后字符串O:4:xctf:2:{s:4:flag;s:3:111;}看到了吗就改了一个数字从1变成了2。现在我们把这个新的字符串作为code参数的值提交给题目。构造完整的URL假设题目地址是http://target.com/challenge/那么我们的攻击载荷Payload就是http://target.com/challenge/?codeO:4:xctf:2:{s:4:flag;s:3:111;}在浏览器里访问这个链接。如果题目环境设置正确并且存在这个漏洞你将不会看到“bad requests”这个错误页而是会看到程序继续向下执行最终输出真正的flag比如可能是cyberpeace{e9f913fd20c51f0302b069c2b9f16fba}。为什么这就成功了我们再来复盘一下PHP解释器处理我们新Payload的过程读取字符串看到O:4:xctf准备创建xctf对象。看到:2:解释器认为“用户告诉我这个对象有2个属性我得预留两个属性的位置。”开始解析花括号里的内容s:4:flag;s:3:111;。这解析出了第一个属性flag和它的值111。解析完第一个属性后解释器期待找到第二个属性的描述比如s:5:flag2;...但它发现花括号已经结束了}。预期和实际不符反序列化过程在此处出现异常。由于这个异常发生在对象属性填充阶段在触发__wakeup()这个“最终步骤”之前因此__wakeup()方法没有被调用。虽然反序列化过程可能不完整或有警告但对象的核心部分第一个属性已经被设置。如果后续的代码逻辑比如直接输出$obj-flag或者有其他利用链依赖于这个对象那么它就能在__wakeup失效的情况下继续执行从而让我们达到目的。这个过程是不是很像在玩一个规则游戏我们找到了规则说明书里的一个模糊地带然后巧妙地利用它达成了目标这种感觉正是Web安全研究的乐趣所在。4. 漏洞的根源与影响CVE-2016-7124的前世今生我们上面利用的绕过技巧并不是一个偶然的“特性”而是一个在历史上被正式记录的安全漏洞编号为CVE-2016-7124。它的影响范围是PHP 5.6.25 之前版本和 PHP 7.0.10 之前版本。在这个漏洞被修复前它成了许多CTF题目和真实世界攻击的“常客”。漏洞核心在unserialize()过程中当序列化字符串中表示对象属性个数的值就是O:...:后面的那个数字大于对象实际拥有的属性个数时会跳过__wakeup()魔术方法的调用。为什么会产生这个漏洞这涉及到PHP内核Zend Engine在处理反序列化时的逻辑顺序。简单来说对象的“复活”分为几个步骤分配内存、恢复属性、调用__wakeup。属性计数的校验可能发生在恢复属性之后、调用__wakeup之前。如果校验失败计数不符内核可能直接设置了一个错误状态并提前返回而忘记或跳过了调用__wakeup这一步。这本质上是一个逻辑缺陷。这个漏洞的危害有多大千万别觉得它只能用来解CTF题。在真实的Web应用里__wakeup()方法常常肩负着重要的安全初始化工作重新建立安全连接比如重新连接数据库并验证当前用户凭证。权限复核检查反序列化后的对象其数据权限是否仍然有效。日志与审计记录对象被反序列化这一事件。关键数据清理或重置将某些敏感临时变量置空。如果攻击者能绕过__wakeup就意味着对象可以在不经过这些安全检查的情况下被“复活”。想象一下一个保存着$isAdmin true的用户对象被序列化后存储在Cookie或缓存里。正常情况下反序列化时__wakeup会去数据库核对用户是否还是管理员。但如果绕过__wakeup这个“复活”的对象就直接带着$isAdmin true的属性被使用了导致权限提升漏洞。我遇到过的一个真实案例已脱敏一个电商系统的优惠券对象在__wakeup里会检查优惠券是否过期。攻击者通过篡改序列化数据绕过检查使用了本已过期的优惠券。虽然这个漏洞的利用条件比较苛刻需要能控制序列化字符串但一旦结合其他漏洞如文件上传、缓存注入其威力不容小觑。所以这个漏洞教会我们一个重要的安全编程原则不要完全依赖__wakeup来做唯一的安全检查。关键的安全逻辑应该在多个层面进行验证。5. 防御之道开发者如何堵上这个缺口知道了攻击手法作为开发者或者安全工程师我们该如何防御呢光知道“升级PHP版本”是远远不够的因为很多遗留系统无法立即升级。我们需要从代码层面构建更坚固的防线。1. 升级PHP版本治本之策最根本的解决办法是将PHP升级到已修复该漏洞的版本PHP 5.6.25 或 PHP 7.0.10。在这些版本中即使属性计数不一致__wakeup()也会被正确调用。这是最推荐的做法。2. 严格校验反序列化数据主动防御不要信任任何来自用户输入的反序列化数据。在调用unserialize()之前进行严格的校验。白名单校验如果可能只反序列化预期的、有限的几个类。可以使用PHP的allowed_classes参数PHP 7.0。// 只允许反序列化 MySafeClass 和 AnotherSafeClass $data unserialize($user_input, [allowed_classes [MySafeClass, AnotherSafeClass]]);签名校验如果序列化数据是你自己生成并存储的比如Session在存储时可以对序列化字符串计算一个HMAC签名。在反序列化前先校验签名是否一致确保数据未被篡改。$secret_key your-secret-key; $serialized_data $_COOKIE[session_data]; $signature $_COOKIE[session_sig]; if (hash_hmac(sha256, $serialized_data, $secret_key) $signature) { $obj unserialize($serialized_data); // 继续处理 } else { die(数据已被篡改); }3. 在__wakeup内部增加冗余校验深度防御即使__wakeup被绕过我们也要让攻击者无法轻易利用对象。在__wakeup方法内部不要只做一件事而是进行多层次的校验。校验关键属性是否存在或合法检查对象的核心属性是否被设置值是否在预期范围内。重新从可信源获取状态比如不要依赖对象里存储的$userId而是从当前Session中重新获取用户ID并去数据库查询最新权限。public function __wakeup() { // 假设对象里有个userId属性 if (!isset($this-userId)) { throw new Exception(非法对象缺少用户ID); } // 重新从session中获取当前用户ID并与对象中的进行比对或去数据库验证 $realUserId $_SESSION[user_id] ?? 0; if ($this-userId ! $realUserId) { throw new Exception(用户身份不匹配); } // 继续其他初始化... }将__wakeup与__construct联动可以在__wakeup中调用构造函数的部分逻辑确保对象总是处于一个已知的、安全的状态。4. 避免使用反序列化架构层面这是最彻底的安全方案。评估是否真的需要PHP的序列化功能。对于简单的数据存储JSON (json_encode/json_decode) 是更安全、更通用的选择。JSON没有执行代码的能力风险远低于PHP序列化。对于对象持久化可以考虑使用ORM对象关系映射框架或者设计专门的、无状态的API。给CTF出题者和学习者的建议在CTF中这个漏洞是绝佳的学习案例。但在实际开发中永远不要将用户可控的数据直接传递给unserialize()函数。如果必须用请像处理SQL语句一样怀着最大的警惕去处理它。6. 举一反三更复杂的反序列化漏洞利用场景绕过__wakeup只是PHP反序列化漏洞的“入门课”。真实世界和更高阶的CTF题目中攻击链条要复杂得多。这里给大家介绍几个常见的进阶利用场景帮大家开阔思路。场景一利用__destruct或__toString作为“跳板”__wakeup被绕过了但对象最终还是被创建出来了尽管可能不完整。如果这个类还有其他魔术方法比如__destruct对象销毁时调用或__toString对象被当作字符串使用时调用攻击者就可以利用它们。 假设一个类是这样的class VulnerableClass { public $cmd; public function __destruct() { system($this-cmd); // 危险 } public function __wakeup() { $this-cmd null; // 试图在唤醒时清空危险属性 } }攻击者可以构造一个序列化字符串设置cmd为whoami并修改属性计数绕过__wakeup。当脚本结束或对象被销毁时__destruct会被调用从而执行系统命令。__wakeup里的清理代码因为被绕过而失效。场景二属性类型混淆Type ConfusionPHP序列化字符串是强类型的s:表示字符串i:表示整数。但PHP本身是弱类型语言。如果后台代码在反序列化后对属性进行了不安全的类型转换或比较可能引发逻辑问题。 例如代码中如果存在if ($obj-id 123)这样的严格比较但攻击者将序列化字符串中的i:123;改为s:3:123;虽然值一样但类型不同可能导致校验绕过。场景三配合其他漏洞形成利用链POP Chain这是反序列化漏洞的“终极形态”也称为“面向属性编程”Property-Oriented Programming。攻击者并不直接利用一个类的危险方法而是通过精心控制多个类的属性让它们在反序列化后相互调用最终形成一个调用链Gadget Chain达到执行任意代码的目的。 这需要攻击者对代码库非常熟悉或者利用已知的、包含“危险魔术方法”的通用类库如Monolog、Guzzle等框架组件。防御这种攻击除了前面提到的allowed_classes限制还需要及时更新依赖库减少可利用的“小工具”。一个综合性的CTF例题分析我们来看一个融合了多种技巧的题目灵感来源于Web_php_unserialize。题目代码通常包含__wakeup方法试图将危险属性重置。__destruct方法包含危险操作如读文件、执行代码。对序列化字符串的过滤比如用preg_match过滤了O:加数字的格式试图阻止对象反序列化。可能还需要Base64编码。绕过思路就成了组合拳绕过正则过滤利用PHP解析的特性在O:4中间加一个号变成O:4正则可能匹配不到但PHP依然能识别为对象。绕过__wakeup就是我们刚学的修改属性计数。构造利用链设置好__destruct方法中需要的属性。编码传输将最终的Payload进行Base64编码后传递。// 构造Payload的示例代码 class Demo { private $file fl4g.php; // 目标文件 } $obj new Demo(); $payload serialize($obj); // 得到基础字符串 $payload str_replace(O:4, O:4, $payload); // 绕过正则 $payload str_replace(:1:, :2:, $payload); // 绕过__wakeup $payload base64_encode($payload); // 编码后传输 echo $payload;这种层层递进的挑战正是Web安全渗透测试的缩影观察、分析、试验、组合利用。攻克它的成就感无与伦比。7. 工具与练习如何高效地挖掘和验证这类漏洞工欲善其事必先利其器。手动拼接序列化字符串在学习和简单场景下没问题但在复杂漏洞挖掘或CTF比赛中我们需要更高效的工具。1. 序列化/反序列化在线工具与本地环境在线PHP沙箱对于快速测试单段代码非常方便。比如像前面提到的快速生成一个类的序列化字符串。但切记不要在其中处理任何真实、敏感的业务代码。本地PHP环境最推荐的方式。安装一个PHP CLI命令行界面环境写个脚本随时测试。这是最安全、最可控的方式。php -r class Test{public $ahello;} echo serialize(new Test());2. 使用Python进行自动化Fuzz和Payload生成在CTF或渗透测试中我们经常需要批量尝试或生成复杂的Payload。Python是绝佳的帮手。手动构造对于简单的修改如改属性个数直接用字符串替换。original O:4:xctf:1:{s:4:flag;s:3:111;} payload original.replace(:1:, :2:) print(payload)使用phpserialize库对于复杂的对象结构可以使用Python的phpserialize库来精确构造和修改。import phpserialize # 创建一个类似PHP的字典结构来模拟对象属性 data {flag: 111} # 注意直接模拟对象比较复杂通常用于数组结构。对象Payload手动构造或使用PHP生成更简单。3. 靶场练习推荐理论结合实践才能学得牢。除了“攻防世界”的unserialize3我强烈推荐以下几个练习平台和题目PortSwigger Web Security Academy (Burp Suite官方靶场)它的“Insecure deserialization”实验模块非常系统从基础到高级涵盖了Java、PHP、Python等多种语言的反序列化漏洞并且有详细的讲解和提示。HackTheBox和TryHackMe上面有很多包含反序列化漏洞的实战机器Box难度从易到难。你需要在一个完整的模拟环境中发现并利用漏洞这对综合能力提升极大。CTFlearn、Root-Me这些平台有大量分类清晰的CTF题目搜索“deserialization”可以找到一堆。我的学习心得我刚开始学的时候喜欢把每个遇到的序列化Payload都存下来建一个自己的“武器库”。并且我会用注释详细记录这个Payload是针对什么场景、绕过了什么防护、在哪个PHP版本下有效。时间长了这就是你最有价值的经验库。遇到新漏洞时翻翻自己的笔记往往能快速找到思路。安全研究是一条漫长的路反序列化只是其中一座有趣的山峰。从理解__wakeup绕过开始一步步去探索__destruct、__toString、POP Chain的奥秘你会发现一个由代码逻辑构成的、充满挑战与乐趣的全新世界。记住最重要的不是记住多少个Payload而是培养那种“看到代码就能在脑中模拟其执行流程”的直觉以及“永不信任用户输入”的安全意识。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2412012.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!