凌晨两点的房间里,我盯着终端上滚动的报错信息,整个人处于一种既兴奋又抓狂的状态。SUSCTF 2018线上赛已经进行了二十个小时,作为一支临时凑起来的三人小队,我们手头还有四道题没解出来,而最让我在意的,是那道报错信息看起来特别奇怪的Web题。SUSCTF是西南科技大学主办的大学生CTF夺旗赛,2018年这一届采用线上初赛加线下决赛的模式,线上赛持续24小时,题型覆盖Web、Misc、Crypto、Reverse和Pwn五个方向。对当时刚入坑CTF不到半年的我来说,这场比赛既是一次综合检验,也是一次难得的选题练习。如果你想了解大学生CTF赛事是怎么玩的、新手应该如何分配精力,这篇文章应该能给你一些参考。
1. 赛前摸底:赛制拆解与破题顺序
1.1 线上赛24小时,题量和题型到底怎么分布
SUSCTF 2018的线上赛持续24小时,题目总数大概在25道左右,分布在Web、Misc、Crypto、Reverse、Pwn五个方向。我记得很清楚,比赛前我们队三个人拉了群,群公告第一条就是我写的“先看题,别急着做”。
为什么先看题这么重要?因为CTF线上赛的出题风格通常是“签到题+中等题+难题”三段式。签到题可能一上来就是一道Base64变形或者一个简单注入,五分钟就能做出来;难题则可能要花好几个小时,往往是多个知识点的组合。如果不先通读所有题目,很容易在某一类题上死磕一下午,最后回头发现签到题都没做。
还有一个现实因素:线上赛的靶机环境是不稳定的。有些题目会因为大量选手同时访问而卡顿,甚至服务直接挂掉。先大概浏览一遍题目列表,可以让你对服务器状态有个心理预期,不至于做到一半发现环境崩了,干着急。
1.2 我给自己定的破题顺序:先易后难,先快后慢
我当时的策略很简单,分了三轮走。第一轮把每个方向的题目标题、附件类型、描述信息全部过一遍,凡是看到flag格式明确、附件能直接下载、描述里有明显提示的,都归为“快题”。第二轮开始做Crypto和Misc,因为这两类题目往往不需要环境交互,纯本地分析,不依赖服务器的稳定性,半夜网络波动也不影响。
Web题放在第三轮,因为Web题通常需要现扫目录、现测注入,服务端可能还会临时修漏洞或者重启环境,干扰因素最多。Reverse和Pwn那一届我基本是放弃状态,只挑了一两道最简单的逆向尝试,把时间留给有把握的方向。这种方式也许拿不到前三名,但对于目标只是“把能拿的分都拿到”的队伍来说,性价比非常高。
1.3 容易被忽略的计分规则与flag格式
这里有个细节很多人会忽略:比赛平台的计分规则是动态的,题目分值会随着解出人数增加而下降,但每种题型的基础分不同。也就是说,先做签到题还能拿到相对高的分,拖到后面再做,分值可能被稀释得很厉害。所以“先易后难”不只是心理上的轻松,而是实实在在的分数差异。
另一个容易被忽略的是flag格式。SUSCTF 2018的flag统一是flag{...},但有些附件的文件名或者题目描述里会隐晦提示“flag格式是SUSCTF{...}”,如果没留意,拼写错了就很难受。我的习惯是开局就把所有题目的flag格式抄到记事本里,做一题填一题,最后统一提交。这种小习惯看似不起眼,但能避免很多低级的丢分。
2. Web题实战:报错注入、命令执行与过滤绕过
2.1 小小查询系统的报错注入
Web方向有一道题给我留下的印象最深,名字就叫“小小查询系统”。页面只有一个输入框,提示输入id即可查询到信息。输入1、2、3都能正常返回用户名和内容,看起来是一个非常普通的前端查询。
当我输入1'的时候,页面报错弹出来一行SQL语法错误,而且错误信息里直接把MySQL版本号都带出来了。那一刻我意识到,后台大概率是拼接SQL字符串直接查询,数据库的报错信息也原样返回到前端。这种“看起来好奇怪的报错”,恰恰是出题人故意留下的路标。
判断是字符型注入之后,我用1' and '1'='1和1' and '1'='2做了对比,前者正常返回,后者查询结果为空。再到1' order by 3看能不能正常排序,通过逐步试探确定当前查询的列数。确定列数后,最常见的思路是用union select直接拼接查询,但当时这道题目把union过滤了,所以我转去用了报错注入。
MySQL的报错注入常用函数有extractvalue和updatexml,原理都是让函数在计算时触发XPATH语法错误,并把我们拼接的查询结果带进错误信息里。最典型的payload是:
1' and updatexml(1,concat(0x7e,(select database()),0x7e),1) --其中0x7e是波浪号~的十六进制,用于把查询结果和错误提示隔开,方便阅读。用这个套路,我先后拿到了数据库名、当前库下的表名、目标表里的字段名,最终在某个字段里读出了flag。
这里有个小坑要提醒一下:报错信息有长度限制,通常一次只能显示32个字符左右,所以如果flag很长,需要用substr分段截取。我当时就是分了三段才把完整的flag拼出来。很多人第一次用报错注入,看到flag只有一半就以为题目有问题,其实只是长度限制。
2.2 命令注入:当空格和关键字都被过滤
第二道Web题是个ping功能。页面让你输入一个IP,后台会执行ping -c 4 $input并把结果打印出来。正常的127.0.0.1会返回ping的统计信息,但如果我在后面加上; ls,就会看到老实的报错提示Hacker?。
这说明题目对命令做了黑名单过滤,或者说某些字符被替换了。我第一个想到的是空格可能被过滤,于是尝试127.0.0.1|ls${IFS}/,居然真的列出了根目录。${IFS}在bash里是内部字段分隔符,默认包含空格、tab、换行,所以可以用它替换命令里的空格,绕过只针对空格的过滤。
根目录里躺着一个/flag文件,接下来就是想办法读取它。cat /flag明显会被过滤,因为cat在关键词列表里几乎铁定存在。我试了tac、more、less、head、tail,发现tail没被过滤,于是用127.0.0.1|tail${IFS}/flag直接把flag的最后几行打了出来。这题到这儿就结束了。
事后看官方writeup,这题其实想让大家熟悉PHP里的命令执行函数,包括system、exec、shell_exec、passthru等,出题的触发点就埋在“输入IP执行ping”这个场景里。很多新手卡住,不是因为不知道命令注入,而是没想到空格过滤之后还有${IFS}这种替代方案。命令注入题的核心始终是:先探测过滤规则,再针对规则构造绕过链。
2.3 从报错内容到路径泄露的小技巧
Web方向还有一个小细节让我长了记性。有一道题的报错信息里暴露了网站的物理路径,比如/var/www/html/query.php。很多人看到报错只想着“哦这是注入”,但忽略了路径本身就是重要信息。
如果题目后续允许文件读取或者日志包含,这条路径能直接帮你少走半天弯路。我当时做的另一道文件包含题,就是因为先知道了物理路径,再用伪协议php://filter/convert.base64-encode/resource=query.php读出了源码,结果发现源码里还藏着一个预留的后门文件。Web题里信息收集永远是第一优先级,任何一条看起来无用的报错信息,都可能是在提示下一步该看哪里。
3. Misc杂项:二维码修复、隐写提取与流量分析
3.1 一张缺了角的二维码
Misc方向第一题给了一张二维码图片,但二维码的三个定位角里缺了两个,用手机和常见的扫码工具都扫不出来。这种题型在CTF里很常见,考察的就是选手对二维码结构的基本理解。
二维码之所以能快速定位,是因为左上、右上、左下三个角的“回字形”定位图案。扫描器先识别出这三个角,才能确定二维码的方向和尺寸。缺了任何一个角,扫码基本上都会失败。修复的思路很简单:找一个完整二维码图片,把缺的定位角区域用图像处理工具裁下来,贴回原图,再重新保存。为了保证识别成功率,最好严格控制粘贴位置的比例和大小。
我当时用的是Python的OpenCV库,先读取图片,再用cv2.rectangle把三个定位角画上去。对,直接用画的方式替代复制粘贴,因为定位角就是固定的黑白方块图案,知道坐标就能画。画完再保存,用微信扫一下,出来了一段base64字符串。后续解base64又是好几层编码嵌套,最后才看到flag。这道题的重点在于:杂项题里很多线索不是“藏得深”,而是“长得眼熟”。看到二维码先别急着解,先看看它结构是不是完整的。
3.2 LSB隐写与zsteg的妙用
另一道Misc题是一张看起来完全正常的风景图,文件体积也不大,直接打开肉眼完全看不出异常。这种题十有八九是LSB隐写。
LSB隐写的原理可以这样理解:图片每个像素的颜色由RGB三个通道组成,每个通道是0到255的一个数。把通道值转成二进制后,最低的那一位(Least Significant Bit)对最终颜色影响极小,人眼几乎察觉不到颜色变化。隐写工具就利用这1比特,把秘密信息按二进制位嵌进去。如果信息是纯文本,一个字符8位,一张尺寸稍大点的图能藏好几百个字符。
我当时先跑了一遍binwalk,没发现附加文件;再用Stegsolve逐通道翻看RGB的0号平面,隐约能看到图案上有不自然的条纹。按理说手工提取也能解,但更快的办法是直接用zsteg:
zsteg -a image.png它会自动遍历所有通道和所有位平面,把隐藏在LSB里的信息直接打印出来。我记得输出里立刻出现了一行像是密码的字符串,推测是后续压缩包的解压密码。后来解压出来的文本文件里放着flag,正好是这张图片的文件名加上一串随机字符。Misc题里的“工具流”思路特别重要:先看binwalk,再翻Stegsolve,不行就上zsteg,一套组合拳打下来,大部分图片隐写都跑不掉。
3.3 流量包里的压缩包:WireShark提取之旅
Misc还有一道题给了个.pcapng文件,题目描述只有一句“抓到了奇怪的网络流量”。这种流量分析题在CTF里也是常客,主要考察选手对WireShark或tshark的熟悉程度。
打开文件后,我习惯先看Statistics -> Protocol Hierarchy,也就是协议层级统计。这一步能快速判断流量里主要是什么协议,是HTTP还是USB,是DNS还是SMTP。那次统计结果显示HTTP请求数量特别多,于是我去File -> Export Objects -> HTTP,把所有HTTP传输对象导出来,发现里面藏着一个压缩包。
用file命令一看,确实是zip压缩包,但解压需要密码。密码哪里来?回到WireShark里翻HTTP的请求参数,发现某个POST请求里的请求体是一串base64,解码之后就是解压密码。整个过程其实不复杂,真正容易卡住的地方是:很多人拿到pcapng就直接盯着一个个报文看,而没有先做协议统计和对象导出。流量分析的第一原则永远是“先把大结构看清楚,再去找细节”。
如果换成USB流量,思路也类似:先用tshark把USB HID数据提取出来,再写个Python脚本把键盘按键的HID码对照表还原成字符。我后来在别的比赛里就用过这个思路解出过键盘输入类的题目,那次SUSCTF虽然没有USB流量题,但工具和脚本都提前备好了。
4. Crypto密码学:编码识别与古典密码爆破
4.1 一串乱码背后的多层编码
Crypto方向第一题是很经典的多层编码。题目给了一段满满当当的字符串,有大小写字母、数字和一些加号、斜杠,末尾还带着等号。看到这个形态,第一反应就是base64。
但base64解码之后,出来的还是一堆看起来有规律但读不懂的字符,里面甚至出现了0x开头的片段。这时候要意识到:解码不是终点,而是下一层编码的起点。我当时的习惯是用CyberChef的Magic功能,让它自动尝试各种编码组合,或者用随波逐流的编码工具直接识别文本特征。CyberChef的Magic会从输入端自动推测可能用到的编码组合,一键解出结果,对新手非常友好。
那次最终的解码链是base64 -> 十六进制 -> 栅栏密码 -> 得到flag。整个过程没有复杂的数学知识,考验的只是对常见编码的敏感度和耐心。很多人在第一步base64解出来还是乱码之后就放弃了,其实多观察一下解码结果的字符集,比如是不是全是十六进制字符,是不是长度是偶数,就能判断下一步该解什么。编码题的通用解题法就是这样:不断解码,每次解码后都重新判断文本形态,直到看见flag格式为止。
4.2 古典密码:从频率分析到维吉尼亚
第二道Crypto题给了一篇看起来像英文的密文,但所有单词都拼不对,题目提示只写了“key”两个字母。这种带key的英文密文,最经典的就是维吉尼亚密码。
维吉尼亚密码本质上是对凯撒密码的扩展:它不是把所有字母固定偏移一位,而是根据一个密钥循环决定每个字母的偏移量。破解的关键在于先确定密钥长度。常用的方法是Kasiski检验:在密文里找到重复出现的字符串,计算它们位置之间的间距,然后求这些间距的最大公约数,往往就接近密钥长度。
这道题的密文不长,我用Python写了个小脚本统计重复子串,算出密钥长度大概是5,然后再用重合指数法确定具体密钥内容,最后把密钥代入维吉尼亚解密就得到了明文。如果你懒得手写,CyberChef的Vigenère Decode功能也支持自动爆破密钥长度,但前提是你理解背后的原理,不然调参的时候会一头雾水。古典密码的难点从来不是解密算法的实现,而是如何确定密码类型和密钥参数。
4.3 一道基础的RSA题目
最后一道Crypto是一道基础RSA题,给了n、e、c三个值。看到这种结构,本质上就是靠分解n来求私钥。
这道题里的n并不大,我用Python的gmpy2库几步就搞定了:
import gmpy2 from Crypto.Util.number import long_to_bytes # 通过工具分解n得到p、q p = ... q = ... n = p * q phi = (p - 1) * (q - 1) e = 65537 d = gmpy2.invert(e, phi) m = pow(c, d, n) print(long_to_bytes(m))如果n再大一些,可以用yafu或在线工具分解。RSA基础题的核心逻辑就一句话:拿到了p和q,题目就结束了。很多新手看到RSA就害怕,其实在CTF里,给n、e、c的基础题往往是最简单的送分题,只要熟悉Python的密码库,十分钟以内能拿下。
5. 做题之外:工具链、复盘与个人体会
5.1 我常用的CTF工具清单
经历了整个SUSCTF 2018之后,我发现一个规律:真正拉开差距的往往不是脑力,而是工具档案的完整度。这里把我当时常用的工具按方向整理了一下:
| 方向 | 工具 | 用途 |
|---|---|---|
| Web | Burp Suite | 抓包、改包、重放 |
| Web | sqlmap | 注入检测辅助 |
| Web | 蚁剑 | 连接Webshell |
| Misc | binwalk、foremost | 文件分离与隐藏内容提取 |
| Misc | Stegsolve、zsteg | LSB隐写、通道查看 |
| Misc | WireShark、tshark | 流量分析与数据提取 |
| Misc | 随波逐流CTF编码工具 | 编码自动识别与解码 |
| Crypto | CyberChef | 编码转换、密码爆破 |
| Crypto | Python的gmpy2/pyCryptodome | RSA、离散对数等数学计算 |
| Reverse | IDA、Ghidra | 静态分析 |
| Pwn | pwntools | 漏洞利用脚本编写 |
工具不在多,在于知道每个工具“在什么时候用”。比如binwalk在Misc题里几乎可以首先无脑跑一遍,因为很多题目会把一个文件藏进另一个文件里;但如果一个图片本身没有附加数据,跑完binwalk什么都没有,也不要浪费时间反复跑,直接换思路看隐写。工具是死的,思路是活的。
5.2 赛后复盘:把writeup变成自己的武器库
比赛结束后还有一个关键动作:复盘。那届SUSCTF的官方writeup和选手自己写的题解,比赛结束后的几天内会在各大安全社区陆续放出来。我会把自己做出来的和没做出来的题都重新做一遍,每道题整理出一个独立的Markdown文件,内容包括题目描述、我的原始思路、卡住的点、最终解法、涉及到的知识点。
整理完之后,这些文件就是自己的“武器库”。以后遇到类似题目,先翻武器库而不是先翻搜索引擎。我后来在实战中发现,90%的新题都是旧知识点的组合:命令过滤和${IFS}是一套,base64和十六进制是一套,二维码修复和图片隐写又是一套。把套路记下来,比临时临场发挥靠谱得多。
复盘还有一个容易被忽略的好处:你能看到出题人的思路。比如那道“小小查询系统”,为什么报错信息会原样回显?因为出题人故意没有关闭错误显示,就是为了引导选手往里走。一旦站在出题人的角度想问题,很多原本觉得“藏得太深”的线索,都会变得合理起来。
5.3 给新手的三条实在建议
如果你准备参加SUSCTF或者任何一场大学生CTF赛事,我有三条实在建议。
第一,组队时要互补。三个人最理想的配置是Web一人、Misc/Crypto一人、Reverse/Pwn一人,哪怕后面两个方向只能做简单题,也能保证不浪费题目。CTF不是个人英雄主义的游戏,分工明确才能最大化拿分效率。
第二,赛前把基础工具装好并至少各用过一次。比赛时没有时间现场学工具,尤其是WireShark的基本过滤语法、Burp Suite的抓包流程、Python的密码库调用,这些都要提前练到条件反射的程度。很多队伍在比赛前半小时还在装环境,心态就已经输了一半。
第三,善用官方writeup,但一定要自己动手敲一遍解题脚本。只看题解觉得“我会了”和真正跑通拿flag之间,差的是一次一模一样的实战。尤其是密码学和Misc,脚本能不能跑通决定了你是不是真的掌握了。
最后再分享一个小习惯。每次比赛结束,我会把自己解出来的flag和题目对应的解题步骤整理成一份文档,按比赛名称归档。SUSCTF 2018的题我后来重新完整做了一遍,发现当初两个多小时才解出来的命令注入,其实就是过滤绕过的一个变体。CTF的成长速度,往往就藏在这些反复拆解和整理里。如果下一届SUSCTF我还有机会参加,大概会比第一次线上赛从容很多。