CTF出题人视角:我是如何设计‘Easy Notes’这道Session反序列化题的

📅 2026/7/26 16:10:54 👁️ 阅读次数 📝 编程学习
CTF出题人视角:我是如何设计‘Easy Notes’这道Session反序列化题的

CTF出题手记:构建"Easy Notes"的Session反序列化陷阱

在2019年HarekazeCTF赛事中,我设计了一道名为"Easy Notes"的Web题目,这道题后来被收录进BUUCTF题库。与其他CTF题目不同,这道题的核心漏洞点是PHP Session反序列化,但解题路径需要选手串联多个看似无害的功能点。今天我想从出题人视角,分享这道题的设计思路和背后的安全考量。

1. 场景设定与核心机制设计

任何好的CTF题目都需要一个合理的应用场景作为载体。我选择了一个极简的笔记系统,包含三个核心功能:

  • 用户登录:基础身份验证
  • 添加笔记:允许用户创建文本内容
  • 导出笔记:将笔记保存为文件

关键设计点在于完全避免使用数据库。整个应用仅通过Session来维持用户状态,而笔记内容则直接以文件形式存储在服务器上。这种设计看似简化了系统架构,实则埋下了第一个伏笔——Session与用户文件共享同一存储路径。

flag.php的实现非常简单:

<?php function is_admin() { return isset($_SESSION['admin']) && $_SESSION['admin'] === true; } if (is_admin()) { echo getenv('FLAG'); } ?>

2. 路径重合与文件名限制的陷阱

服务器配置将Session文件存储在/var/www/tmp目录,这也是用户笔记的保存位置。这个设计产生了两个关键特性:

  1. 路径重合:Session文件和用户笔记文件物理上位于同一目录
  2. 命名规则:笔记文件名必须满足Session文件命名规范

具体验证逻辑如下:

$filename = 'sess_' . preg_replace('/[^a-zA-Z0-9-]/', '', $_SESSION['user']); if (isset($_POST['title']) && isset($_POST['body'])) { file_put_contents("/var/www/tmp/$filename", serialize([ 'title' => $_POST['title'], 'body' => $_POST['body'] ])); }

这里设置了几个关键限制:

  • 文件名必须以sess_开头
  • 只能包含字母、数字和连字符
  • 用户输入中的..会被替换为空(防御目录穿越)

3. PHP Session引擎的特性利用

默认PHP配置使用php序列化处理器,这个引擎有个鲜为人知的特性:它使用|作为键值分隔符。这意味着我们可以构造特殊的输入来"污染"Session数据。

考虑以下攻击链:

  1. 注册用户名为sess_的账户
  2. 添加笔记时,在title字段注入序列化数据:
    |N;admin|b:1;
  3. 完整Session文件内容将变为:
    title|s:12:"|N;admin|b:1;";body|s:5:"hello";
  4. 当PHP解析这个文件时,会错误地将admin识别为Session变量

4. 文件导出功能的精妙设计

导出功能看似无害,实则暗藏玄机:

$type = str_replace('..', '', $_GET['type']); header('Content-Disposition: attachment; filename="' . $filename . $type . '"');

当传入type=.时:

  1. 首先.不会被str_replace过滤
  2. 最终文件名变为sess_xxx.(其中xxx是随机部分)
  3. 但Linux文件系统会忽略末尾的点,实际访问的还是sess_xxx

5. 完整攻击链的构建逻辑

要让选手发现这个漏洞链,我设计了几个引导点:

  1. 信息泄露:导出功能会返回完整的文件名
  2. 路径提示:错误信息中会显示/var/www/tmp路径
  3. 命名限制:注册时的用户名限制暗示了Session命名规则

预期解题步骤:

  1. 发现导出功能存在文件名拼接
  2. 通过特殊输入获取Session文件名
  3. 利用笔记功能污染Session数据
  4. 伪造PHPSESSID获取管理员权限

6. 防御视角的思考

这道题展示了几个重要的安全原则:

  • 最小权限原则:Session文件不应与用户上传文件混存
  • 输入过滤:简单的字符替换往往不够彻底
  • 上下文感知:序列化处理需要完整上下文验证

在真实环境中,防御措施应当包括:

// 安全的Session配置 ini_set('session.serialize_handler', 'php_serialize'); ini_set('session.upload_progress.cleanup', 'On'); ini_set('session.save_path', '/var/lib/php/sessions');

7. 出题过程中的调试技巧

设计这类题目时,我常用的调试方法包括:

  • 日志记录:在关键位置添加日志输出
  • 差分测试:对比正常和攻击请求的服务器状态变化
  • 沙盒验证:在隔离环境中完整重现攻击链

例如,可以使用这个命令实时监控Session目录:

watch -n 0.5 'ls -al /var/www/tmp && cat /var/www/tmp/sess_*'

从出题人角度看,好的CTF题目应该像侦探小说一样,每个线索都精心布置,最终引导选手发现真相。这道"Easy Notes"题目通过多个看似无害的功能点组合,创造出了一个有趣的Session反序列化挑战。