从零复现0CTF 2016 PHP反序列化漏洞:一个CTF新手的通关日记
记得第一次看到这道题时,我盯着屏幕发了半小时呆——那些看似简单的PHP代码背后,到底藏着怎样的玄机?作为刚接触CTF的菜鸟,我决定用最笨的方法:像侦探破案一样记录每个线索的发现过程。下面就是我的完整解题手记,包含所有踩坑经验和恍然大悟的瞬间。
1. 环境侦查与源码泄露分析
拿到题目首先访问web界面,发现只是个简单的登录页面。按照CTF常规思路,先用dirsearch扫描目录结构:
python3 dirsearch.py -u http://target.com -e php扫描结果中出现了意外的收获:profile.php和update.php可以直接下载源码。这种"源码泄露"在真实漏洞挖掘中其实很常见,但在CTF里往往就是解题的关键入口。
关键文件分析优先级:
profile.php:包含反序列化操作update.php:处理用户输入的核心逻辑class.php:定义用户类方法config.php:可能包含flag
提示:源码审计时建议先全局搜索危险函数,如unserialize、eval、system等,可以快速定位潜在漏洞点
2. 反序列化漏洞的入口定位
在profile.php中发现这段关键代码:
$profile = unserialize($profile); $photo = base64_encode(file_get_contents($profile['photo']));这里形成了典型的"反序列化+文件读取"攻击链。但要想利用这个漏洞,必须满足三个前提条件:
- 能够控制被反序列化的
$profile数据 - 使
$profile['photo']指向目标文件路径 - 绕过
update.php中的输入过滤
通过注册测试账号发现,用户信息确实会被序列化存储。但update.php对每个字段都有严格限制:
| 字段 | 过滤规则 | 绕过难度 |
|---|---|---|
| phone | 必须为11位数字 | ★★☆☆☆ |
| 简单邮箱格式校验 | ★★☆☆☆ | |
| nickname | 只允许字母数字下划线,长度≤10 | ★★★★☆ |
| photo | 文件大小限制5B-1MB | ★☆☆☆☆ |
3. 突破过滤:字符逃逸的艺术
最棘手的限制是nickname字段的过滤。仔细分析class.php中的处理逻辑:
// 序列化后的数据类似: // a:4:{s:5:"phone";s:11:"12345678901";...}当nickname中包含引号时,会导致序列化字符串的结构被破坏。通过精心构造超长nickname,可以实现"字符逃逸"——让后续的photo字段突破引号限制。
构造payload的关键点:
- 计算需要逃逸的字符数(这里需要闭合前面的引号和分号)
- 使用数组绕过长度限制(
nickname[]=) - 确保最终photo参数能指向
config.php
我的第一次尝试失败了,因为没考虑到转义字符的影响。经过多次调试,最终有效的payload结构如下:
class Exploit { public $phone = "12345678901"; public $email = "test@example.com"; public $nickname = ["wherewherewhere..."]; // 170个字符 public $photo = "config.php"; }4. 完整攻击链的组装与执行
实际操作流程分为四个关键步骤:
- 注册测试账号:通过register.php创建普通用户
- 构造恶意数据:使用PHP脚本生成特殊序列化字符串
$exp = new Exploit(); echo serialize($exp); - 绕过前端限制:通过Burp Suite拦截修改请求:
nickname[]=wherewherewhere...&photo=config.php - 触发漏洞:访问profile.php查看返回的base64编码
当在页面源码中看到data:image/gif;base64,PD9waHA...时,心跳瞬间加速。将这段base64解码后,flag终于现身:
flag{9458d34b-ef95-a030-70c9-c1493d6d2017}5. 漏洞原理的深度解析
为什么这个payload能生效?关键在于PHP序列化字符串的解析特性:
- 长度欺骗:超长nickname导致解析器错误计算字符串边界
- 结构破坏:闭合了原本的引号,使photo参数逃逸出限制
- 文件包含:
file_get_contents读取任意文件路径
这种漏洞在实际PHP应用中危害极大,可能导致:
- 敏感文件读取(如/etc/passwd)
- 远程代码执行(结合phar协议)
- 权限提升(篡改序列化对象属性)
6. 防御方案与最佳实践
完成挑战后,我查阅了PHP官方文档,总结出几种防御方案:
代码层防护:
// 使用安全的反序列化方法 $data = json_decode(json_encode($untrusted), true); // 或实现__wakeup()方法进行校验 public function __wakeup() { if ($this->photo !== 'upload/'.md5($file)) { throw new Exception('Invalid photo path'); } }架构层防护:
- 禁用不必要的序列化功能
- 对用户输入实施严格白名单过滤
- 使用签名验证序列化数据完整性
那次深夜三点终于看到flag弹出的瞬间,我对着屏幕傻笑了五分钟。这道题教会我的不仅是技术,更是一种思维方式——永远不要被表面限制困住,漏洞往往藏在数据类型转换的边界处。