HTTP/3协议走私与通过QPACK值转换中CRLF实现头部注入

本文详细披露了在libcurl处理HTTP/3响应头时发现的一个关键设计缺陷。该漏洞源于libcurl将二进制QPACK头转换为内部使用的文本HTTP/1.1格式时,未能验证或清理从QUIC栈接收的头部值,导致CRLF注入攻击,可能引发协议走私、缓存投毒、会话固定和安全绕过。

HTTP/3协议走私与通过QPACK值转换中CRLF实现头部注入

一个基本的设计缺陷存在于libcurl处理所有受支持后端(ngtcp2, quiche, openssl-quic)的HTTP/3(QUIC)响应头的方式中。该漏洞根源于将二进制QPACK头(HTTP/3)不安全地转码为curl内部管道使用的文本HTTP/1.1格式。

具体来说,libcurl未能验证或清理从QUIC栈接收的头部值。如果恶意HTTP/3服务器发送包含回车(\r, 0x0D)和换行(\n, 0x0A)字符的头部值,libcurl会盲目地将它们连接进其内部缓冲区。然后,该缓冲区作为单个"头部行"向下游传递给客户端应用程序,而实际上该行包含了多个注入的头部,甚至是一个走私的响应体。

这造成了一个协议去同步化漏洞。虽然curl的内部状态机(cookies/HSTS)只解析第一行,但任何依赖libcurl获取内容的下游应用程序、代理或WAF都会将注入的有效负载处理为有效的HTTP头部或正文内容。这导致大规模的缓存投毒、会话固定和安全绕过场景。

技术分析(根本原因)

该漏洞存在于负责从底层QUIC库接收解码头部的回调函数中。

位置:

  • lib/vquic/curl_ngtcp2.c: 函数 cb_h3_recv_header
  • lib/vquic/curl_quiche.c: 函数 cb_each_header
  • lib/vquic/curl_osslq.c: 函数 cb_h3_recv_header

易受攻击的逻辑(示例来自curl_ngtcp2.c): 当nghttp3传递一个头部名称(h3name)和值(h3val)时,libcurl将文本行重建到 ctx->scratch中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1 /* curl_ngtcp2.c around line 1780 */
2 /* store as an HTTP1-style header */
3 curlx_dyn_reset(&ctx->scratch);
4 result = curlx_dyn_addn(&ctx->scratch, (const char *)h3name.base, h3name.len);
5 if(!result)
6   result = curlx_dyn_addn(&ctx->scratch, STRCONST(": "));
7 if(!result)
8   /* VULNERABILITY: h3val.base contains raw bytes from the network.
9      If it contains \r\n, they are appended directly. */
10   result = curlx_dyn_addn(&ctx->scratch, (const char *)h3val.base, h3val.len);
11 if(!result)
12   result = curlx_dyn_addn(&ctx->scratch, STRCONST("\r\n"));
13
14 /* The corrupted buffer is then passed to the write handler */
15 if(!result)
16   h3_xfer_write_resp_hd(cf, data, stream, curlx_dyn_ptr(&ctx->scratch), ...);

信任链失效:

  1. 传输: HTTP/3允许QPACK值中存在任何二进制序列(RFC 9114)。
  2. 转换: libcurl将其转换为HTTP/1.1风格的 “Name: Value\r\n”。
  3. 传递: Curl_client_write通过CURLOPT_HEADERFUNCTION将此原始缓冲区传递给应用程序。
  4. 利用: 应用程序收到 Name: Value\r\nInjected-Header: Evil\r\n。标准HTTP解析器将其读取为两个不同的头部。

受影响版本 当前master分支以及所有启用HTTP/3支持的版本。

重现步骤

为了演示此漏洞而无需您设置能够进行畸形QPACK编码的复杂自定义HTTP/3服务器,我提供了一个"模拟补丁"。此补丁修改了libcurl,以模拟从服务器接收恶意头部。这证明了如果服务器发送此类数据,libcurl未能过滤它。

  1. 构建支持HTTP/3的curl(例如,使用ngtcp2) 确保您已准备好构建环境。

  2. 应用模拟补丁 将以下差异应用于 lib/vquic/curl_ngtcp2.c。这将强制curl模拟一次攻击,其中服务器发送包含CRLF注入的Server头部。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    1 diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c
    2 index XXXXXXX..XXXXXXX 100644
    3 --- a/lib/vquic/curl_ngtcp2.c
    4 +++ b/lib/vquic/curl_ngtcp2.c
    5 @@ -1780,6 +1780,16 @@ static int cb_h3_recv_header(nghttp3_conn *conn, int64_t stream_id,
    6      result = curlx_dyn_addn(&ctx->scratch, STRCONST(": "));
    7      if(!result)
    8        result = curlx_dyn_addn(&ctx->scratch,
    9                                (const char *)h3val.base, h3val.len);
    10 +
    11 +    /* POC SIMULATION: If the server sends a "server" header,
    12 +       we simulate a malicious CRLF injection appended to it. */
    13 +    if(h3name.len == 6 && !strncmp((char*)h3name.base, "server", 6)) {
    14 +        const char *injection = "\r\nSet-Cookie: session=HACKED_BY_CRLF";
    15 +        result = curlx_dyn_addn(&ctx->scratch, injection, strlen(injection));
    16 +    }
    17 +
    18      if(!result)
    19        result = curlx_dyn_addn(&ctx->scratch, STRCONST("\r\n"));
    20      if(!result)
    
  3. 重新编译curl

    1
    
    1 make
    
  4. 运行利用程序 针对任何有效的HTTP/3服务器运行curl(例如,google.com 或 cloudflare-quic.com)。该补丁将模拟来自该服务器的恶意有效负载。

    1
    
    1 ./src/curl --http3 -v -I https://www.google.com/
    
  5. 观察关键输出 查看接收到的头部。您将看到:

    1
    2
    3
    4
    5
    
    1 HTTP/3 200
    2 ...
    3 server: gws
    4 Set-Cookie: session=HACKED_BY_CRLF
    5 ...
    

    分析: Set-Cookie 头部出现在新的一行。对于任何下游解析器(包括curl自身的 -I 输出显示,以及任何使用libcurl的应用程序),这都是一个有效的、独立的cookie。curl中的逻辑未能检测到该"头部"实际上是服务器头部值的一部分。

支持材料/参考文献

  • RFC 9114 (HTTP/3): 定义字段值是字节序列,将清理的负担放在转换为文本的实现上。
  • RFC 7230 (HTTP/1.1): 严格禁止头部值中出现CR/LF,以防止拆分。

影响

该漏洞对使用支持HTTP/3的libcurl的应用程序生态系统具有关键影响。

  • WAF 和 网关绕过: 使用libcurl检查流量的安全网关可能被绕过。攻击者可以在良性头部后面隐藏恶意头部(如 Content-Security-Policy: unsafe-inlineTransfer-Encoding: chunked)。网关验证良性头部,但其后的浏览器/客户端会处理注入的恶意头部。
  • 大规模缓存投毒: 通过注入 Transfer-EncodingContent-Length,攻击者可以使连接去同步化(请求走私)。这允许他们毒化为数千用户服务的反向代理的缓存,为合法URL提供恶意内容。
  • 会话固定/劫持: 如PoC中所演示,攻击者可以注入 Set-Cookie 头部。即使应用程序逻辑尝试过滤头部,它也很可能将注入的行处理为有效的新头部,从而允许在客户端进行会话固定攻击。
  • 污染curl_easy_header API: 使用结构化头部API将头部从一个请求复制到另一个请求的应用程序,将在不知情的情况下传播恶意有效负载,将攻击扩散到更深的内部网络。

这是一个经典的HTTP协议走私向量,由库在跨协议数据转换时未能清理数据而促成。

该漏洞允许在库级别进行HTTP协议走私。

  • 缓存投毒: 如果libcurl在反向代理或网关中使用,注入的头部(如 Transfer-EncodingContent-Length)可以使连接去同步化,导致缓存为后续用户提供恶意内容。
  • 会话固定: 攻击者可以强制在客户端应用程序上设置 Set-Cookie 头部,即使应用程序逻辑尝试根据键过滤头部。
  • 安全绕过: 使用libcurl检查头部的WAF或安全扫描器可能被隐藏在良性头部后面的恶意有效负载蒙蔽或欺骗。

讨论与验证过程

漏洞报告者的跟进: 报告者(0x0000nosfu)在收到curl团队要求提供真实服务器复现的反馈后,编写了一个独立的恶意HTTP/3服务器(使用Python和aioquic),并成功演示了在未修改的curl客户端上的CRLF头部注入。

关键复现细节:

  1. 报告者确认漏洞在curl的quiche后端上可复现。
  2. 他提供了一个 exploit_server.py 脚本,该服务器发送包含 \r\n 序列的恶意头部值 x-custom: foo\r\nSet-Cookie: session=HACKED_BY_CRLF
  3. 使用quiche后端构建的curl在连接到此服务器时,输出中错误地将 Set-Cookie: session=HACKED_BY_CRLF 解析为一个独立的HTTP头部,而不是 x-custom 头部值的一部分。

curl团队的回应:

  • 团队确认了quiche后端存在此漏洞。
  • 然而,他们指出quiche后端被声明和记录为实验性功能,不鼓励在生产中使用,因此其中的错误不被视为安全漏洞。
  • 团队为quiche后端提供了修复补丁(#20101)。
  • 对于ngtcp2和openssl-quic后端,团队的多名成员无法复现该问题,表明这些后端可能具有不同的行为或已包含某些防护。
  • 报告被标记为Informative(信息性),并根据项目透明政策被公开披露。

结论: 此报告详细描述了一个影响libcurl实验性quiche HTTP/3后端的CRLF注入漏洞。它突显了在不同HTTP/3实现(ngtcp2 vs. quiche)中进行协议转换时,严格验证和清理网络数据的必要性。虽然该漏洞的完整利用需要特定配置(使用quiche后端),但它展示了HTTP/3协议处理中一个潜在的危险模式。curl团队已针对quiche后端进行了修复,并强调了实验性功能的风险。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计