前几天一个刚入门的朋友在CTFshow上刷“菜狗杯”的新手题卡在了一道叫“迅疾响应”的题目上。他跑来跟我说页面就一行字啥提示都没有不知道从哪下手。我让他把浏览器开发者工具打开先别碰页面内容去翻一下网络面板里的响应头三秒钟之后他沉默了——flag的前半段就明晃晃躺在里面。这道题给我的印象特别深。不是说它有多难恰恰相反它的难度非常低但它把新手做Web题最该养成的几个习惯全部串起来了先看响应再翻源码最后构造请求。很多人刷题喜欢盯着页面正文看半天恨不得把每个汉字都脑补成线索却忘了HTTP响应里除了body还有headers忘了服务器上可能躺着备份压缩包也忘了代码里的注释有时候比代码本身更有价值。这篇文章我就以“迅疾响应”为例把完整解题过程、背后的原理、以及我踩过和看别人踩过的一些坑全部摊开来讲。如果你是刚接触CTF、Web方向还处于“打开题目不知道干嘛”阶段的人这道题的思路值得完整走一遍。哪怕你以后不打CTF这套信息收集和源码审计的流程放到真实的渗透测试、漏洞挖掘里也完全适用。1. 先搞清楚“迅疾响应”到底在考什么很多新手拿到题目就急着找flag根本不看题面。这其实是大忌。一道正规的CTF题目题面、题目名、环境特征都是线索的一部分尤其是新手题出题人恨不得把考点直接写在脸上。1.1 从题目名和题面能读出哪些信息“迅疾响应”这个题目名关键就在“响应”两个字。响应是什么在Web里就是HTTP Response也就是服务器返回给客户端的那一整套数据。一个完整的HTTP响应由状态行、响应头、空行、响应体组成。绝大多数新手默认只看响应体也就是浏览器里渲染出来的页面内容响应头里的信息几乎被完全忽略。这道题的场景是这样的打开题目环境页面上只有一句欢迎语看起来人畜无害。没有登录框没有链接没有交互功能静态得不像一道Web题。很多人在这一步就懵了开始在页面里找隐藏文字、看HTML源码注释、甚至尝试SQL注入。方向全错。出题人的意图其实很直白题目都叫“迅疾响应”了你还不去看响应头这是一种典型的“题目名即提示”的设计思路。CTF新手赛里大量使用这种命名方式比如“就在嘴边”可能答案就藏在页面文字里“一眼丁真”可能是个图片隐写题。做题的第一步永远是先读题题名、题面、分值分布、题目来源都是信息。1.2 为什么这道题被放进“新手必刷”清单我后来专门去翻了这套题目的其他题发现“菜狗杯”的整体定位就是给零基础玩家设计的。它不会考复杂的逆向、不会考高阶的堆利用更多是让你熟悉比赛的基本玩法看响应头、翻源码、扫目录、改请求、提交flag。这样的题目对于有经验的人来说可能觉得“就这”但对于新手来说价值非常高。因为它完整覆盖了Web题的一条基础解题链路信息收集阶段观察页面、检查响应头、探测敏感文件源码分析阶段下载备份文件、阅读关键代码、定位线索漏洞利用阶段理解权限校验逻辑、构造满足条件的请求、拿flag我见过太多新人一上来就背payload、刷题目完全没建立“先收集信息再分析”的思维。结果就是遇到需要信息收集的题就抓瞎。而“迅疾响应”就是一道逼着你走完这条链路的小题目做完之后后面再遇到类似的新手Web题你的肌肉记忆里自然就有了“先看响应头、再找备份文件”的操作。2. 开局三板斧信息收集的实操记录信息收集是Web题里最容易被轻视、但性价比最高的环节。很多时候flag的线索就摆在明面上只是你还没学会“看”。这里分享一下我在“迅疾响应”这道题上的完整信息收集过程。2.1 先摸清服务端“嘴上说了什么”打开题目环境之后不要急着做任何操作先用工具把最原始的HTTP请求响应完整地看一遍。浏览器地址栏里呈现出来的只是渲染后的页面真正有价值的信息往往藏在网络层。我当时的操作是这样curl -v http://目标地址/curl的-v参数会输出完整的HTTP会话过程包括DNS解析、TCP连接、发送的请求头、接收的响应头。如果你觉得-v输出太乱也可以用-i只看响应头和响应体curl -i http://目标地址/输出大概是这样的HTTP/1.1 200 OK Server: nginx/1.18.0 Date: ... Content-Type: text/html; charsetutf-8 X-Part-1: flag{sta Content-Length: 68 Connection: close 你好我是菜狗杯的一只小菜狗听说你在找我看到没有那个X-Part-1: flag{sta就是线索。它是一个自定义响应头以X-开头的HTTP头通常都是开发者自定义的不属于HTTP标准头。标准响应头里没有X-Part-1这个东西它出现在这里只可能是开发人员或者出题人主动加进去的。为什么出题人要把flag拆一半放进响应头这就是“迅疾响应”的核心考点。从题目设计角度来说它想让新手明白HTTP响应是一个完整的结构头部信息和正文内容同等重要。从现实角度来说很多真实系统的信息泄露就是通过响应头发生的比如运维人员会在响应头里暴露内部IP、服务版本、框架类型甚至直接把调试信息写在自定义头里。学会了看响应头你以后做渗透测试信息收集阶段也会比别人多一个维度的信息源。2.2 响应头才是隐藏信息的主战场拿到X-Part-1之后我第一时间意识到这不是完整flag因为格式对不上。CTF的flag标准格式是flag{内容}而当前拿到的是flag{sta明显被截断了。这时候常规思路有两种一是继续翻页面、找其他线索二是看看有没有别的响应头。我用curl -I单独看了一下响应头确认只有一个自定义头curl -I http://目标地址/输出确认了X-Part-1: flag{sta是唯一的自定义字段。那么flag后半段去哪里了大概率在源码里。这里也体现了一个解题原则当flag被拆成多段时每一段都会对应一个信息收集的入口。你前面发现的每一个线索可能都是通往下一层线索的路标。顺便说一下为什么推荐用curl而不是浏览器开发者工具。浏览器完全可以看响应头F12打开Network面板点一下对应请求就能看到但对新手来说有两个问题第一Network面板信息密集新手容易迷失第二浏览器默认会加载页面资源产生大量额外请求干扰判断。用curl看原始响应干净、直接、没有任何渲染干扰。而且curl是几乎每个Linux发行版自带的工具Windows 10以上也自带没有额外安装成本应该是每个Web方向选手的标配技能。2.3 源码备份文件的获取姿势有了“后半段在源码里”的判断接下来要解决的问题就是如何拿到源码。对一个没有交互功能的静态页面来说直接找源码的常规思路是扫描目录和探测备份文件。很多开发者习惯把网站根目录打包成压缩文件放在服务器上比如www.zip、wwwroot.zip、website.zip、backup.zip本意是备份迁移结果忘记删这在真实世界的网站里也屡见不鲜。新手阶段不推荐一上来就上大字典扫描工具先手工探测几个最常见的文件名更有针对性。我当时的做法是逐个尝试curl -I http://目标地址/www.zip curl -I http://目标地址/backup.zip curl -I http://目标地址/wwwroot.zip结果第一个www.zip就返回了200 OK说明这个备份文件确实存在。这里还要看一下Content-Type和Content-Length确认它是一个真实的zip包而不是一段错误页面HTTP/1.1 200 OK Content-Type: application/zip Content-Length: 2048看到application/zip就可以放心下载了。当然手工探测命中率高是运气好实际做题时如果手工猜不出来还是得用工具跑字典。这里顺手给大家列一下我常用的目录扫描方案对比工具特点适合场景dirsearch自带字典大递归扫描全面新手首选直接默认配置跑gobuster速度快但字典需要自己准备有一定经验、追求效率的人ffuf高度可定制支持并发调优需要和Burp流量联动时用手工curl猜测零成本最快但依赖经验题目有明确提示或常见文件名时我个人的习惯是新手期不要过度依赖扫描器先用手工把常见的文件名试一遍。因为新手题的敏感文件往往就放在显而易见的位置出题人不会故意刁难你去扫描一个五层目录深处的文件。等手工探测没有结果再上扫描器扩大范围。下载下来之后记得先看一眼压缩包里的文件结构。解压www.zip一眼就看到了app.py这就是整个Web应用的源码所在。3. 源码里挖出来的第二段线索拿到源码不等于结束恰恰是开始。很多人下载了源码却不知道看哪里这很正常。完整的Web应用源码可能包含一堆配置文件、静态资源、依赖清单如果毫无章法地乱翻很容易看花眼。审计源码的第一步不是通读所有代码而是先明确你要找什么。这道题里我们的目标是找到flag的下半段或者找到能够拼凑出完整flag的逻辑。3.1 路由与注释审计优先看“门面”文件先把app.py从头到尾读一遍。对于新手题源码量通常很小核心逻辑基本都在单个文件里。我当时的阅读顺序是先看路由定义也就是app.route(/)这类装饰器了解这个应用对外暴露了哪些接口再看每个路由对应的视图函数重点关注有没有返回flag的逻辑最后看代码里的注释和可疑的硬编码字符串app.py的简化版大概长这样from flask import Flask, request app Flask(__name__) flag flag{sta rt_here} app.route(/) def index(): resp app.make_response(你好我是菜狗杯的一只小菜狗听说你在找我) resp.headers[X-Part-1] flag{sta return resp app.route(/flag) def get_flag(): auth request.headers.get(X-Real-Flag) if auth please_give_me: return flag return 没有钥匙进不来哦, 403 if __name__ __main__: app.run(host0.0.0.0, port8000)看到这段代码的时候我心里其实是有点想笑的——因为出题人把flag的下半段直接写在了flag变量里而/flag接口的返回逻辑也没做任何加密混淆只要你让X-Real-Flag请求头的值等于please_give_me服务器就会把完整flag吐出来。但更关键的信息在后面的代码行这个flag变量的赋值是字符串拼接。前半段flag{sta和响应头里的一致后半段rt_here}就直接写在那里。所以其实到这一步你甚至不需要去找/flag接口直接把两段拼起来就是完整flag。当然为了把题目做完整我建议还是把/flag接口的流程也走一遍。这里也引出一个非常重要的审计经验阅读源码时要多留意字符串拼接。尤其是在CTF题目里出题人为了制造“线索分散”的观感经常会把flag拆成几段散落在注释、变量拼接、响应头、JavaScript文件等位置。你在代码里看到flag flag{sta rt_here}这种写法时不要把它当作普通的代码风格问题它极有可能是故意为之。3.2 用Python交互环境模拟服务端逻辑看懂了源码之后我建议新手在本地把核心逻辑跑一遍而不是直接对着远程环境凭感觉构造请求。这不是多此一举——在本地模拟服务端逻辑能帮你确认请求头参数名、校验值、返回逻辑是否和你理解的一致。我当时做的操作是把app.py的flag赋值逻辑单独摘出来flag flag{sta rt_here} print(flag)输出当然是flag{start_here}这里有个看似微不足道但很关键的细节字符串拼接后的和不是flag{start_here}还是你预期的什么别的值取决于两段拼接顺序。如果出题人把顺序打乱比如后半段放在前面或者中间夹了其他字符你就需要按照源码里的实际顺序来组装。千万不要想当然地默认part1part2有些题目拼接规则可能是part2part1甚至中间要插入某个页面里出现过的单词。当然我摘出来跑一遍更重要的意图是让自己记住这个flag的形状flag{start_here}。后续构造请求时看到返回内容和这个一致才能确认整条链路走通了。4. 最后的利用构造请求拿到完整flag源码审计完成之后题目就变成了一个简单的“请证明你能构造特殊请求”的环节。你需要用某种方式访问/flag接口并让服务器认可你的身份。4.1 需要搞清楚接口在“校验”什么继续看源码里路由的逻辑auth request.headers.get(X-Real-Flag) if auth please_give_me: return flag这个接口做的是请求头校验。每当一个HTTP请求到达/flag接口时Flask会从请求头中取出名为X-Real-Flag的字段值拿它和字符串“please_give_me”做比较。相等就返回flag不相等就返回403。由此可见只用浏览器直接访问/flag是拿不到flag的因为浏览器不会主动带上X-Real-Flag这个请求头。你需要手动构造请求把这个头加到HTTP包里。这类基于自定义请求头的身份校验在CTF里非常常见在真实世界里也经常被开发者当作简单的鉴权手段来用。它本质上就是“你知道暗号我就认你”安全性非常依赖暗号是否泄露。在CTF场景里暗号必然存在源码里所以审计源码就等于拿到了钥匙。4.2 构造请求一个curl命令搞定构造自定义请求头最简单的方式是用curl的-H参数。命令如下curl -v -H X-Real-Flag: please_give_me http://目标地址/flag-H参数的完整写法是-H 字段名: 字段值注意冒号后面要跟一个空格这是HTTP头部的标准格式。如果你不加空格很多服务器也能正常解析但为了规范和兼容建议保留。我执行这条命令时的完整输出大致如下 GET /flag HTTP/1.1 Host: 目标地址 User-Agent: curl/8.0.1 Accept: */* X-Real-Flag: please_give_me HTTP/1.1 200 OK Content-Type: text/html; charsetutf-8 flag{start_here}看到200 OK和flag{start_here}的时候这道题就正式解完了。此时可以再把之前响应头里的X-Part-1拼一下确认两段吻合flag{sta加上rt_here}正好就是flag{start_here}。这里顺带给大家补充一个新手容易犯的错误-H参数里的请求头名称是大小写不敏感的但请求头值的大小写是敏感的。也就是说X-Real-Flag写成x-real-flag没问题但please_give_me写成Please_Give_Me就会校验失败。如果你构造的请求一直返回403优先检查请求头值是不是抄错了、多了空格或者大小写不一致。你也可以用Burp Suite来构造请求更适合需要反复调试的场景。开一个Repeater把请求方法改成GET在请求头区域手动加上X-Real-Flag: please_give_me发送即可。不过对于这种一条命令就能解决的题我个人的习惯是能curl就curl简单直接还能在终端里留下操作记录方便复盘。5. 新手最容易踩的坑与排查速查表做这道题的过程中我让几个朋友按照同一个思路去复现结果发现他们卡住的地方五花八门。这些坑对于有经验的人来说可能觉得根本不是问题但对新手来说就是实打实的拦路虎。我整理了一下出现频率最高的四个顺便附上一张排查速查表。5.1 用浏览器直接访问却看不到响应头这是最常见的问题。很多人在第一步就卡住了拿着浏览器反复刷新页面却找不到X-Part-1在哪里。原因很简单他们没有打开开发者工具或者打开了但不知道切到Network面板。正确做法是在页面里按F12打开开发者工具切换到Network网络面板然后刷新页面点击主文档请求通常是第一个名字是当前页面的路径在右侧的Headers请求头区域往下滚动找到“Response Headers”部分。自定义响应头一般在列表偏下的位置。如果你还是找不到可以点击“View source”查看原始响应头文本再用CtrlF搜索X-关键词。不过说实话用浏览器查看效率还是不如curl所以如果你手边有命令行环境强烈推荐直接用curl -I或curl -i。5.2 下载的zip解压不了或内容不对www.zip下载下来之后如果你在Windows上用资源管理器直接打开发现“压缩文件已损坏”先别急着怀疑题目有问题。很可能是你下载的时候用了浏览器而浏览器把zip当作普通文件处理时出了编码问题或者下载不完整。建议用命令行工具重新下载并检查文件大小。curl -o www.zip http://目标地址/www.zip file www.zip ls -l www.zip用file命令查看一下文件类型确认输出是Zip archive data而不是HTML document或者ASCII text。如果文件类型变成HTML说明你下载到的是404报错页面那么路径可能不对。至于解压乱码则可能是压缩包使用了特殊编码Windows自带解压会乱码换成7-Zip通常能解决问题。还有个细节如果解压之后发现源码比你预期的复杂很多不要慌。新手题的核心文件就那么一两个优先找包含路由定义的Python文件或主入口文件别把所有时间耗在阅读配置文件上。5.3 找不到后半段flag死磕没有意义的接口有些人看到/flag接口存在就不管三七二十一先对着它爆破、强啃完全忽略了源码里其实已经把后半段flag拼好了。这是个思路问题。做题要时刻记住自己的目标拿flag。如果已经能从响应头拼出一半、从源码注释拼出另一半那么这道题其实已经解完了不需要把每个接口都打通。很多接口在CTF里就是出题人设置的“冗余内容”用来迷惑你的注意力不一定需要完全利用。你先拼出来的flag如果提交后显示正确那就果断提交不要因为“好像还有个接口没打通”而纠结。5.4 提交flag时格式不对或多了空格这是一个非常低级的错误但也是每年比赛里出现次数最多的问题之一。flag复制下来之后粘贴到提交框之前一定要检查首尾有没有多余的空格或换行。尤其不要自己手打flag——flag{start_here}这个字符串里start_here是下划线不是空格也不是短横线。手打非常容易在细节上出错。还有一点提交框如果提示“flag错误”可以看看是不是大小写问题。CTF的flag一般区分大小写但没有统一标准有的题目会全部小写有的会混合大小写。以服务器返回的原始文本为准原样复制粘贴最稳妥。5.5 问题排查速查表现象可能原因排查思路页面没有线索无从下手没看响应头或没看源码用curl -i完整查看HTTP响应尝试下载www.zip等常见备份文件看到X-Part-1但flag不完整后半段藏在源码或其他文件中下载备份文件审计路由和注释搜索字符串拼接访问/flag返回403缺少请求头或请求头值不对阅读源码确认字段名和值用-H参数构造精确请求提交flag提示错误格式、大小写、多余字符回看源码拼接逻辑检查下划线和空格原样复制粘贴zip解压失败下载不完整或非zip文件检查Content-Type和文件大小用file命令识别真实类型这道题做完之后我个人对“迅疾响应”这套题的体会是它不考智商考的是细心和习惯。我把这次解题流程完整走下来最大的收获不是那一个flag而是养成了一个条件反射——拿到任何Web题目第一件事永远是curl -i看完整响应而不是急着在页面上点点点。说句实在话这个习惯后来帮了我很多次。很多题目的线索就放在响应头里面你只要养成“先看头、再翻源码、最后构造请求”的固定流程新手期就能少走一大半弯路。后面如果你继续刷“菜狗杯”系列的其他题目会发现它们多多少少都延续了这套思路。把这道题的每个细节吃透比盲目刷十道题都管用。