HTTP 请求与响应
HTTP 请求与响应
HTTP 是浏览器和服务器之间传递数据的协议。Web CTF 里,很多漏洞不是藏在页面文字里,而是藏在请求和响应的细节里。
HTTP 请求是什么
一次请求大致长这样:
GET /search?q=test HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Cookie: session=abc123第一行包含三个信息:请求方法、路径、协议版本。
GET /search?q=test HTTP/1.1 表示浏览器正在请求 /search,并传了一个参数 q=test。
请求方法
GET 通常把参数放在 URL 里,适合查询。
POST 通常把参数放在请求体里,适合登录、提交表单、上传文件。
PUT、DELETE、PATCH 在普通网站里少见,但在 API 题里可能代表修改或删除资源。遇到 API 时,不要只测试 GET 和 POST。
Header 是什么
Header 是请求的附加信息。CTF 中常见重要 Header:
Cookie 用来保存登录状态。
User-Agent 表示客户端类型,有些题会要求伪装成特定浏览器或爬虫。
Referer 表示请求从哪里跳来,有些防盗链或弱校验会检查它。
X-Forwarded-For 常用于记录真实 IP,部分题会用它做本地访问或管理员判断。
HTTP 响应是什么
响应大致长这样:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session=abc123
<html>...</html>第一行是状态码。200 表示正常,302 表示跳转,403 表示禁止访问,404 表示不存在,500 常表示服务端异常。
响应 Header 里也可能藏线索,比如 Set-Cookie、Server、X-Powered-By、自定义 token。
CTF 中怎么看 HTTP
不要只看浏览器页面。页面是响应体渲染后的结果,很多信息在渲染前更清楚。
要同时看:
- URL 参数。
- 请求方法。
- Cookie 和 token。
- 响应状态码。
- 响应 Header。
- 响应体中的注释、报错和隐藏字段。
最小例子
如果访问:
/profile?id=1返回用户 A;访问:
/profile?id=2返回用户 B,说明 id 参数影响了服务端查询。这个观察可以引出越权、SQL 注入、IDOR 等后续测试。
HTTP/2 概述
HTTP/2 是 HTTP 协议的第二个主要版本,主要改进包括:
HTTP/2 特性:
1. 二进制分帧: 不再是纯文本协议
2. 多路复用: 一个连接可以并行多个请求
3. 头部压缩: HPACK 压缩减少开销
4. 服务器推送: 服务器可以主动推送资源
5. 流优先级: 可以设置请求优先级HTTP/2 安全相关:
1. 请求走私: HTTP/2 到 HTTP/1.1 降级可能产生走私
2. 头部注入: 某些实现允许注入 \r\n 到头部值
3. 伪头部: :method, :path, :authority, :scheme
4. 不使用 Transfer-Encoding: 长度由帧头决定# HTTP/2 请求结构 (概念)
# 与 HTTP/1.1 的文本格式不同,HTTP/2 使用二进制帧
# HTTP/1.1 请求:
# GET /path HTTP/1.1\r\n
# Host: example.com\r\n
# \r\n
# HTTP/2 请求 (伪头部 + 普通头部):
# :method: GET
# :path: /path
# :authority: example.com
# :scheme: https请求走私基础知识
请求走私利用前端代理和后端服务器对 HTTP 请求边界解析的差异:
# 请求走私的核心: Content-Length vs Transfer-Encoding
# CL.TE 示例
cl_te_request = """POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED"""
# TE.CL 示例
te_cl_request = """POST / HTTP/1.1
Host: target.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
"""
# 前端和后端对同一个请求的解析不同:
# - 前端按 CL 解析: 认为请求到某个位置结束
# - 后端按 TE 解析: 认为请求在另一个位置结束
# - 差异部分被当作下一个请求的开头import socket
def send_raw_http(host, port, request):
"""发送原始 HTTP 请求"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((host, port))
sock.send(request.encode())
response = b""
try:
while True:
data = sock.recv(4096)
if not data:
break
response += data
except socket.timeout:
pass
sock.close()
return response.decode(errors='ignore')
# 测试请求走私
def test_smuggling(host, port=80):
# CL.TE 测试
request1 = (
f"POST / HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"Content-Length: 6\r\n"
f"Transfer-Encoding: chunked\r\n"
f"\r\n"
f"0\r\n"
f"\r\n"
f"X"
)
resp = send_raw_http(host, port, request1)
print(f"CL.TE 响应: {resp[:200]}")
# TE.CL 测试
request2 = (
f"POST / HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"Content-Length: 3\r\n"
f"Transfer-Encoding: chunked\r\n"
f"\r\n"
f"8\r\n"
f"SMUGGLED\r\n"
f"0\r\n"
f"\r\n"
)
resp = send_raw_http(host, port, request2)
print(f"TE.CL 响应: {resp[:200]}")Hop-by-Hop Headers
Hop-by-hop headers 只对单个连接有效,不会被代理转发:
- Connection
- Keep-Alive
- Proxy-Authenticate
- Proxy-Authorization
- TE
- Transfer-Encoding
- Upgrade
安全问题:
如果代理删除了 Transfer-Encoding 但后端仍然处理它
可能导致请求走私def test_hop_by_hop_bypass(url):
"""测试 hop-by-hop header 绕过"""
# 正常请求
headers_normal = {
"Connection": "close",
}
# 尝试添加 hop-by-hop header 绕过
headers_bypass = {
"Connection": "close, Transfer-Encoding",
"Transfer-Encoding": "chunked",
}
resp1 = requests.get(url, headers=headers_normal)
resp2 = requests.get(url, headers=headers_bypass)
print(f"正常请求: {resp1.status_code} - 长度 {len(resp1.text)}")
print(f"绕过请求: {resp2.status_code} - 长度 {len(resp2.text)}")
if resp1.text != resp2.text:
print("[!] 响应不同,可能存在 hop-by-hop 绕过")HTTP 缓存行为
HTTP 缓存机制:
1. 浏览器缓存: 本地存储响应
2. 代理缓存: CDN/反向代理缓存
3. 网关缓存: 应用层缓存
缓存相关 Header:
- Cache-Control: max-age, no-cache, no-store, public, private
- Expires: 过期时间
- ETag: 资源标识符
- Last-Modified: 最后修改时间
- Vary: 缓存键
缓存投毒:
- 利用缓存存储恶意响应
- 影响后续访问同一 URL 的用户
- 常与请求走私结合def check_cache_behavior(url):
"""检查缓存行为"""
resp = requests.get(url)
cache_headers = {
"Cache-Control": resp.headers.get("Cache-Control", ""),
"Expires": resp.headers.get("Expires", ""),
"ETag": resp.headers.get("ETag", ""),
"Last-Modified": resp.headers.get("Last-Modified", ""),
"Vary": resp.headers.get("Vary", ""),
"Age": resp.headers.get("Age", ""),
}
print("缓存相关 Header:")
for header, value in cache_headers.items():
if value:
print(f" {header}: {value}")
# 测试缓存键
# 如果 Vary 不包含某个 Header,该 Header 可能被用于缓存投毒
if "Vary" in resp.headers:
print(f"\n缓存键 (Vary): {resp.headers['Vary']}")
else:
print("\n未设置 Vary,可能更容易被投毒")请求方法安全测试
import requests
def test_http_methods(url):
"""测试各种 HTTP 方法"""
methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD",
"TRACE", "CONNECT", "PROPFIND", "MKCOL", "COPY", "MOVE"]
for method in methods:
try:
resp = requests.request(method, url, timeout=5)
print(f"{method:10} -> {resp.status_code}")
# OPTIONS 通常返回允许的方法
if method == "OPTIONS":
allow = resp.headers.get("Allow", "")
print(f" Allow: {allow}")
# TRACE 可能返回请求内容 (XST 攻击)
if method == "TRACE" and resp.status_code == 200:
print(f" [!] TRACE 启用,可能存在 XST 风险")
except Exception as e:
print(f"{method:10} -> 错误: {e}")Burp Suite HTTP 分析技巧
Burp Suite 分析 HTTP 请求响应:
1. Proxy > HTTP History
- 查看所有请求和响应
- 按类型、状态码、MIME 过滤
2. Repeater
- 手动修改请求并重放
- 测试不同参数和 Header
3. Comparer
- 对比两个请求或响应
- 发现细微差异
4. Decoder
- 解码 Base64、URL、HTML 实体等
- 编码 payload
5. Logger
- 记录所有请求,包括被丢弃的
6. 关注点:
- 状态码: 200/302/403/404/500
- Set-Cookie: 安全标志
- Content-Type: 是否正确
- 自定义 Header: X-*, Authorization
- 响应体: 注释、隐藏字段、报错信息常见误区
- 只看页面,不看请求。
- 只改 URL,不看 Cookie。
- 看到 302 跳转就忽略跳转前响应。
- 看到 500 就关掉页面,其实 500 可能正是漏洞线索。
一句话判断
Web 题里只看浏览器渲染页面解释不了现象时,就回到 HTTP 请求和响应:方法、路径、参数、Header、Cookie、状态码、跳转、响应体和缓存细节往往就是突破口。
HTTP 是 Web 题的底层观察面。很多漏洞不是先从 payload 开始,而是先从"这个请求到底发了什么,服务端到底回了什么"开始。
题目中常见信号
- 页面没有明显入口,但请求里有隐藏参数、JSON、Cookie 或自定义 Header。
- 302 跳转前响应体或 Header 里有线索。
- 修改
X-Forwarded-For、Referer、User-Agent后响应变化。 OPTIONS暴露额外 HTTP 方法。- 500 报错泄露框架、路径、SQL、模板或调用栈。
- API 接口前端不显示,但响应 JSON 包含更多字段。
- 同一路径在不同方法、不同 Content-Type 下行为不同。
核心概念
一次 HTTP 交互至少包含这些可观察对象:
请求方法 + 路径 + query 参数
请求 Header + Cookie + Authorization
请求体 + Content-Type
响应状态码 + 响应 Header
响应体 + 跳转链 + 缓存行为做 Web CTF 时要先建立基线请求,再单独改变一个变量。这样才能知道响应变化到底由哪个字段造成。
最小分析流程
- 用浏览器或 Burp 保存正常请求。
- 记录方法、路径、参数、Cookie、Header、请求体。
- 在 Repeater 或 curl 中重放,确认基线可复现。
- 单独修改一个参数或 Header。
- 对比状态码、响应长度、Header、响应体关键词和响应时间。
- 对 302、403、500、401 等响应单独保存。
- 根据变化归类到 SQL 注入、越权、XSS、命令注入、SSRF、请求走私等方向。
最小验证示例
保存基线:
curl -i 'http://target/profile?id=1' \
-H 'Cookie: session=abc'测试 Header 影响:
curl -i 'http://target/admin' \
-H 'Cookie: session=abc' \
-H 'X-Forwarded-For: 127.0.0.1'测试方法差异:
for m in GET POST PUT DELETE PATCH OPTIONS; do
echo "== $m =="
curl -i -X "$m" 'http://target/api/user/1' | head
done测试 JSON 和表单差异:
curl -i -X POST 'http://target/login' \
-H 'Content-Type: application/json' \
--data '{"username":"admin","password":"test"}'如果只改一个字段就稳定改变响应,说明这个字段参与了服务端逻辑。
常见利用 / 解题路线
路线总览:
路线一:请求差异定位漏洞
保存基线 -> 单字段修改 -> 对比响应 -> 归类漏洞 -> 深入对应文章路线二:Header 绕过
适合 IP、来源、浏览器、代理相关校验。
X-Forwarded-For / X-Real-IP / Referer / User-Agent -> 响应差异 -> 权限或路由变化路线三:方法绕过
适合 API 题或权限校验不一致。
GET 被拒 -> PUT/PATCH/POST 成功 -> 检查是否修改资源路线四:跳转和错误信息取证
适合登录、权限、框架报错题。
不自动跟随跳转 -> 保存 302 前响应 -> 分析 Set-Cookie/Location/响应体路线五:协议边界问题
如果前后端代理对请求边界解析不一致,进入 请求走私;如果跨源读取异常,进入 CORS基础。
常见失败原因
- 没有基线:不知道正常请求和异常请求差在哪里。
- 一次改太多字段:无法判断哪个字段导致响应变化。
- 忽略 302 前响应:线索常在跳转前的 Header 或响应体。
- 只看浏览器页面:浏览器会自动跟随跳转、执行 JS、合并 Cookie,掩盖原始响应。
- Content-Type 不匹配:JSON、表单、multipart 走的是不同解析路径。
- 缓存干扰:重复请求被 CDN 或浏览器缓存,响应不是后端实时结果。
- 代理改写请求:Burp、浏览器或中间代理可能自动调整 Header 和长度。
迷你案例
题目访问 /admin 返回 403。浏览器页面只有"Forbidden"。
第一步保存基线:
curl -i 'http://target/admin'响应:
HTTP/1.1 403 Forbidden
Server: nginx第二步测试来源 IP:
curl -i 'http://target/admin' -H 'X-Forwarded-For: 127.0.0.1'响应变成:
HTTP/1.1 200 OK
Set-Cookie: role=admin结论:
服务端把 X-Forwarded-For 当成真实来源 IP
本地来源可以访问 /admin
伪造 127.0.0.1 后拿到管理员 CookieWP 要贴两次请求和响应差异,而不是只写"加 Header 绕过"。