1. 三场比赛连轴转的赛后状态:题目可以不会,复盘必须到位
虎符、红明谷、ctfshow渔人杯这几场比赛扎堆出现在赛历上之后,我身边大多数人的状态都差不多:比赛那两天高度紧张,白天盯着题目发呆,晚上躺床上还在想某个报错是不是绕过姿势不对;等到比赛一结束,又在各种群里蹲官方题解和选手writeup。我自己也经历了这么一轮,但真正让我觉得有收获的,不是赛中的灵光一现,而是赛后那一个星期集中做的复现。
说实话,CTF比赛比的从来不只是"知识点会不会",而是"在有限时间内能不能把会的东西用出来"。很多题目放在题库里,你慢慢磨总能做出来;可一到了比赛,题目一变长、环境一变复杂、时间一变紧,原本熟练的操作突然就卡壳了。我这次在三个比赛里的最大感受就是:我的失分点大多不是因为题目超纲,而是因为自己的排查链路不够流畅——赛后才意识到,那些丢分题只要按照平时刷题的节奏慢慢理,其实都能做出来。
所以这篇文章不打算做那种"全场题目逐个详解"的writeup整理,而是想从赛后复现的角度出发,聊聊我在这三场比赛结束后,是怎么把Web、Pwn、Misc几个方向重新捡起来盘的,以及哪些工具、哪些步骤、哪些思考方式是我觉得最值得沉淀下来的。如果你也是那种"比赛结束就不知道该干嘛"的选手,这篇应该能给你一个还算明确的复现方向。
1.1 虎符杯留给我的几个"比赛时根本想不起来"的考点
虎符杯的题目风格给我留下一个很深的印象:它不太喜欢出一个单知识点的小题,而是喜欢把两三个基础考点叠在一起放进同一个场景里。比如一道Web题,你得先通过信息收集发现备份文件,再从备份文件里找到数据库连接信息,接着才能进入真正有注入点的页面;如果你只盯着入口页面的参数看,大概率会错过后面一大串东西。
这种"套娃式"出题方式,恰恰是比赛中最容易让人心态崩掉的。因为平时在ctfshow刷题时,题目编号基本已经把考点暗示得很清楚了,比如"web入门 sql注入",你看到标题就知道该往注入方向试;但比赛里没有这种提示,拿着一个看似正常的登录框,你得自己去判断该测SQL注入、弱口令、还是逻辑越权。
赛后复现虎符的题目时,我给自己定的规矩是:不急着看别人的writeup,先把比赛当时的所有操作记录翻出来,找到自己第一次"思路断掉"的位置。比如有一道题,我看到页面注释里有一个奇怪的路径,当时犹豫了一下没点开,后来发现那就是整道题的入口。复盘时我专门把这个动作记了下来:遇到注释信息、奇怪的响应头、JS文件里的隐藏路由,第一反应不是继续测当前页面参数,而是先收集这部分信息。它不一定每道题都有用,但一旦有用,就是决定性的。
1.2 红明谷题目与现实业务的贴近程度让我意识到差距
红明谷给我最大的冲击不是题目有多难,而是它很多题目场景的"真实感"比普通CTF高很多。平时我们刷题面对的很多靶场,都是为CTF专门设计的:端口就一个,功能很单一,漏洞点非常清晰。但红明谷的部分题目更像是把一个简化版的业务系统丢给你,界面上有各种功能按钮,数据库、缓存、日志模块都有,你需要自己判断漏洞可能藏在哪一层。
这种风格对日常训练的要求就变了。如果你只在ctfshow这类题库里按编号刷题,习惯了"拿到题目直接测参数"的节奏,遇到业务仿真的环境会很容易不知道从哪入手。赛后复现红明谷题目时,我发现自己的问题出在"信息收集太粗糙"上:我只关注了HTTP请求里肉眼可见的参数,没有仔细看Cookie结构、没有检查是否存在多个同类型接口、也没有去对比登录前后返回内容的差异。
因此我在复现这类题目时额外做了一个动作:先把整个应用的目录结构、路由列表、接口参数全部过一遍,形成一个"功能地图",然后再逐个功能去看它是否存在安全风险。这个习惯放到Web方向所有题目里都很受用,后面我也一直在提醒自己:别急着打,先看清楚战场长什么样。
1.3 ctfshow渔人杯:萌新题库和比赛题之间的那座桥
ctfshow平台上有一个很大的题库体系,从萌新入门到web应用安全与防护、从misc入门到pwn基础,题号非常多。渔人杯这种比赛,很大程度上就是把这些日常训练内容做了一次"混编":你不会再知道某道题对应哪个题号,但所有考点的原型都能在题库里找到。
我注意到网络上一搜"ctfshow web入门 sql注入"这类关键词的人特别多,这说明很多人其实就是从ctfshow开始接触CTF的。渔人杯对这批人来说,是个很合适的中间站:它的题目难度跨度比纯萌新题大,又比虎符这类大赛的决赛题温和,用来检验自己"刷题刷到什么水平了"非常合适。
赛后复现渔人杯题目时,我的处理方式和前两个比赛不太一样:我更多是把它当成"查漏补缺"用的。哪道题没做出来,我就回ctfshow去找同考点的基础题,先确认自己对基础知识的理解没有偏差,再回来看比赛里的变形。比如HTTP头注入,如果我只知道它大概和SQL注入有关,但不知道具体在哪个Header里触发,那我就会把题库里相关题目翻出来重新做一遍,把这个知识点彻底打通。后面我会详细说说我在Web方向复现时梳理的完整思路。
2. Web题复现,绕不开的SQL注入、HTTP注入与"过滤盲区"
Web方向永远是我赛后复现的第一站,原因很实在:它环境最好搭,本地起一个服务就能反复试payload;而且Web题目的知识点和流行关键词高度重合,搜一下"ctfshow web入门 sql注入"能找到一大堆练习材料,非常利于对照复现。
我在这次三个比赛中,Web方向暴露的问题比较集中:一是SQL注入的过滤绕过不够系统,二是对HTTP请求层的一些隐式入口不敏感,三是在拿到一个不熟悉的源码后,缺少一套固定的过滤规则分析路径。赛后复现的时候,我针对这三个问题分别做了补课。
2.1 整理失败点:先从sqlmap的日志开始
有一种情况很常见:比赛时遇到一个疑似SQL注入点,时间紧迫,直接上了sqlmap,结果它跑了很久也没有跑出数据,最后时间耗尽就放弃了。赛后千万别直接把日志删了,sqlmap的日志是很有价值的复现线索。
我一般会用sqlmap -u "http://target/news.php?id=1" --batch --level=3 --risk=2 --log-file=sqlmap.log这类命令重跑一遍,然后去看日志里它到底测试了哪些payload、在哪个阶段停止、是因为响应异常还是检测规则把它拦了。很多时候你会发现,sqlmap不是没有找到注入点,而是因为需要某种绕过条件,它的默认模板没有触碰到。
比如它可能在日志里尝试了AND 1=1、AND 1=2,但目标过滤了空格或者过滤了AND,这种情况下即使存在注入,sqlmap默认的检测也可能失效。复现时如果发现这种情况,就要手动用注释符/**/代替空格,或者使用%0a等替代空白字符,先确定注入是否存在,再谈后续的利用。手动测试的基本思路是:先提交一个正常参数看响应基线,再提交单引号等特殊字符观察报错变化,然后尝试布尔条件表达式看响应是否出现差异。
2.2 SQL注入从"能用poc"到"懂原理"的差距
很多新手拿到SQL注入,第一反应是' or 1=1#进去,能过就过,不能过就陷入迷茫。赛后复现时最值得花时间补的基础,就是搞懂后端查询大概长什么样。比如一个登录查询,后台逻辑很可能是:
SELECT * FROM users WHERE username='$username' AND password='$password'我传入admin' or '1'='1后,拼接出来的语句就变成:
SELECT * FROM users WHERE username='admin' or '1'='1' AND password='...'这里的核心是"闭合前一个单引号,并让后续条件恒真"。
接着就要练习各种过滤绕过思路,我按照"输入被过滤的地方"把它们分成几类:
| 过滤点 | 常见绕过手法 |
|---|---|
| 空格被过滤 | 使用/**/、%0a、反引号或Tab字符替代 |
or/and被过滤 | 尝试` |
select被过滤 | 使用内联注释/*!50000select*/或拆分字符串 |
information_schema被过滤 | 尝试使用sys.schema或通过join方式探测 |
| 引号被过滤 | 考虑宽字节注入,例如在GBK编码下使用%df%27 |
宽字节注入是个很经典的坑。如果程序使用了GBK编码,并且把用户输入的单引号转义为\',那么在单引号前加上%df,%df\就会被当作一个中文字符,从而让单引号逃逸出来。复现这类题目时,我通常用Burp Suite改包,把URL里的参数改成%df%27,观察页面是否出现SQL报错,来判断宽字节注入是否生效。
除了布尔盲注和报错注入,堆叠注入也是近年比赛的高频考点。比如在MySQL环境里,如果目标允许一次执行多条语句,那么:
?id=1;show tables;--+就可能直接在响应中带出表名列表。赛后复现时,我会专门在本地搭建一个MySQL环境,把这类带分号的payload跑一遍,从而理解"堆叠注入能用、能带回显、为什么默认情况下页面不回显"这些细节。
2.3 HTTP请求层的隐式入口:Header注入的复现思路
网络热词里有一个"ctfshow http注入这么完成",我猜测有不少人搜这个,是因为在题目里发现往URL参数里注入不管用,但问题其实出在HTTP头部。这是很多人做Web题的一个盲区:明明把参数值翻了个底朝天,却从来没想过改动User-Agent、Referer、X-Forwarded-For这些请求头。
HTTP头注入的常见场景是:后端日志或数据库会记录用户的浏览器信息、来源页面、IP地址等信息。比如登录成功后页面会显示"欢迎你,来自xxx的用户",如果你把User-Agent改成:
User-Agent: ' OR '1'='1而后端把UA的内容拼进了SQL查询,就会产生注入点。类似地,X-Forwarded-For如果被用于写入数据库,也可能存在同样的隐患。
复现这类题目我一般用Burp Suite的重放器,在Raw视图里直接修改请求头,然后观察响应是否出现异常或变化。重点测试的请求头包括:
User-AgentRefererX-Forwarded-ForX-Real-IPClient-IP- 自定义头如
X-Custom-Header
如果后端是记录访问日志并输出到页面的场景,还可以尝试头注入叠加XSS,比如在UA里构造<script>alert(1)</script>,看是否被原样输出。这类题目在ctfshow题库里其实也有原型,复现时只要把重点从"URL参数"切换到"整个HTTP报文",思路一下子就打开了。
2.4 ctfshow题库编号之后的通用总结:过滤绕过像一个排列组合
很多人在群里问"ctfshow web165怎么做""ctfshow web82的考点是什么",但我个人觉得,题号本身并不重要,重要的是从这些题目里抽出来的通用规律。ctfshow的Web题库编号看起来又多又杂,实际上核心Web漏洞类型就那些:SQL注入、文件上传、命令执行、文件包含、反序列化、SSRF、XXE、条件竞争等。
以搜索热度很高的"ctfshow web入门 sql注入"类题目为例,你刷到后面会发现,真正的拦路虎往往不是"不知道注入语法",而是"不知道后端过滤规则"。过滤规则千奇百怪,可大体上就是一个排列组合问题:后端过滤了关键字、空格、注释符、引号、逗号、等号中的某几项,你需要找到漏掉的那一项。
我在复现时会把每道题的后端过滤规则猜一遍,并记录成笔记。比如某道题过滤了select和空格,那么动态构造的payload可能就是:
id=1/**/union/**/select/**/1,2,3#如果这一步过了,再继续试字段数、回显点、查表名。整个流程就是一个"探测-构造-再探测"的循环。不要指望一道题能一步到位,更不要因为一个payload失败就立刻怀疑自己没找到注入点。先确认过滤的是什么,再用排列组合的思路去构造,你会发现大多数过滤问题都能解。
3. Pwn方向复现:七道题里至少有四道是同样的栈利用思路
Pwn方向是我以前最怕的,因为一上来要面对的不是"页面回显",而是汇编指令、内存布局、保护机制。但这次赛后复现让我心态好了不少:实际上很多Pwn题目考来考去就是栈溢出、格式化字符串、堆利用几种核心套路。像ctfshow题库里的pwn07、pwn074这类题目,本质上也逃不出这些范畴。
3.1 复现前的环境准备
Pwn复现绝不能直接在比赛环境下瞎试,需要一套可重复搭建的工具链。我目前习惯用的环境是:
- Ubuntu 20.04虚拟机,为了兼容老题目也可以再装一个Ubuntu 18.04
- Python3 + pwntools
- gdb + pwndbg插件
- checksec(查看二进制保护机制)
- one_gadget、ROPgadget、LibcSearcher
装上之后先跑一遍checksec,它能告诉我这个二进制开了哪些保护,比如栈执行保护NX、地址随机化PIE、栈保护canary等。这一步非常关键,因为保护机制直接决定了利用思路:开了NX就不能直接执行栈上shellcode,开了PIE就需要先泄露程序基址,开了canary就得先绕过canary检查。
3.2 从checksec到泄露地址:栈题的不变套路
典型的栈溢出题目,我一般按这样一个固定流程复现:
- 用
file命令确认二进制架构和位数。 - 用
checksec查看保护机制。 - 用IDA或Ghidra打开二进制,定位存在危险函数的地方,比如
gets()、strcpy()、read()这些不检查输入长度的函数。 - 用
cyclic 200生成溢出序列,在gdb里运行程序输入序列,崩溃后cyclic -l确认偏移。 - 根据保护机制设计payload。
比如最常见的ret2libc思路,如果程序没有开PIE、但开了NX,那我就先通过puts或write这类函数泄露一个真实地址,再用LibcSearcher匹配出libc版本,最后计算system函数和/bin/sh字符串的地址,构造ROP链拿到shell。exp骨架类似:
from pwn import * p = process('./pwn') elf = ELF('./pwn') libc = LibcSearcher('puts', puts_addr) payload = b'A' * offset payload += p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(main_addr) p.sendline(payload) ...赛前我总觉得ROP很难,但实际上只要把偏移算对、把gadget地址找对,整个过程机械性比较强。赛后反复练三五道类似题目后,看到栈题第一反应就不再是"怎么开始",而是直接进入"确认保护-算偏移-找gadget"的流程。
3.3 格式化字符串类题目:为什么pwn07会让萌新崩溃
ctfshow pwn07这类的格式化字符串题,是很多Pwn新手的第一道心理阴影。明明代码看起来很简单,就是printf(user_input),但程序里既没有明显的栈溢出,又不知道怎么控制流程。
格式化字符串的核心是:printf的格式化参数是从栈上取值的,而你的输入往往也在栈上。通过构造%p、%s、%n,你可以实现两个能力:读任意地址的内容、写任意地址的内容。
复现时我一般先在本地试出"输入内容在格式化参数中的位置"。比如输入:
AAAA%p.%p.%p.%p.%p.%p观察输出中哪一段出现了0x41414141(也就是AAAA的十六进制),就能确定偏移量。假如它出现在第6个参数位置,那么AAAA%6$s就可以把某个地址作为字符串读出。
如果要利用%n写数据,还需要理解%n会把已经输出的字符数量写到指定地址。比如构造一个payload,先让前面输出的字符串长度等于目标值,再通过%n把该值写入某个GOT地址,从而改变程序流程。这种利用用手工构造非常痛苦,pwntools提供了fmtstr_payload函数自动生成payload:
payload = fmtstr_payload(offset, {target_addr: target_value})但自动化工具能用好,不代表可以不懂原理。赛后复现时,我会先手工构造一个最简单的写单个字节的payload,再对比fmtstr_payload生成的结果,这样才能在工具失灵时及时发现问题。
4. Misc方向复盘:流量包和文件分离才是送分题的重灾区
很多人觉得Misc就是"靠运气",我觉得可能是因为平时刷"ctfshow misc入门"这类题刷得不够系统。Misc看似杂,其实也有高频套路。这次三场比赛的Misc题,我总结下来大多数都没离开两个大类:一个是文件与隐写,一个是流量包分析。
4.1 第一步永远是binwalk、foremost、strings三连
遇到Misc题,我给自己定的规矩是:不管题目描述说什么,先做三件事。
第一件,用file查看文件真实类型。很多题目会给你一个看似是图片的文件,实际上里面塞了一个压缩包,或者扩展名被改掉了。file能直接告诉你文件的真实格式。
第二件,用binwalk扫一遍文件,看看能否检测到内部嵌入的其他文件。比如一张PNG图片里可能藏了一个ZIP压缩包,binwalk会列出偏移位置。接着用foremost或者dd根据偏移把隐藏文件提取出来。
第三件,用strings查找文件中的可见字符串。有时候flag本身就藏在某个角落,根本不需要复杂隐写分析。常用命令:
strings flag.png | grep -i "ctf\|flag\|key"如果这一步没结果,再考虑LSB隐写、EXIF信息、文件尾附加数据等更复杂的隐写手段。复现时我习惯把"文件分离"和"特征搜索"这两步放在最前面,因为它们的成本很低、收益很高,很多人比赛时连这两步都没做就去做像素级隐写分析,纯属浪费时间。
4.2 流量包分析:从HTTP流回到TCP流
流量包题是Misc的重点,也是比赛现场最容易让人手忙脚乱的题目。拿到一个.pcap文件,我会先打开Wireshark,快速浏览协议分级统计,看看这个包主要是HTTP流量、DNS流量还是TCP裸流量。
如果是HTTP流量,优先查看是否有文件传输。在Wireshark里可以通过"文件 -> 导出对象 -> HTTP"直接导出HTTP响应中的文件,这种方式比手动去翻流快得多。如果是DNS流量,就要关注是否有DNS隧道或者比较可疑的解析记录,比如大量看起来像base64编码的子域名。
进阶一点的题目可能把flag藏在TCP流里,需要你跟踪TCP流去还原完整数据传输。有时候数据是分多个包传输的,只看某一个包会漏掉上下文。用tshark命令行也能快速做统计和过滤,比如:
tshark -r traffic.pcap -Y "http.request" -T fields -e http.host -e http.request.uri这条命令可以把所有HTTP请求的域名和URI打印出来,快速定位可疑请求。
4.3 编码迷宫:从base64到培根密码,不要急着解
Misc题目里还有一种很常见的干扰项:一串看起来完全不像flag的字符串。它可能是base64、base32、十六进制、URL编码,也可能是一串"培根密码"风格的二进制组合。复现时不要一上来就随便选一个编码去解,而是先观察特征:
- 如果字符串只包含
A-Za-z0-9+/,且尾部可能有=,优先考虑base64。 - 如果只包含
A-Za-z2-7,优先考虑base32。 - 如果只包含
0-9a-f,优先考虑十六进制。 - 如果全是大小写A/B的序列,比如
BAABB AABBB,考虑培根密码。 - 如果能看到
%XX,优先URL解码。
解码工具我推荐CyberChef,它可以把多个解码步骤串联起来,比如先base64解码再zlib解压,不用来回切工具。复现时,把每一步解码结果都记录下来,因为有些题目在中间步骤里还嵌着另一层隐写。
5. 我的赛后复现工具链与时间安排
复盘了具体方向之后,说说我自己的整体复现流程。以前我复盘是"随缘式"的,今天觉得这道题有意思就看看,明天没时间就扔一边,结果两周后什么也没沉淀下来。这次我改成了一套固定流程,效果比以前好很多。
5.1 本地靶场用Docker还是虚拟机
Web题复现,我优先用Docker。很多赛题官方会提供Dockerfile或者部署环境,直接在本地把容器拉起来,端口映射到本机,就能无限次测payload,不怕把环境打坏。即使官方没提供,也可以用phpstudy、LNMP这类一键环境快速搭建。Web题要反复改代码看效果,Docker的轻量特性比虚拟机舒服很多。
但Pwn题就不一样了,它依赖特定的libc版本和系统环境。我习惯在虚拟机里装一个Ubuntu 18.04和20.04双环境,比赛题目要求哪个版本就切到哪个版本,避免本地libc和远程不一致导致exp在本地能通、远程不通的尴尬情况。
复现时我会把目标服务的地址直接指向本地容器或虚拟机IP,这样既不影响正常上网,又能随时抓包调试。
5.2 Payload的归档方式:按过滤条件建目录
我以前存payload的方式非常随意——散落在聊天记录、浏览器历史、临时文件里,等到要用时根本找不到。后来我改成在本地建一个目录,按过滤条件或者漏洞类型归档。比如:
payloads/ ├── sqli/ │ ├── space_filter.txt │ ├── keyword_filter.txt │ └── hex_encoding.txt ├── rce/ │ ├── preg_match_bypass.txt │ └── length_limit.txt └── upload/ ├── ext_bypass.txt └── content_type_bypass.txt每个文件里不仅记录payload,还要写上"适用条件"和"验证结果"。这样下次比赛遇到相似过滤规则,直接去翻对应文件,比现场空想快得多。
5.3 复现时写题解:顺手把exp改成可复用模板
复现过程中,我强烈建议把写好的exp顺手改成可复用模板。比如一个SQL注入脚本,不要只在某个靶场上能用,而是把它抽成函数:传入URL、参数名、注入类型,返回结果。再比如Pwn的利用脚本,我会把"连接目标"和"本地调试"写成一个变量切换,这样赛后复现和下次比赛都能直接用。
写题解也不是为了发博客,而是为了让自己理解更清楚。我一般会用Markdown记录题目背景、漏洞点、绕过方式、完整exp和最终flag,再把自己当时卡住的地方单独标出来。过几个月回头再看,这份记录比重新做一遍题有价值得多。
5.4 给萌新的一条复现顺序建议
最后聊一下复现顺序。我自己的建议是:先Web、再Misc、最后Pwn。
理由是Web环境最容易被复现,遇到不会的题很快就能动手验证;Misc需要一些文件分析工具,但不需要太多环境搭建;Pwn则对环境和工具链要求最高,前期容易卡在"exp跑不起来"这个坎上。
时间上,我的习惯是每道题最多给自己两个小时。第一个小时不看任何writeup,完全按照比赛时的思路继续尝试;第二个小时可以查阅官方题解或者别人的writeup,搞清楚思路后,再回到题目里自己重新走一遍。每次都直接看答案的复现没有意义,因为你需要锻炼的是"如何自己想出来"的能力。
如果你是刚入门、还在刷ctfshow萌新题的朋友,完全可以按照这个套路来:做完一道题,哪怕做出来了,也要在赛后把相关考点扩展练一遍。比如今天做了一道SQL注入题,就回题库把sql注入分类下的其他题目都过一遍;今天遇到一个HTTP头注入的坑,就把Burp的Repeater用熟、把常见Header都测一遍。这种"以点带面"的复现方式,比单纯追求题数要扎实得多。
赛后复现是一件短期看不到收益、但长期非常划得来的事。比赛的名次是一时的,从比赛里沉淀下来的工具、笔记、节奏感,才是能一直带走的东西。