某CTF练习平台的技能树里Web方向的第一站几乎都挂着一个叫HTTP协议的前置模块。很多新手看到这个名字会直接跳过——HTTP谁不会浏览器天天在用。但真正卡在这里的人一点都不少而且卡的方式很统一明明知道考点是改User-Agent打开工具却不知道从哪改明明看到302跳转却盯着页面内容找flag明明抓到了包改完发出去却发现服务器不认。这个模块的价值不在于教你HTTP是什么而在于把HTTP从一个抽象的协议名词变成一组可以手工操作、可以任意篡改、可以反复重放的报文。它要训练的核心能力只有一句话把浏览器自动替你做的事情全部拆开、手动做一遍并且想改哪就改哪。这篇文章就围绕这条主线把请求报文的结构、方法语义、状态码链路、请求头伪造、Cookie会话、抓包改包工作流以及我在这类题目里反复踩到的坑一次性讲透。适合刚接触Web方向、能看懂浏览器开发者工具但不知道下一步该干嘛的读者也适合已经做过几题但总是靠猜的人。1. 从一条原始请求报文说起协议类题目到底在考什么1.1 浏览器替你隐藏掉的那部分信息在浏览器地址栏敲一个网址、按回车你看到的是页面看不到的是这中间飞过去的一整段纯文本。这段文本长这样GET /index.php HTTP/1.1 Host: example.local User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Accept: text/html,application/xhtmlxml Accept-Encoding: gzip, deflate Connection: keep-alive Cookie: sessionabc123四行请求行加请求头空一行然后才是可选的请求体。这就是HTTP请求的全部真相——它就是一段有固定格式的字符串没有任何加密、没有任何魔法。理解这一点非常关键因为只要它是字符串你就可以任意编辑它。所谓HTTP协议这一关的所有题目本质上都是在问同一件事你能不能把这段字符串里的某一个字段改成服务器期望的值。我见过太多人卡住的原因是把浏览器当成了唯一入口。他们习惯性地点页面上的按钮、填表单、刷新然后发现无论怎么操作结果都一样。问题就在这里——按钮能改的字段服务器早就默认你会改了真正被考的字段页面上根本没有对应的输入框。你必须绕到浏览器背后直接对报文下手。1.2 请求行三要素与它们各自的考点请求行是方法 路径 版本三段式每一段都可以单独出题。第一段是方法决定服务器用什么逻辑处理这次请求。绝大多数人只用过GET和POST但协议规范里定义了一整套动词服务器是可以对每个动词挂不同处理逻辑的。所以方法这一段的考点往往是服务器只接受某个你没用过的方法用常规GET打过去会得到405。第二段是路径也就是资源定位。这部分的考点比方法丰富得多路径大小写、路径末尾是否带斜杠、路径里的编码字符、路径对应的隐藏文件。很多题目不需要你改请求头只需要你猜中一个没被链接暴露的路径。第三段是版本实际做题时很少成为考点但它会影响到连接行为比如HTTP/1.1默认keep-alive。真正需要注意的是某些服务器对不同版本的头部处理严格程度不同这一点在后面讲Host头的时候会用到。提示判断一道题该从哪下手最快的办法是先看响应头。响应头里出现的Allow、Location、Server、Set-Cookie这几个字段几乎等于把线索写在了明面上。1.3 为什么前置技能这四个字很重要这个模块被放在Web方向的最前面不是因为简单而是因为后面所有模块都要用到它。SQL注入要改请求体文件上传要构造multipart请求体越权要替换CookieSSRF要让服务器发出请求。这些操作的前提都是同一件事你能熟练地手工构造和修改任意一条HTTP报文。所以我的建议是做这个模块时不要只追求通关。每道题做完回头问自己三个问题这题的判定逻辑写在服务端的哪一层我是通过改哪一段报文绕过的如果我手上没有浏览器只用命令行能不能复现第三个问题能答上来说明你是真的掌握了而不是碰巧点对了。2. 请求方法不是只有GET和POST动词语义与405的破局思路2.1 常用方法的能力边界对照先把规范里那几个标准动词的实际用途理清楚这样遇到服务器不认我这个方法时你才有方向去试。方法语义是否带请求体典型响应码协议题里的常见用法GET获取资源一般不带200 / 404常规探测、参数传递POST提交数据带200 / 201表单、接口调用HEAD只要响应头不要响应体不带200探测响应头、绕过内容检查PUT上传/覆盖资源带201 / 405尝试写入文件DELETE删除资源一般不带204 / 405探测资源操作权限OPTIONS查询支持的方法不带200 / 204直接看Allow头TRACE回显请求不带200观察请求被中间层改了什么这张表里最被低估的是OPTIONS。很多题目服务器会被配置成对OPTIONS请求返回一个Allow响应头把该路径支持的方法全列出来。这一步等于免费拿到答案不需要盲猜。我在做协议类题目时如果拿到一个看起来平平无奇的路径第一件事往往就是发一个OPTIONS过去看看。curl -i -X OPTIONS http://target.local/admin/ -H Host: target.local-i的作用是把响应头一起打印出来-X指定方法。这两条命令组合起来比你打开浏览器点半天效率高得多。2.2 405与501的区别服务器在告诉你什么请求方法被拒绝时最常见的两个状态码是405和501但它们的含义完全不同很多人混为一谈。405 Method Not Allowed的意思是这个资源我认识路径是存在的但我不支持你用这个方法访问它。这是最有价值的错误码因为它等于间接确认了路径有效。看到405几乎可以确定下一步应该换方法而不是换路径。501 Not Implemented的意思是我这个服务器压根不认这个方法。这种情况通常出现在你用了自定义方法或者拼写错误时。服务器连解析都不愿意解析直接拒绝。所以这两个码给你的是完全相反的行动指引405让你改方法501让你先检查方法名写对了没有。我刚开始做这类题时把501当成这个路径不支持这个方法然后去换路径绕了一大圈才发现是自己方法名里多打了一个字符。2.3 自定义方法与HEAD的妙用除了标准动词服务器是可以注册任意自定义方法的。这类题目的判定逻辑通常写在路由层如果方法名等于某个特定字符串直接返回结果否则走默认分支。用浏览器永远触发不到因为浏览器只会发GET和POST。命令行直接构造就行curl -i -X 自定义方法名 http://target.local/ -H Host: target.local另一种常见情况是HEAD。有些题目会把结果藏在响应头里而不是响应体里页面正文看起来一片空白但用HEAD或者带-I参数发请求响应头里就带着线索。反过来有些题目会对响应体做内容检查但检查逻辑没有覆盖HEAD请求这时候用HEAD往往能拿到原始响应头而不触发过滤。注意HEAD请求的响应里不会有响应体所以如果你用HEAD去请求一个正常情况下会返回内容的接口看到的空是正常的不代表出错。判断依据要看状态码和响应头不要看页面。2.4 一个容易忽略的细节请求体与Content-Length的配合用POST或PUT发请求体时Content-Length必须和实际发出的字节数一致。用curl自动算没问题但如果你手工拼报文或者用某些代理工具改包改完请求体却没改长度服务器会因为读不到完整请求体而挂起或者直接返回400。我在一开始用代理工具改包时吃过这个亏把一个长度很短的参数改成一个很长的字符串工具没有自动重算长度请求发出去后就一直转圈最后超时。后来养成的习惯是凡是手工改过请求体一定回头检查Content-Length那一行。3. 状态码与重定向链路线索经常藏在你不看的地方3.1 3xx家族的分工与跳转链路追踪3xx系列是重定向但里面每个码的行为差别很大尤其是302和307这两个在协议题里经常被拿来设陷阱。状态码语义客户端行为注意事项301永久重定向跳转且可能缓存第二次请求可能不再访问原地址302临时重定向跳转方法通常变GET最常用也最容易出题303见Other强制改用GET语义明确少见307临时重定向跳转且保持原方法和302最容易混淆308永久重定向跳转且保持原方法少见关键差异在方法是否保留。302在历史实践中大量客户端会把POST改成GET再跳转而307明确要求保持原方法。如果一道题的判定逻辑依赖跳转后仍然是POST那它就一定会用307而不是302。追踪跳转链路最直接的参数是-L让curl自动跟随curl -i -L http://target.local/start -H Host: target.local但这里有个坑加了-L之后中间每一次跳转的响应头会连在一起输出如果你不仔细分辨很容易漏掉第一次跳转的Location。我习惯的做法是先用-i不带-L发一次看清楚第一次响应长什么样再决定要不要跟随。第一次跳转的响应头往往就是答案所在。3.2 Location头里的信息量Location响应头告诉客户端下一站去哪。在协议题里它经常承担两个作用一是把请求引到一个需要特殊条件的路径二是直接在路径里携带参数。举个例子访问/返回302Location: /next.php?tokenxxx。这时候如果你用浏览器会自动跳到/next.php?tokenxxx并正常显示页面你可能完全没意识到中间发生过什么。但如果服务器判定必须带着token参数从首页跳转过来而你在next.php上直接刷新参数丢了就会被拒绝。这就是为什么我一直强调要看不带跟随的原始响应。浏览器帮你把跳转过程抹平了而题目往往就设在这些被抹平的环节上。3.3 4xx与5xx的误判成本新手在这类题目里最常见的时间浪费是把403当成这个路径不存在然后放弃。401需要认证。看响应头里有没有WWW-Authenticate有的话说明它期望你带上认证信息。403路径存在但拒绝访问。多半是访问条件不满足来源、IP、请求头这是最值得继续挖的状态码。404路径确实不存在。418一个玩笑状态码但有些题目会用它作为特定条件的标记。500服务端出错可能是你的输入触发了某个分支值得看响应体有没有报错信息。我的判断顺序一直是403优先深挖401先看认证头500看报错404才考虑换路径。把403当404直接跳过是我早期最常见的失误。4. 请求头伪造类题型UA、Referer与来源判断的绕过逻辑4.1 User-Agent判断最基础的你不是你以为的浏览器服务端可以读到你发来的任何请求头包括User-Agent。如果判定逻辑是只有特定UA才返回结果那你只要把UA改成那个值就行。curl -i http://target.local/ -H User-Agent: 指定的UA字符串看起来简单但这里有两个实操细节。第一UA字符串里如果含有特殊字符注意引号包裹别让shell把空格拆开。第二有些判定是子串匹配而不是全等匹配也就是只要UA里包含某个关键词就放行。这种情况下你不需要知道完整UA包含那个关键词就够了。反过来如果判定是全等匹配你多一个字符都不行这时候可以先用一个完全错误的UA试探看返回的提示信息里有没有透露期望值。4.2 Referer来源校验的薄弱环节Referer表示你是从哪个页面点过来的。判定逻辑通常是检查它是否包含某个域名或某个路径。curl -i http://target.local/admin.php -H Referer: http://target.local/index.php这类校验的弱点在于Referer完全由客户端提供服务器没有任何办法验证真伪。所以它能防住的只有普通用户在地址栏直接输入网址这一种情况对任何会发请求的人都是无效的。实际做题时要注意两点。一是有些题目会检查Referer的前缀是不是某个域名这种情况下你构造http://target.local.xxx/这种前缀相同的地址也可能绕过取决于服务端用的是子串匹配还是严格解析。二是现代浏览器在跨域场景下可能不发送Referer所以用浏览器插件改Referer有时不生效命令行反而更可靠。4.3 X-Forwarded-For本地访问限制的突破点这类题目的形态很固定页面提示只允许本地访问你直接访问是拒绝的。服务端判断是否本地通常读的是客户端的IP但很多部署环境前面有反向代理于是约定用X-Forwarded-For这个头来传递真实客户端IP。问题就出在这里这个头也是客户端可以自己填的。curl -i http://target.local/ -H X-Forwarded-For: 127.0.0.1常见的几个变体和对应关系值得记一下请求头常见用途绕过思路X-Forwarded-For真实客户端IP直接填本地地址X-Real-IP同上部分中间件使用同上Client-IP同上同上Forwarded标准化写法格式为for127.0.0.1我遇到过一道题前面三个头都试了没用最后是Forwarded: for127.0.0.1起效。原因是那套中间件用的是标准化写法。所以这类题的正确姿势是把几个变体一次全带上一次请求里同时设置多个头服务器读哪个都能读到本地地址curl -i http://target.local/ \ -H X-Forwarded-For: 127.0.0.1 \ -H X-Real-IP: 127.0.0.1 \ -H Client-IP: 127.0.0.1 \ -H Forwarded: for127.0.0.1提示判断服务端到底读的是哪个头有个小技巧——把几个头设成不同的值比如分别设成 1.1.1.1、2.2.2.2然后看它的提示信息里回显的是哪个。有些题目会把识别到的IP打印在页面上等于直接告诉你答案。4.4 Host头与虚拟主机判断Host头是HTTP/1.1里唯一强制要求的请求头它告诉服务器你要访问哪个站点。同一台服务器上可以挂多个站点靠Host头区分。这类题目的判定逻辑是只有Host等于某个值时返回结果。你在浏览器里访问时Host是自动填的改不了所以必须手工构造curl -i http://target.local/this/is/the/path -H Host: 期望的主机名这里有个容易搞错的地方URL里的主机名和Host头不必相同。URL决定你连到哪台机器Host头决定服务器认为你要哪个站点。所以你可以连到目标IP但把Host设成任意值。这正是这类题目的出题空间所在。5. Cookie、会话与认证头一次会话里能被读写的全部状态5.1 Set-Cookie到Cookie的往返服务器通过Set-Cookie响应头下发状态客户端通过Cookie请求头带回。这个往返是HTTP保持状态的唯一手段也是最容易被篡改的地方。用curl时要显式带上Cookiecurl -i http://target.local/profile -H Cookie: sessionabc123; roleuser或者用-b参数它会自动处理格式curl -i -b sessionabc123; roleuser http://target.local/profile两者的区别在于-H Cookie: ...是原样发送你写的内容而-b在某些版本里会做一些规范化处理。做协议题时我更倾向用-H因为我要精确控制发出去的每一个字符。5.2 Cookie内容的常见编码方式Cookie里的值经常不是明文常见的有几种形态认出来基本就知道怎么处理了。第一种是Base64。特征是字符集为大小写字母加数字末尾可能有等号填充。判定逻辑往往就是把Cookie解Base64后看某个字段是不是特定值你只要反着做解码、改字段、再编码回去。echo -n dXNlcj11c2Vy | base64 -d echo -n useradmin | base64第二种是URL编码。特征是出现大量百分号。这种在路径和参数里更常见。第三种是序列化格式比如形如a:2:{s:4:role;s:4:user;}的结构。这种一看就是某种语言的序列化结果改动时要特别小心长度字段——上面那个例子里s:4:user的4表示字符串长度你把user改成admin就必须同时把4改成5否则解析直接失败。我第一次碰到这种格式时就是因为没改长度反复提交都失败还以为是判定逻辑更复杂。5.3 Authorization与Basic认证Authorization: Basic头的内容是用户名:密码做Base64。有些题目会把凭据藏在页面注释、响应头或者某个可预测的位置拿到之后拼起来编码就行。echo -n admin:password123 | base64 # 输出会放在下面这个头里 curl -i http://target.local/ -H Authorization: Basic YWRtaW46cGFzc3dvcmQxMjM注意Base64只是编码不是加密它不提供任何保密性。做题时看到一个形如Basic xxxxx的值直接解开看十有八九就是明文凭据。另外401响应里的WWW-Authenticate头会告诉你认证方式Basic还是其他这是判断要不要走这条路的关键信号。看到它就是明牌告诉你去准备凭据。6. 抓包改包的实际工作流从打开工具到发出重构请求6.1 先看清再动手响应头是第一手线索我见过很多人拿到题目第一反应是开始猜参数猜了二十分钟没进展。更高效的做法是先做一次侦察式请求把响应头完整看一遍。浏览器开发者工具的网络面板里点开任意一个请求Headers标签页会给出发送的头和接收的头。重点看这几项Server什么服务器、Set-Cookie下发了什么状态、Location有没有跳转、Allow支持哪些方法、Content-Type服务端预期什么格式。命令行下等价的操作是curl -i或者curl -v。-v的详细程度更高会把连接建立、TLS握手如果是https、发送的请求头、接收的响应头全打出来适合排查我明明改了头为什么没生效这类问题。6.2 浏览器里的Copy as cURL是效率拐点在开发者工具里右键任意请求选择复制为cURL命令你会得到一条完整还原这个请求的命令行包含所有请求头、Cookie、请求体。这条命令的价值在于它是一个可以直接编辑的起点。拿到之后把里面的目标地址改成你要测的把要改的头改掉删掉不需要的头比如Accept-Encoding有时会让响应变成压缩格式不方便看然后执行。这比从零手写curl快得多也不容易漏掉关键的头部。# 复制出来的命令大概长这样改起来很直观 curl http://target.local/api/query \ -H Host: target.local \ -H User-Agent: 改成需要的值 \ -H Referer: http://target.local/index.php \ -H Cookie: session改这里 \ --data-raw {key:value}6.3 用脚本批量试探而不是一条条手敲当需要试探的组合比较多时比如同时试几个XFF变体、试几个方法、试几个路径手敲效率太低。用一段短脚本批量跑输出只保留状态码和长度速度会快很多。import requests base http://target.local/ headers_list [ {X-Forwarded-For: 127.0.0.1}, {X-Real-IP: 127.0.0.1}, {Client-IP: 127.0.0.1}, {Forwarded: for127.0.0.1}, ] for h in headers_list: try: r requests.get(base, headersh, timeout5, allow_redirectsFalse) print(list(h.keys())[0], r.status_code, len(r.text)) except Exception as e: print(list(h.keys())[0], error, e)关键是allow_redirectsFalse不让它自动跟随跳转这样你才能看到第一跳的原始响应。很多人用requests时被默认的自动跳转坑过——明明看到302却抓不到Location后面的内容原因就在这里。6.4 工具选型的取舍方式适合场景优点缺点浏览器开发者工具初次侦察、看页面结构零成本、直观改包不方便浏览器插件改包改动小、需要保持登录态上手快改复杂报文容易出错curl精确控制每个字段完全可控、可复制长命令容易打错脚本批量组合试探效率高需要一点编码基础图形化代理工具反复重放、看完整报文报文结构一目了然需要配置代理我的习惯是侦察用浏览器构造用curl批量用脚本需要反复微调同一份报文时用代理工具的重放功能。不要在一种工具上死磕切换成本远低于你想象。7. 我在这类题目上反复踩的坑7.1 编码的二次处理你以为你发的是你写的URL编码是最容易出事的地方。你在curl里写的%20到了服务端可能被解码成空格也可能被解码两次变成别的字符。有些题目的判定逻辑是在解码前比对有些是在解码后比对你还得判断它是在哪一层做的判断。我的做法是遇到带特殊字符的路径或参数先用最朴素的写法试一次再试编码后的写法对比两次响应有什么差别。如果两次结果不同说明服务端对编码的处理有讲究那就顺着这个差异往下挖。另外Base64和URL编码经常嵌套出现什么先Base64再URL编码。看到一串既像Base64又带百分号的东西先做URL解码再做Base64解码顺序反了会解出乱码。7.2 大小写敏感的假象路径和方法名在协议层面有大小写约定方法名是大小写敏感的规范里是大写而路径是否敏感取决于服务端文件系统。Linux下路径区分大小写某些其他环境不区分。所以当你发现这个方法不行时先确认一下自己写的是不是全大写。同样路径里的大小写差异可能导致404但如果你把路径整体换成小写可能又命中了另一套逻辑。这类细节不值得深挖但值得在失败时快速自查一遍。7.3 缓存与keep-alive带来的改了没反应这是我被坑得最久的一次。改完请求头重发返回的还是老结果检查了十遍请求确实改对了。后来发现是中间层的缓存问题——某些请求被缓存了不管你请求头怎么改返回的都是缓存的那份。排查办法很简单加一个无意义的查询参数或者一个Cache-Control: no-cache头把缓存绕过去。curl -i http://target.local/page?t$(date %s) -H Cache-Control: no-cachet后面跟时间戳是常见做法保证每次URL都不同缓存自然失效。这个技巧在日常调试接口时同样管用值得养成习惯。还有一种情况是连接复用。某些工具默认使用长连接如果你在同一个连接上连续发多次请求某些中间设备可能对第二次请求的处理有差异。遇到第一条请求正常第二条就异常的诡异现象试试强制关闭连接复用。7.4 看不见的字符空格、换行与全角符号请求头里如果混入了不可见字符服务器解析会直接失败但你肉眼完全看不出来。最常见的来源是从网页复制UA或者其他头部值时带上了行尾的空格或不可见字符。判断方法把请求头值用十六进制看一眼。命令行下echo -n 字符串 | xxd就能看出有没有异常字节。我曾经因为UA末尾多了一个空格反复确认了七八次都认为我写的就是它要求的那个值最后还是靠十六进制对比才发现的。另外一个隐蔽来源是输入法。做中文环境下的题目时如果路径里混入全角字符比如全角的斜杠或者全角字母看起来和半角几乎一样但服务器完全不认。这个坑没有捷径只能靠对比。7.5 判定逻辑的层级改错地方等于没改最后说一个思路层面的坑。同一道题服务器可能在三层做判断路由层、业务层、模板层。你在路由层改了请求头通过了检查业务层还有一道业务层过了模板层可能还要看某个Cookie。所以当一次改动部分生效时比如页面内容变了但结果还不对不要以为改错了而是应该意识到可能还有下一道。这时候的线索通常出现在变化后的页面里可能是新的提示文案可能是新的Cookie可能是一个新出现的路径。顺着变化走而不是推倒重来这是我在做了几十道这类题之后形成的最有效习惯。提示每改一次都把完整的响应头存下来做对比。很多时候答案不在当前响应里而在上一次响应和这一次响应的差异里。最后分享一个我觉得最实用的小技巧把每次成功的请求完整记录下来包括完整的URL、所有请求头、请求体、以及那一串导致成功的改动点。这类协议题的知识点密度很低但组合方式很多你这次靠XFF过的下次可能是Host加Referer的组合。有一份自己的记录本比任何笔记都值钱因为那是你自己踩过的路径。
阅读完成 · 觉得有帮助?