刷 CTF 的 Web 方向也有一年多了,从只会照着网上的 Payload 复制粘贴,到能自己审代码、找绕过点、写简洁的利用脚本,中间踩过不少坑。恰好在某平台上把 Web 方向的题目从易到难过了一遍,整理了一份自己的 WriteUp(也就是俗称的 WP),今天把核心思路和踩坑记录拎出来聊一聊。这份内容不是粘贴题目答案那种流水账,而是想告诉你每一类题目拿到手之后,第一步该干什么、哪里容易卡住、不同思路之间怎么切换,以及哪些工具要练成肌肉记忆。
这份东西适合两类人:一类是刚入门 CTF 想系统刷 Web 题的新人,另一类是自己刷过一些题但总觉得思路发散、遇到新题型就断片的选手。我尽量把话说得直白,Payload 给全,原理讲透,坑点标清楚。不信你看完再去刷题,会明显感觉到看题的第一眼反应不一样了。
1. 为什么我坚持整理 Web 方向的 WP
1.1 刷题路径的价值
很多新手喜欢每天换着花样刷新题,刷完就忘,三个月后遇到同类题还是懵。我自己的体会是,CTF 刷题的核心不是“刷了多少”,而是“沉淀了多少”。一份好的 WP 不只是把答案写出来,而是把漏洞点、绕过思路、工具用法、报错特征全部记录在案。这个习惯积累半年之后,会形成你自己的“解题反应链”。
BUUCTF 这类平台最大的好处是题目有梯度,从签到级别的信息泄露,到需要审计完整代码的复杂反序列化,都有覆盖。你可以在同一个平台上连续刷几十道题,感受难度递进,这对建立知识体系特别重要。我自己就是先把 Web 板块的题目按难度从低到高刷了一遍,每道题都留了笔记,最后再回看这些笔记,才真正理解了不同漏洞之间的关联性。
1.2 这份 WP 适合谁
如果你是刚接触 CTF 的新人,这份 WP 能帮你建立一个基础的解题框架,知道每类题目的突破口在哪。如果你已经有了一些经验,那这份 WP 里的绕过思路和排错过程会更适合你,因为翻车现场往往比完美解题更有信息量。
打个比方:做题就像开锁,知道锁芯结构的人可以拿一把万能钥匙直接开,但更多人需要的是了解锁芯内部结构,然后根据不同的锁选择不同的工具和方法。WP 的价值就在于把这些“锁芯结构”记录下来,而不是只给你看那把打开的钥匙。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 信息收集:被忽略的第一道防线
2.1 源码备份与敏感文件泄露
Web 方向很多题目,第一步根本不需要什么高级技巧,把信息收集做扎实就够了。我刷题时发现,初学者最大的毛病就是拿到 URL 后直接开扫开打,结果扫描器跑了一圈什么也没发现,反而在浏览器里手动打开几个常见路径就找到了突破口。
常见的敏感路径就那么几个:.git 源码泄露、robots.txt、www.zip 或者 .bak 备份文件、index.php~ 这类编辑器临时文件。其中最经典的就是 .git 泄露——如果目标 Web 目录做过 Git 初始化且通过 Web 可以访问 .git 目录,那整个源码几乎等于裸奔。
我遇到过一道题,页面就是一个简单的登录框,扫目录也没结果。偶然在路径末尾看到 index.php.bak,下载回来打开,发现备份文件里把数据库连接信息、甚至后台管理员密码的哈希值都留下了。这就是典型的备份文件泄露,简单粗暴,但很多人就是想不到去试 index.php.bak 这种后缀。
注意:信息收集不是扫描器跑一遍就完事,建议手动把常见路径都点一遍:
/.git/HEAD、/robots.txt、/www.zip、/.DS_Store、/config.php.bak、/index.php~。
2.2 响应头与 HTML 注释的信息价值
除了路径探测,响应头里的蛛丝马迹也值得留意。有些题目会把 flag 的一部分放在自定义响应头里,比如 X-Admin: true 这种;有时候响应头里的服务器版本信息也会暴露中间件的具体型号,为后面的解析漏洞利用埋下伏笔。
另外,HTML 注释经常是新手最容易忽略的地方。很多开发者在写前端页面时习惯把调试信息、接口地址、甚至测试账号写在注释里,上了生产环境却没有清理。我在一道模拟“后台管理”的题目里,就是从页面源码的注释中找到了一个隐藏的接口路径,顺着这个路径拿到了后台登录入口。
经验是:拿到一个 Web 目标后,先手动看源码、看请求头、看 cookie,再上扫描器。很多题目把这些信息就摆在明面上,只是你扫得太快,根本没来得及看页面本身。
3. SQL 注入:从识别到三连击
3.1 注入点的判断方法
SQL 注入算是 Web 题里出现频率最高的题型之一。它的核心说白了就是:用户输入的数据被拼接进了 SQL 查询语句,且没有经过有效的过滤或转义,导致输入的“额外语法”被执行。
我判断注入点通常三步走。第一步,在正常的参数后面加一个单引号 ',看页面是否报错或者返回内容是否有变化;第二步,加上 -- - 或者 # 注释符,让原始语句后面的内容被注释掉,看页面是否恢复正常;第三步,尝试 and 1=1 与 and 1=2 两个条件,看返回内容是否有区别。
以经典的 id 参数为例,正常请求是:
http复制GET /index.php?id=1
先测单引号:
http复制GET /index.php?id=1'
如果页面报错,说明很可能存在注入。再测注释符:
http复制GET /index.php?id=1' -- -
页面恢复,那基本就确定是数字型或字符型的闭合注入。接下来就看当前语句的具体形态,用 order by 去探测列数。
3.2 三种注入方式的拆解
联合注入是思路最直观、效率也最高的一种,前提是页面会把查询结果展示出来。核心步骤就是先用 order by 探测字段数量,然后用 union select 去匹配回显位置。
比如探测出共有 3 列,那就可以用:
sql复制id=-1' union select 1,2,database() -- -
先把前两个位置置为占位数字,观察页面上数字 2 出现在哪里,再把那个位置的 2 换成自己想要的数据,比如数据库名、表名、字段名。整个过程就像在表格里找对齐的列,找到能回显的列后,剩下的就是从 information_schema 里把库名、表名、字段名一层层扒出来。
报错注入则用于页面没有明显回显,但会把数据库错误信息直接打到前端的情况。我最常用的是 updatexml() 和 extractvalue() 这两个函数,利用 XPath 报错把子查询的结果带到错误信息中:
sql复制id=1' and updatexml(1,concat(0x7e,version(),0x7e),1) -- -
这里 0x7e 是波浪号 ~ 的十六进制,通常用于在报错信息中标界,方便肉眼定位。
盲注则是最麻烦的场景,页面既不回显数据,也不报错,只有“真/假”两种状态。原理很简单:通过构造条件判断语句,比如 if(ascii(substr(database(),1,1))>100, 1, 0),根据页面是否正常显示来逐字符猜测数据。手工执行盲注很容易崩溃,建议直接写脚本或者用现成工具跑,我后面会专门说工具配合的问题。
实用心得:很多题的表名字段名都会做混合大小写处理,直接
information_schema查出来的字段名可能是错的,需要对大小写敏感的参数做调整。
4. 命令执行与文件包含:入口与链路的接力
4.1 命令执行函数的分类与绕过思路
命令执行(RCE)类题目的本质是用户可控的变量被带入了系统命令中。常见函数有 PHP 的 system()、exec()、shell_exec()、passthru(),还有一些代码执行类的 eval()、assert()。
我在刷题时发现,这类题目的关键在于“输入如何拼接到命令中”。比如:
php复制system("ping " . $ip);
这里 $ip 是用户可控的,那我就可以传入:
code复制127.0.0.1; whoami
分号让前面的 ping 命令执行完后继续执行 whoami。若题目过滤了分号,还可以用 &&、|、|| 或者换行符 %0a 来做命令分隔符。
有些题目会过滤关键字,比如直接不允许出现 cat、flag 这类词。常见的绕过思路有:用变量拼接(a=fl;ag)、用通配符(cat *lag)、用反斜杠(c\at)、在中间插入空注释(ca""t)。这些方法看起来花哨,本质都是利用 shell 的词法解析规则来规避字符串匹配。
4.2 文件包含的入口识别与两种利用方式
文件包含漏洞在 PHP 里经常见到,核心函数是 include()、require() 等。当传入的文件路径可控时,就能让服务端加载任意文件。
这类题常见的入口是参数名,比如 ?page=home、?file=upload。识别方法很简单:把参数换成 ../../../../etc/passwd 或者 /etc/passwd,如果页面回显了文件内容,那就说明存在文件读取漏洞。配合伪协议还可以做更多事,比如 php://filter/read=convert.base64-encode/resource=index.php,先把源码读出来,拿到源码后再分析下一步利用点。
文件包含另一个高价值的利用方向是配合日志注入或图片马。比如 Web 访问日志里记录了 User-Agent,如果我没有拿 shell 的能力,可以在 UA 里写入 PHP 代码,再通过文件包含把日志文件当成 PHP 执行,从而拿到执行权限。这个思路在真实环境里也很有参考价值,因为日志文件通常路径可预测、内容可控。
5. 文件上传:从绕过检查到运维失误
5.1 常见校验点与绕过顺序
文件上传在 Web 题目里几乎是必考项,而且它的考点往往不是单一的“上传文件”,而是服务端在校验环节上出现了疏漏。常见的校验位置有四层:前端 JS 校验、Content-Type 校验、扩展名黑白名单校验、文件内容头校验。
前端 JS 校验最弱,直接在浏览器里禁用 JS,或者用抓包工具改包就能绕过去。Content-Type 校验也很容易绕,把请求头里的 Content-Type: application/octet-stream 改成 image/png 就行。扩展名校验是重点,也是难点,有的题只过滤 php / php3 / phtml,那就可以试试 php5、php7、phar 等后缀;有的题校验文件内容头,那就需要在文件开头插入图片文件头,比如 GIF89a,做成图片马。
5.2 解析漏洞与隐藏的运维配置
除了解校验本身,文件上传题还会考察解析漏洞。拿 Apache 和 Nginx 来说,常见的解析漏洞各有各的触发条件。Nginx 曾经有个经典解析漏洞:当请求的 URL 以 .php 结尾时,即使文件本身不是 php,也可能被交给 PHP 解析器执行。比如我上传一个 shell.jpg,然后访问 shell.jpg/.php,就可能被当作 PHP 脚本执行。
另一种思路是看目标服务是否配置了“可将特定扩展名文件作为脚本解析”的逻辑,比如某些 IIS 环境会把 .asp、.asa 当成可执行脚本。这些都属于运维配置层面的疏漏,在 CTF 题里经常作为一个附加考点出现。
做题时的推荐流程是:先看上传点附近有没有“只允许图片”的提示,再抓包看看请求结构,判断校验层在客户端还是服务端;然后按“改包绕过扩展名 -> 图片马+解析漏洞 -> 上传 .htaccess 改变解析规则”的顺序逐步尝试。
6. 反序列化与代码审计:魔术方法背后的调用链
6.1 反序列化漏洞的核心原理
PHP 反序列化是我个人觉得 Web 题目里综合性最强、也最能拉开差距的一块。它的原理不复杂:序列化是把对象变成字符串,反序列化是把字符串还原成对象。问题在于,反序列化时如果对象属性可控,且代码里存在某些魔术方法(__destruct()、__wakeup()、__toString()、__call() 等)会对属性值进行危险操作,那攻击者就能构造一个恶意对象,让它恢复后触发危险行为。
比如一个常见的场景:
php复制class User {
public $name;
public $role;
function __destruct() {
echo "User: " . $this->name;
}
}
如果我能控制传入的序列化字符串,把 $name 改成一段包含 PHP 代码的内容,或者把 $role 改成 admin,就能在对象销毁时影响程序逻辑。这只是一个简化模型,真实题目中往往会利用一连串魔术方法和普通方法拼接出完整的“利用链”,也就是所谓的 POP 链。
6.2 POP 链的拼接思路
做反序列化题目,第一步不是想 Payload,而是审代码。拿到源码后,先找出所有的魔术方法,再看它们调用了哪些普通方法,这些普通方法里有没有能执行代码、读写文件、调用数据库的函数。从危险函数反推参数来源,是拼 POP 链最常用的思路。
我刷题时经常用的步骤是这样的:
- 全局搜索危险函数,如
eval()、call_user_func()、system()、file_put_contents()等。 - 追踪这些函数的参数是否来自某个对象属性。
- 再看哪条调用链能从魔术方法自动触发,或者能通过多个可控参数联动触发。
- 最后构造序列化字符串,注意对象属性名、访问修饰符(
public/protected/private)对序列化格式的要求。
踩坑最多的地方也在这里:PHP 的序列化字符串对类名、属性名、属性值的格式有严格要求,一个空格或者分号错了都可能导致反序列化失败。而且不同 PHP 版本的魔术方法触发方式会有差异,所以做题前先确认题目环境的 PHP 版本还是很有必要的。
7. 刷题过程中的常见问题与排错实录
7.1 连不上靶机或链接失效的应对方案
我在刷题时最常遇到的问题不是不会做,而是环境和工具层面的各种小坑。比如端口突然不通了、题目链接返回 404、页面响应慢到怀疑人生。遇到这类情况,第一反应不是怀疑题目,而是确认网络环境和容器状态。我自己一般会先做四件事:换浏览器试一次、用命令行 curl 看一眼完整请求响应、检查代理配置、重启平台上的容器实例。很多时候链路本身没问题,只是本地代理缓存了旧页面。
7.2 Payload 没问题却执行不了的情况
我最头疼的情况是:Payload 在本地环境验证没问题,但放到题目里就是没反应。后来总结下来,原因基本集中在这么几类:
- 参数名拼错了,多一个字母少一个字母,或者把
id写成ID,大小写不同导致后端拿不到值。 - 过滤规则和预期不一样,比如过滤了空格,但我还在用空格分隔关键词。
- 单词被 URL 编码问题影响,后端拿到了被编码后的字符串,没有执行。
- 完整数据没有经过预期函数,比如代码先
trim()去掉了末尾空白,我却把 Payload 放在末尾。
排查方法就是每走一步都看响应差异:先确认参数生效,再确认拼接位置,再确认过滤规则,一层层缩小范围,不要一口吃个胖子。
7.3 工具与手工的配合经验
工具是放大效率用的,不是判断依据。我刷题时对工具的态度是:能用工具自动化的一定自动化,但每个步骤的原理必须明白。比如 SQL 注入,我会先用 sqlmap 判断注入类型,但确认后一定要手工构造一次完整的 union select 流程,确保自己清楚每一步在数据库里发生了什么。文件上传的改包则强烈建议手工练几次,用抓包工具自己改 Content-Type 和扩展名,比一味依赖现成插件更能加深理解。
我个人常用的工具组合是:浏览器开发者工具看请求、抓包工具改包重放、一个轻量级的目录扫描器做信息收集、以及一个支持多协议的环境(比如 Docker 本地搭 PHP 环境)用来本地验证代码行为。工具不用多,顺手就好,关键是能解释你正在做的每一步。
8. 最后说点实在的做题体会
写这份 WP,最想传达的一层意思是:CTF 的 Web 题目虽然花样多,但底层逻辑就那些——可控输入、危险函数、过滤绕过、链式调用。你把这四件事想明白,很多题目即使没见过,也能靠分析拆解出解法。我自己在刷题过程中最大的成长,就是从“背 Payload”转变成了“理解漏洞在代码里的位置”,这个过程很慢,但到了一定阶段会突然加速。
再分享一个最后一刻才发现的小技巧:很多题目故意把关键线索放在那些看似“无用”的响应里,比如 404 页面里的自定义字段、Cookie 里的注释信息,甚至是 .well-known 目录下的安全说明文件。每次做题卡住时,我习惯把原始请求和响应逐行过一遍,而不是反复尝试 Payload。实践证明,这种“慢下来看细节”的方式经常能柳暗花明。
总之,保持刷题节奏,持续沉淀自己的 WP,才是 Web 方向拉开差距的有效路径。下次遇到新题,你可以先不急着看已有题解,自己试着把分析过程写下来,哪怕写错了,也比直接抄答案有价值得多。
