Writeup复盘方法¶
为什么要复盘¶
很多新手打完一场 CTF 的第一反应是:「终于结束了,休息!」——然后关掉所有文档,下一次比赛又从零开始。这样做的结果是:打了十场比赛,水平和打第一场时差不多。
CTF 圈子里有一句话:比赛结束才是学习的开始。原因很朴素:
- 比赛中你处于「压力模式」,目标是拿分,不是理解。很多题你是照着提示、师傅的口述甚至运气做出来的,知识点根本没消化。
- 比赛限时,你没做出来的题、卡住的点,恰好精确暴露了你的知识盲区。这些盲区不会自己消失,下一场还会再卡你一次。
- Writeup 是出题人和解题者把几个小时甚至几天的思考压缩成的文章,免费、公开、针对你刚打过的题——这是 CTF 学习性价比最高的材料,没有之一。
复盘的目标不是「把比赛记录一遍」,而是回答三个问题:
- 我做出来的题,我是真懂还是蒙的?
- 我没做出来的题,卡在哪一步?是知识不会、思路没想到,还是操作失误?
- 下次遇到同类的题,我凭什么能更快地做出来?
带着这三个问题去复盘,一场比赛的价值能翻好几倍。
如何读别人的 Writeup¶
复盘的核心动作是读 Writeup(比赛后选手们公开的解题过程,常简写为 WP)。但「读」不等于「看懂然后收藏」,正确姿势是对照自己的经历来读。
对照自己的卡点¶
拿到一篇 WP,先别急着从头读到尾。先问自己:这道题我当时做到哪一步?
- 你卡在第 2 步,WP 从第 1 步写起:跳过你已经会的部分,精读你卡住的那一步。重点不是「他用了什么 payload」,而是「他怎么想到要用这个 payload」。是响应里的某个字段提示了模板引擎?还是
filter函数黑名单里漏了某个函数?把这个「触发思路的信号」记下来。 - WP 用的方法和你完全不同:恭喜你赚到了。一道题有两种解法,说明你对它的攻击面理解翻倍了。把两种方法都记录,并注明各自适用的前置条件。
- WP 你也看不懂:说明中间隔着知识点,见下一条。
补知识盲区¶
读 WP 最常见的障碍是「这一步引用了我不知道的东西」——比如 WP 里一句「显然这里可以用 PHP 伪协议 php://filter 读源码」,而你不知道伪协议是什么。
处理办法是:立刻去补,补到能复现这一步为止,够用即止。不要掉进兔子洞把 PHP 源码读一遍,也不要收藏了事。以上面的例子为例,你只需要搞懂:伪协议是什么、php://filter 怎么读文件、为什么能绕过简单的后缀检查。本书的「文件包含」章节正好覆盖了这块内容,配合「PHP特性与常见绕过」一起看,这类题的套路基本就齐了。
一个实用的流程:
读 WP → 遇到不懂的名词/技巧
→ 在本书对应章节(或官方文档)里查原理
→ 找一道只考这个点的入门题亲手做一遍
→ 回到 WP,确认这一步现在能看懂
→ 把这个技巧记入自己的 payload 库 / 错题本
一条最小示例¶
假设某题 WP 的关键一步是:
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=admin'-- -&password=anything
你当时卡在登录框,试了 ' 报错就放弃了。复盘时要记录的不是这条 payload 本身,而是:
- 信号:输入
'返回 500 错误 → 存在 SQL 注入的可能; - 思路:注释掉后面的密码校验逻辑,绕过认证;
- 盲区:不清楚
--后面为什么要跟空格(MySQL 要求--后必须有空白字符才视为注释)。
最后这条盲区,就是你需要去「SQL注入」章节补的知识点。
payload 库的建立¶
随着打的题变多,你会发现很多 payload 在反复出现。靠脑子记迟早会忘,靠搜索引擎现查又慢。解法是建立自己的 payload 库:按漏洞类型组织,每条带适用场景注释。
要点有两个:
- 按漏洞类型分文件/分节,与本书章节结构对应(SQL注入、XSS、SSTI、文件上传……),遇到题时按题型直接翻对应文件。
- 注释比 payload 更重要。只记
{{7*7}}没有用,三个月后你会忘记它是测 SSTI 的探针、以及返回 49 意味着什么。每条 payload 至少注明:干什么用、什么场景下用、预期的成功现象是什么。
推荐的组织形式:每个漏洞类型一个文件(或一个二级标题),内部按「探测 → 利用 → 绕过」排列。示例结构:
# ssti.md —— SSTI 注入 payload 片段
探测:
- payload: "{{7*7}}"
场景: 页面回显输入内容,怀疑服务端模板渲染
现象: 返回 49 → 存在模板注入;返回 {{7*7}} 原文 → 无注入或被过滤
备注: 同时试 ${7*7}(可能对应其他引擎语法)
识别引擎:
- payload: "{{7*'7'}}"
现象: Jinja2 返回 7777777,Twig 返回 49
场景: 区分 Python/Jinja2 与 PHP/Twig,决定后续利用链
利用(Jinja2):
- payload: "{{config.items()}}"
场景: 先泄露配置,常能直接拿到 SECRET_KEY 或 flag
前置: 确认是 Jinja2
这样的库有两个使用时机:比赛中快速翻查;复盘时把新学到的 payload 补进去。它不是抄来的大全(网上现成的大全反而不好用),而是你自己一条一条攒出来的——攒的过程本身就是复习。
除了 payload,库中还可以放:常用命令(如 curl、nc 的惯用参数)、常用脚本(如你写过的盲注脚本模板)、踩过的坑(如「-- 注释后要有空格」)。
错题本做法¶
错题本记录的是你没做出来的题,以及——更重要的——卡的原因。只记答案的错题本是自我安慰:「哦,原来用 scandir 列目录」记一百条,下次换一道题照样不会,因为你缺的不是这个函数,而是「目录被禁言时该怎么枚举文件」这条思路。
每条错题记录建议包含四个字段:
题目:NSSCTF - [羊城杯 2023] 某题(附题目链接)
考点:RCE 绕过(disable_functions 绕过)
我的卡点:拿到了 webshell 但无法执行命令,不知道 disable_functions 的存在
卡点原因(关键):知识盲区——不知道 php.ini 可以禁用函数,
也不知道可以通过 LD_PRELOAD 劫持或利用 PHP-FPM 绕过
正确思路:先用 phpinfo() 查看 disable_functions 列表 →
确认目标函数被禁 → 选择对应绕过手法
沉淀:payload 库 rce.md 新增「disable_functions 探测与绕过」一节
注意「卡点原因」这一项要诚实分类,因为不同原因的对策完全不同:
- 知识盲区(根本不知道某个技术存在)→ 去学对应章节,补进 payload 库;
- 思路缺失(知道技术但没想到用在这里)→ 记录「什么信号应该触发这个思路」,比如「看到 404 页面带服务器签名就该试目录扫描」;
- 操作失误(思路对但打错了、环境没配对)→ 记录具体教训,如「SSTI 的 payload 被前端 URL 编码吃掉了花括号,要用 Burp 改原始包」;
- 时间不足(其实会做)→ 不算错题,但可以记录哪些步骤可以脚本化提速。
定期(比如每打完两三场比赛)翻一遍错题本,统计卡点原因的分布。如果八成都是知识盲区,说明该系统性学章节;如果思路缺失居多,说明该多做题多见识题型。
知识库沉淀¶
payload 库和错题本都是碎片,还需要一个结构化的知识库把它们串起来。知识库的作用是:当遇到一个陌生的漏洞类型时,你有一份自己写的、能快速上手的笔记,而不是从搜索引擎的十个广告链接里淘金。
工具选择¶
够用即止,别在工具上折腾太久。常见选择:
- 本地 Markdown + Git:VS Code / Obsidian 管理一堆
.md文件,配合 Git 备份和同步。推荐,和本书(MkDocs)一样的格式,写顺手了甚至可以直接给本书投稿。 - 在线笔记(语雀、飞书文档、Notion 等):优点是随开随用,缺点是迁移和检索不便,适合轻量记录。
- 博客:把笔记公开发表。写给别人看会逼你把知识讲清楚,是最强的巩固方式,也能积累社区声誉。
无论用什么工具,核心要求是:全文可搜索、结构稳定、能放代码块。
目录组织建议¶
按「漏洞类型 → 原理 + 手法 + 例题」组织,与比赛题型直接对应:
knowledge-base/
├── web/
│ ├── sqli/ # 对应本书「SQL注入」
│ │ ├── 原理笔记.md
│ │ ├── payload.md
│ │ └── 做过的题.md
│ ├── xss/
│ ├── ssti/
│ ├── ssrf/
│ ├── upload/ # 文件上传
│ └── rce/
├── crypto/
├── misc/
└── review/ # 每场复盘的归档,按比赛命名
├── 2024-XX杯.md
└── 2025-XX赛.md
和 payload 库的分工是:payload 库是「弹药」,知识库是「兵法」——前者查具体字符串,后者讲原理、思路和前置条件。两者可以合并成一套笔记(比如上面的结构就是合并版),关键是别只存其中一种。
复盘时「沉淀」动作的标准流程:错题本里解决掉的卡点 → 若是新知识,整理进知识库对应目录 → 若是新 payload,补进 payload.md → 在「做过的题.md」里加一行题目链接和一句话题解。一套下来,这个知识点才算真正归你了。
从解出题到出一道题¶
学习 CTF 的能力链条大致是:看题 → 解题 → 复盘 → 出题。出题是这个闭环的最后一步,也是检验你是否真的理解一个漏洞的终极标准——你必须想清楚漏洞的成因、所有绕过路径、以及哪些「非预期解」需要堵死。
不需要一开始就设计多复杂的题。从「魔改」开始:找一道你复盘过的经典题,改动一个条件,看会产生什么变化。例如经典的文件上传题只校验后缀,你魔改成「加了 getimagesize() 校验图片内容」,解题思路就从「改后缀」变成了「制作图片马 + 配合文件包含」——改动过程中你会被迫真正理解 getimagesize() 到底校验了什么、图片马为什么有效,这正是「文件上传」章节的核心。
一个最小出题示例,考察弱类型比较(对应「PHP特性与常见绕过」章节):
<?php
// challenge.php —— 一道考察 md5 弱比较的入门题
$flag = "flag{w34k_c0mp4r1s0n}";
if (!isset($_GET['a'], $_GET['b'])) {
highlight_file(__FILE__);
exit;
}
// 要求 a != b,但 md5(a) == md5(b)
if ($_GET['a'] !== $_GET['b'] && md5($_GET['a']) == md5($_GET['b'])) {
echo $flag;
} else {
echo "try again";
}
出这道题的过程逼着你回答一串问题:
- 考点是什么?—— PHP 的
==弱比较与0e开头的 md5 值被当作科学计数法(0e123 == 0e456为真)。 - 预期解是什么?—— 找两个 md5 均为
0e\d+形式的字符串,如s878926199a(0e545993274517709034328855841020)和s155964671a(0e342768416822451524974117254469)。 - 有没有非预期解?—— 如果写成
$_GET['a'] == $_GET['b']就漏了,必须对输入本身用!==严格比较;如果不过滤数组,a[]=1&b[]=2会让md5()返回null,null == null成立——所以还需要考虑是否用is_string()拦截,或者干脆把「数组绕过」作为第二考点。 - 难度合适吗?—— 给源码展示(
highlight_file)还是黑盒?黑盒会偏向考信息搜集。
当你能把这几个问题都答清楚,说明你对「弱类型比较」这个点的掌握已经闭环了:下次比赛中再遇到,你不只是「见过」,而是「知道出题人在考什么、坑埋在哪」。
出好的题还可以投给各大赛事或平台,或者反哺社区——写 WP、给本书这样的教程投稿,都是闭环的一部分。教是最好的学,这句话在 CTF 里尤其成立。