
新手在 CTF Web 方向卡住的地方往往不是 SQL 注入也不是反序列化而是最基础的 HTTP 协议。我见过太多人对着题目提示“请修改请求方法”“请从指定页面跳转过来”发呆地址栏敲了一遍又一遍页面就是不给 flag。问题不在于题目难而在于浏览器太“热心”它把请求方法固定成 GET 或 POST把头字段自动补齐把 302 跳转一路跟到底把一个本来可以拿来做文章的完整过程全藏了起来。CTF Web 前置技能里的 HTTP 协议这一块本质上就是让你把浏览器替你做的这些事一件一件手工接管回来。下面这套东西我会掰开讲报文长什么样、每个字段在靶场里对应哪类题型、用什么工具改、改完怎么验证以及我在这一关踩过的坑。1. 先建立报文级视角浏览器替你藏了什么1.1 一条请求从地址栏到服务器的完整旅程在地址栏敲一个 URL 回车浏览器背后做的事比大多数人想的多。它先解析域名拿到地址建立连接然后按 HTTP/1.1 规范拼出一条文本报文发出去。这条报文由四部分组成请求行、请求头、一个空行、可选的请求体。请求行里三个部分用空格分隔——方法、路径、协议版本例如GET /index.php HTTP/1.1。请求头是若干行字段名: 字段值每行以回车换行结束。最后一个空行是分隔符告诉服务器“头到此为止后面是体”。服务器收到后按同样格式回一条响应报文状态行、响应头、空行、响应体。理解这个结构你在靶场里看到的每一个“提示”才有落点。题目说“请从某处跳转过来”考的就是 Referer 请求头说“只允许本机访问”考的是 X-Forwarded-For 或 X-Real-IP说“请用某种方法访问”考的就是请求行里第一个词。浏览器默认只会发 GET 和 POST且不会主动构造这些“可疑”的头所以你必须跳出地址栏去直接操作报文本身。这不是什么高深技巧只是把被封装的东西重新暴露出来。1.2 用 nc 手工发一条请求理解“空行”的意义想真正搞懂报文结构最快的方式是绕开浏览器用ncnetcat手工发一条原始请求。这个练习我建议每个刚接触 Web 的人都做一次printf GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n | nc 127.0.0.1 80这里面有四个细节值得注意。第一行尾必须是\r\n回车加换行只写\n在某些服务端实现里会被判为格式错误直接返回 400。第二Host在 HTTP/1.1 里是必填字段缺了大概率 400这是很多人手工发包第一个翻车点。第三最后那个“空行”就是连续两个\r\n它标记头的结束少了它服务器会一直等你的后续数据看起来像卡死。第四Connection: close让你的请求发完就断开否则 nc 会一直挂着等响应。提示手工发包时如果终端一直没输出又不退出八成是少写了结尾空行或者 Content-Length 和实际 body 长度对不上。这个练习的价值在于它把你从“点链接”的思维拉到了“字节流”的思维。后面所有关于改方法、加头、构造 body 的操作本质上都只是往这段文本里增删几行而已。1.3 浏览器开发者工具和代理抓包该用哪个两个工具各有用处别混着用。浏览器 F12 的 Network 面板适合看“浏览器实际发了什么”它能完整展示请求头、响应头、响应体、状态码、耗时还能右键“Copy as cURL”把整条请求复制成命令——这个功能非常关键是你从浏览器过渡到命令行工具的桥梁。但它的局限也明显你只能在浏览器允许的范围内改改不了请求方法部分浏览器新版支持编辑重发但能力有限也看不到被自动跟随的跳转中间态。代理抓包工具比如 Burp Suite 或者 ZAP的价值在于“拦截并随意修改”。你把浏览器代理指到本地监听端口请求就会先到工具里停住你可以改任意一行再放行。对于 302 跳转类题目把“自动跟随跳转”关掉你才能看到那个带Location头的原始响应——flag 常常就藏在那条响应里。我的习惯是先用 F12 看懂正常流程再把代理打开重放一遍两者对照问题基本就定位了。下面这个表格是我对不同场景的取舍场景推荐工具原因看正常请求长什么样F12 Network零配置信息完整只改请求头重发F12 编辑重发或代理代理更稳能改任意字段改请求方法、构造 body代理 Repeater浏览器做不到观察 302 原始响应代理关闭自动跳转浏览器会吞掉中间态批量尝试参数代理 Intruder 或脚本手工点太慢2. 请求方法第一道也是最便宜的一道关2.1 九个标准方法各自的语义与考点HTTP 方法定义在规范里语义各不相同。GET 取资源参数在 URL 上POST 提交数据参数在请求体里HEAD 和 GET 一样但只返回响应头不返回体PUT 用于上传或覆盖资源DELETE 删除资源OPTIONS 查询服务器支持哪些方法TRACE 把请求原样回显CONNECT 用于建立隧道PATCH 做局部更新。这套语义在真实业务里被严格程度不一地实现而在靶场里服务器往往就是靠“方法名是否匹配”来放行。方法幂等性典型靶场考点GET是参数在 URL注意编码POST否参数在体注意 Content-TypeHEAD是只拿响应头常用于藏 flagPUT是上传文件、覆盖配置DELETE是删除资源个别题会考OPTIONS是探测支持的方法列表TRACE是回显请求观察报文PATCH否局部更新少见但会出现新手最容易犯的错是以为“方法只有 GET 和 POST”。实际上服务器完全可以接受任何合法 token 作为方法名规范里管这叫扩展方法。所以当题目要求你用某个非标准方法名访问时不要怀疑题目出错直接构造就行。2.2 换方法类题型的标准操作流程假设页面提示“请使用 XX 方法访问本页面”标准动作是这样先用 curl 确认基线。curl -i http://127.0.0.1:8000/target.php-i的作用是把响应头一起打出来这一步很多人省掉结果 flag 明明在响应头里却看不见。确认基线返回 200 但内容提示方法不对之后换方法重发curl -i -X POST http://127.0.0.1:8000/target.php curl -i -X PUT http://127.0.0.1:8000/target.php-X后面跟的就是方法名curl 会把它原样写进请求行。如果题目要求的是一种平台自定义的方法名有些靶场会用一个四字母的单词作为方法照抄进去即可curl -i -X CUSTOM http://127.0.0.1:8000/target.php用代理工具的话流程更直观拦截请求在 Repeater 里把请求行第一个词改掉点发送右侧看响应。Repeater 的好处是改完立刻重发、立刻看结果不用来回切终端。我一般会先在两三个候选方法之间手动试一遍确认方向后再考虑写脚本。注意方法名在规范里是大小写敏感的。get和GET在严格实现的服务端会被区别对待有些题就卡在这里。改的时候照抄题目提示的大小写别自作主张。2.3 自定义方法与大小写陷阱扩展方法的实现差异很大。有的服务端框架会把不认识的方法直接当 GET 处理有的会返回 405还有的会在路由层做精确匹配。所以你在靶场里遇到“方法对了但没反应”的情况先怀疑三件事方法名拼写是否和提示完全一致包括大小写、路径是否带上了必要的查询串、请求体是否为空但服务端要求非空。还有一个隐蔽的坑部分服务端在解析请求行时对空格数量敏感。标准格式是“方法 空格 路径 空格 版本”只允许单个空格。你用脚本拼接字符串时如果不小心多打了个空格请求行就变成了四个字段服务端解析失败返回 400。这种错误在 curl 里不会出现它帮你拼好了但在自己写 socket 或者用 nc 手工构造时很常见。2.4 HEAD 和 OPTIONS 的侦察价值这两个方法在真实渗透里是信息收集手段在靶场里同样好用。HEAD 只返回响应头响应体为空适合快速确认某个路径是否存在、返回什么状态码、有没有Location或自定义头。有些题目的 flag 就明晃晃地放在某个响应头里用 GET 请求时被响应体里的内容盖住了注意力HEAD 一打反而清爽。curl -I http://127.0.0.1:8000/secret.php-I是 curl 里发 HEAD 的简写注意它和-i不是一回事-i是 GET 加显示响应头-I是直接发 HEAD。这两个参数我见过不少人搞混。OPTIONS 则用来问服务器“你支持哪些方法”返回的Allow头会列出清单有时候清单里就藏着一个平时不会用到的方法顺着它试往往能通。2.5 一个常被忽略的点请求体的编码方式换方法只是第一步如果该方法要求带请求体还得考虑编码。application/x-www-form-urlencoded是最常见的形如a1b2特殊字符要做百分号编码multipart/form-data用于文件上传边界字符串必须和 Content-Type 里声明的一致application/json则要求严格的 JSON 语法少个引号整个请求就废了。用 curl 的-d参数时默认会按表单编码发送并自动加上Content-Type: application/x-www-form-urlencoded要发 JSON 得手动指定curl -i -X POST -H Content-Type: application/json -d {user:admin} http://127.0.0.1:8000/api手工构造 multipart 请求时Content-Length必须和实际字节数严格一致。少一个字节服务端会一直等后续数据多一个字节可能被拒。我一般的做法是不手算让工具自动生成——curl 的-F参数会自动算好长度和边界省掉一整类低级错误。3. 请求头逐个拆每一行都是可以改的变量3.1 Host 头与虚拟主机HTTP/1.1 强制要求 Host 头原因是同一台服务器可以承载多个站点服务器靠 Host 判断你要访问哪个。靶场里如果题目提示“只允许从某个域名访问”那考的就是改 Host。用 curl 加-H即可curl -i -H Host: internal.example.com http://127.0.0.1:8000/这里有个容易翻车的点curl 在加了自定义 Host 之后连接目标仍然是 URL 里的地址只是把 Host 头替换掉这正是我们要的效果。但如果你用的是浏览器加代理改 Host 头可能导致 TLS 握手失败或者代理层拒绝因为 Host 和实际连接的主机名不一致。靶场一般是明文 HTTP问题不大一旦涉及 HTTPS 就要留神。3.2 Referer 与来源伪造Referer 记录的是“你从哪个页面点过来的”。很多业务逻辑会拿它做简单的来源校验靶场里对应题型就是“必须从指定页面跳转访问”。浏览器正常点链接会自动带上 Referer但你直接在地址栏输入就不会带。改法很简单curl -i -H Referer: http://127.0.0.1:8000/from.php http://127.0.0.1:8000/target.php值必须和题目要求的地址对得上协议、主机、路径一个字符都不能差。我见过有人把http://写成https://或者路径少个斜杠结果页面一直提示“来源不正确”折腾半天。另外要注意 Referer 的拼写规范里就是这么拼的历史上是 referrer 的误拼少个 r 服务器不认识。3.3 User-Agent 与身份伪装User-Agent 标识客户端类型。靶场里常见的提示是“请使用某种浏览器访问”或者“只允许某个客户端”。改法同样是-Hcurl -i -H User-Agent: Mozilla/5.0 (compatible; SpecialBot/1.0) http://127.0.0.1:8000/这里的关键是搞清楚服务端到底在匹配什么。有的题只匹配 UA 字符串里的某个关键词有的做完整字符串比对。前者随便加点内容就行后者必须一字不差。判断方法很朴素先按提示原样抄一遍看有没有反应没反应就把提示里的字符串完整贴进去再试。如果还是不行回头看看是不是大小写或者空格的问题。3.4 X-Forwarded-For 与代理链路这类头是给代理服务器用的用来记录原始客户端地址。服务端如果直接信任这个头就可能被伪造。靶场里的典型提示是“只允许本地访问”此时加一个curl -i -H X-Forwarded-For: 127.0.0.1 http://127.0.0.1:8000/admin.php同类头还有X-Real-IP、Client-IP、X-Client-IP、X-Originating-IP、Forwarded。不同框架认的头不一样一个不行就换一个试。有时候服务端会取这个头里的第一个 IP有时候取最后一个还有的会做合法性校验。如果加了没反应可以试着写成链式形式X-Forwarded-For: 127.0.0.1, 10.0.0.1看看服务端取哪一段。注意这类伪造只在靶场或你有明确授权的测试环境里做。真实业务系统里伪造来源地址属于越权行为性质完全不同别把学习环境的习惯带出去。3.5 Cookie 与权限位伪造Cookie 是服务端存在客户端的一小段状态。靶场里的 Cookie 类题目通常有两种形态。第一种是明文标记比如is_admin0改成 1 就完事curl -i -b is_admin1 http://127.0.0.1:8000/admin.php-b参数就是带 Cookie多个值用分号加空格分隔。第二种是编码过的常见的是 Base64。这时候要先把值解出来看看结构改完再编回去。我的一般流程是浏览器登录后复制 Cookie用命令行解码观察字段含义改掉权限位重新编码再用-b发过去。如果服务端还做了签名校验比如带个sign字段那单纯改值会被拒这时候要看看签名算法有没有弱点——不过那已经超出前置技能的范围了前置阶段遇到的基本都是明文或简单编码。Cookie 本身的属性也值得看一眼HttpOnly表示 JS 读不到Secure表示只在加密连接发送Path和Domain限定作用范围。这些属性不影响你手工构造请求但能帮你判断服务端的安全实现水平。3.6 Authorization 与基础认证有些页面会弹出一个原生的账号密码框这是 HTTP 基础认证。认证信息放在Authorization头里格式是Basic加空格加用户名:密码的 Base64 编码。用 curl 的-u参数最省事curl -i -u admin:admin http://127.0.0.1:8000/basic/curl 会自动帮你做 Base64 编码并加头。想手工构造也行echo -n admin:123456 | base64 # YWRtaW46MTIzNDU2 curl -i -H Authorization: Basic YWRtaW46MTIzNDU2 http://127.0.0.1:8000/basic/注意echo要加-n否则换行符也会被编进去服务端解出来密码末尾多个换行认证必然失败。这个坑我踩过不止一次。基础认证类题目的常见考法是弱口令用代理的 Intruder 或者一段小脚本跑字典就行while read p; do code$(curl -s -o /dev/null -w %{http_code} -u admin:$p http://127.0.0.1:8000/basic/) if [ $code 200 ]; then echo hit: $p; break; fi done dict.txt-o /dev/null丢掉响应体-w %{http_code}只取状态码这样循环跑得快。认证成功返回 200失败返回 401。3.7 Connection 与请求复用Connection: keep-alive表示复用连接close表示用完就断。这个头在真实业务里影响性能在手工调试里影响你的终端会不会“卡住”。用 nc 手工发请求时如果不加close服务端可能保持连接等你发下一条看起来就像没响应。用 curl 时它自己会处理一般不用管。但如果你在做请求走私一类的进阶内容前置阶段不涉及这个头就是核心变量之一。4. 响应侧状态码、重定向与藏在头里的答案4.1 状态码速查与考点映射响应状态码是三位数字第一位决定大类。1xx 是信息性日常很少见2xx 表示成功3xx 表示重定向4xx 是客户端问题5xx 是服务端问题。靶场里每个码都可能成为线索状态码含义靶场里的常见含义200成功正常返回flag 可能在体或头里301永久重定向跳转目标可能在 Location 里302临时重定向需要看原始响应别自动跟随304未修改缓存相关少见400请求格式错误报文拼错、Host 缺失、长度不对401未认证需要 Authorization 头403禁止访问权限不足检查来源头或方法404未找到路径错或者需要通过跳转才能到达405方法不允许换方法看 Allow 头提示500服务端错误参数异常触发了内部错误看状态码是排查的第一步比读响应体里的提示快得多。我习惯每次请求都加-i或-w %{http_code}先确定码再决定往哪个方向查。4.2 302 跳转类题型的正确处理姿势浏览器遇到 302 会自动跳到新地址你看到的永远是最终页面中间那个带Location头的响应被吞掉了。而很多题目恰恰把关键信息放在第一次响应里。正确做法是把自动跟随关掉。curl 默认不跟随跳转这一点和浏览器相反。也就是说你直接curl -i http://...就能看到 302 的原始响应里面通常有Location: /next.php这样的头。如果你想要的是跟随后的结果加-Lcurl -i http://127.0.0.1:8000/step1.php # 看到 302 和 Location curl -iL http://127.0.0.1:8000/step1.php # 跟随跳转看最终页用代理工具的话在设置里把“不拦截/自动跟随重定向”的选项关掉Repeater 里就能看到每一步的响应。有时候跳转不止一层中间某一步的响应头里会带个特殊字段那就是答案。我遇到过的形态是访问页面返回 302Location 指向一个不存在的路径但响应头里另有一个自定义字段藏着 flag——这种设计就是逼你看原始响应而不是最终页面。4.3 响应头里的 flag 与 curl 的正确用法flag 放在响应头里是很常见的套路。用curl -i就能看到全部响应头但响应体如果很长头会被刷上去肉眼容易漏。更清爽的做法是用-I发 HEAD 请求或者把输出重定向到文件再搜curl -i http://127.0.0.1:8000/ 2/dev/null | head -30 curl -i http://127.0.0.1:8000/ | grep -i flag\|ctf\|tokenhead -30截取前 30 行响应头一般都在这个范围内。grep用-i忽略大小写把可能的字段名都覆盖到。这个习惯养成之后很多“看不见答案”的问题会直接消失。浏览器里也能看F12 打开 Network点开对应请求切到 Headers 标签页Response Headers 一栏就是全部内容。5. 工具链怎么搭curl、代理与重放5.1 curl 参数速查表curl 是这一关的主力工具参数记牢几个高频的就够用参数作用典型场景-i显示响应头看 Location、Set-Cookie-I只发 HEAD快速看响应头-v详细模式排查连接、TLS 问题-X指定方法换 PUT、DELETE 等-H加请求头改 Referer、UA、XFF-d发请求体POST 表单-b带 Cookie伪造会话-c保存 Cookie登录后维持会话-u基础认证账号密码-L跟随跳转看最终页面-k忽略证书校验自签名证书-s静默模式脚本里去掉进度条我的习惯是组合使用curl -is -H ... URL | head -40静默加显示头加截断输出干净适合快速迭代。5.2 本地代理抓包的配置要点代理抓包的核心就三步工具在本机监听一个端口浏览器代理指向这个端口工具里开启拦截。常见的坑有三个。第一只配了 HTTP 代理没配 HTTPS 代理导致加密站点的请求抓不到第二装了工具自带的证书但没信任HTTPS 页面报证书错误第三开了系统代理但浏览器用了独立代理设置两边不一致。排查的时候先确认浏览器能正常上网再看工具里有没有请求进来逐层缩小范围。Repeater 是改包重放的核心界面。我一般的工作流是Proxy 里拦截到一条请求右键发送到 Repeater然后在 Repeater 里改方法、改头、改体点发送看右侧响应。改错了可以撤销改对了可以直接保存成文件备查。这个流程比在终端里反复敲 curl 快得多尤其是需要对比多次请求结果的时候。5.3 把重复劳动脚本化当你要试的候选值超过十个手工点就浪费时间了。比如基础认证爆破、多个 XFF 候选头、多个自定义方法名这些都可以用几十行脚本跑完。Python 的 requests 库处理认证最省事import requests url http://127.0.0.1:8000/basic/ for pwd in open(dict.txt, encodingutf-8): pwd pwd.strip() r requests.get(url, auth(admin, pwd), timeout3) if r.status_code 200: print(hit:, pwd) break脚本化的时候一定要加超时timeout否则某个请求卡住会把整个循环挂死。请求之间可以加几十毫秒的间隔避免把靶场打崩。这类脚本只在你自己搭的环境或者明确授权的靶场里跑这是底线。6. 排错与常见问题6.1 改了没反应先怀疑这三件事第一改的地方对不对。用代理抓包时经常出现“改完发送响应还是一样”一看才发现改的是浏览器发出的那条而真正回源的是另一条比如预检请求或者静态资源。第二改的字段名拼对没有。Referer少个 r、X-Forwarded-For少个连字符服务端直接忽略表现就是“改了跟没改一样”。第三服务端是不是根本就不看这个字段。有些题目看起来像考 Referer实际考的是 Cookie两个都试一遍才知道。还有一个高频问题用 curl 时 URL 没加引号被 shell 解释掉了。URL 里带、?、的时候一定要用单引号包起来否则 shell 会在处截断命令后半段被当成后台任务。这个错误的表现是“请求发出去了但参数不对”查起来很费时间。6.2 问题速查表现象可能原因排查动作400 错误报文格式错、Host 缺失用-v看实际发出的头401 反复出现认证信息错误或编码错确认echo -n没带换行405 错误方法不对看响应里的 Allow 头改了头没效果字段名拼错或被忽略用小写 grep 检查响应页面一直加载Content-Length 与实际不符让工具自动算长度302 看不到浏览器自动跟随关跟随或用 curl 不带-L请求被截断URL 未加引号用单引号包裹 URL代理里没请求浏览器代理设置不一致检查系统与浏览器代理提示遇到完全没头绪的情况把整条请求原样复制出来逐行读一遍。八成的问题都能在“请求行、Host、Content-Length、空行”这四处的某一处看出来。7. 几句实操体会这一关真正的难点不在知识点而在习惯。我刚开始做的时候遇到提示就条件反射去地址栏改 URL改半天没反应就怀疑题目坏了。后来强迫自己每次都先抓包、先看原始请求长什么样问题定位速度快了好几倍。浏览器是个很好的展示工具但它同时也是一个很厚的封装层而这一关考的就是拆封装。你在这一关练出来的“报文思维”后面做注入、做越权、做文件上传的时候都会反复用到——因为那些漏洞的入口归根结底还是某一行请求头或者某一段请求体。再分享一个小技巧每做完一道题把那条能通的完整请求保存下来用注释写清楚改了哪一行、为什么这么改。攒上十几条之后你会发现这些题型的变体其实就那么几类换个字段名、换个路径而已。等你再看到“请从指定来源访问”这类提示时脑子里第一时间跳出来的应该是-H Referer: ...而不是去地址栏瞎试。