新手在 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 手工发一条请求,理解“空行”的意义
想真正搞懂报文结构,最快的方式是绕开浏览器,用nc(netcat)手工发一条原始请求。这个练习我建议每个刚接触 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-Type |
| HEAD | 是 | 只拿响应头,常用于藏 flag |
| PUT | 是 | 上传文件、覆盖配置 |
| 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是最常见的,形如a=1&b=2,特殊字符要做百分号编码;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 标识客户端类型。靶场里常见的提示是“请使用某种浏览器访问”或者“只允许某个客户端”。改法同样是-H:
curl -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_admin=0,改成 1 就完事:
curl -i -b 'is_admin=1' 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这样的头。如果你想要的是跟随后的结果,加-L:
curl -i http://127.0.0.1:8000/step1.php # 看到 302 和 Location curl -iL http://127.0.0.1:8000/step1.php # 跟随跳转看最终页用代理工具的话,在设置里把“不拦截/自动跟随重定向”的选项关掉,Repeater 里就能看到每一步的响应。有时候跳转不止一层,中间某一步的响应头里会带个特殊字段,那就是答案。我遇到过的形态是:访问页面返回 302,Location 指向一个不存在的路径,但响应头里另有一个自定义字段藏着 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\|token'head -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", encoding="utf-8"): pwd = pwd.strip() r = requests.get(url, auth=("admin", pwd), timeout=3) 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: ...',而不是去地址栏瞎试。