该工具用于检测和利用 HTTP 请求走私,场景是通过前端服务器进行 HTTP/2 -> HTTP/1.1 转换时可能实现。
其方案如下:
攻击者希望找到这样的请求,使得后端服务器将其视为两个独立的请求。
如果前端<->后端 HTTP/1.1 连接使用 keep-alive,则前端可能会将其他用户的请求发送到同一连接。如果我们能够通过在合法请求之后的部分请求来“毒化”连接,就可以检索到其他用户的请求。
其他可能的情况包括绕过前端服务器保护和重写、缓存投毒或缓存欺骗。
有关 HTTP 请求走私的更多信息,请参阅 Portswigger Web 安全学院。
在 HTTP/2 中,所有 HTTP 头部的名称和值都是二进制。这意味着从技术上讲,它们可以包含额外的空格甚至换行符。
RFC7540#10.3 规定,将 HTTP/2 请求转换为 HTTP/1 的实现必须注意此类转换所导致的字符集限制;大多数实现确实拒绝了这些字符。尽管如此,我们希望能找到允许此类头部的实现。它们将破坏向后端转换后的 HTTP/1.1 请求。
另一点是,一些近期修复的与 HTTP 请求走私相关的问题可能仅针对 HTTP/1.1 解析器。
总的来说,我们希望存在一些 HTTP/2 实现,它们对 HTTP/1.1 中关于 HTTP 请求走私的最新研究不太了解,因此未包含相应的缓解措施。
令人惊讶的是,是的!
我发现了一种通过在 Cloudflare 中走私包含空格字符的头部的可能性,从而为 Cloudflare<->客户端走私打开了大门(前提是 Cloudflare 客户的软件接受并修剪头部名称)。这是 博客文章。
还有另一个尚未公开的漏洞赏金报告。它利用了自定义软件未过滤 HTTP/2 头部中的换行符这一事实,并且走私 100% 发生(我可以看到其他用户的请求)。
然而,我理解这类漏洞必然非常罕见:与 HTTP/1.1 不同,HTTP/2 的实现并不多,而且大多数实现都考虑到了安全性,从而拒绝可疑或无效的头部。
该工具有一个子命令,尝试自动检测目标是否容易受到 HTTP 请求走私攻击。本节描述了该功能背后的算法。
要执行 HTTP 请求走私攻击,我们实际上需要首先“走私”一个头部(Content-Length 或 Transfer-Encoding)。这意味着我们需要发送一个头部,它 a) 控制请求主体的结束位置,并且 b) 不被前端处理,但被后端处理。
通常通过以某种方式修改头部来实现:在名称末尾添加空格或制表符、将值替换为半等效值等。
漏洞检测算法的基本思想是检测服务器是否实际处理了走私的头部,就像它是 Content-Length 或 Transfer-Encoding 一样。我们通过发送多个请求来实现:一些请求带有头部的有效值,另一些带有无效值。然后尝试检测是否存在一种方法能够区分这两组请求的响应。
这就是为什么工具输出不包含“易受攻击/不易受攻击”这样的词:它只是说能否区分来自这两组请求的响应。
如果满足以下两个条件中的至少一个,则工具认为两个 HTTP 响应集是可区分的:
超时被视为一个唯一的、不等于其他任何状态码的值;因此,该工具取代了经典的“通过时间检测”方案。
让我们考虑一个例子。假设我们尝试通过用下划线替换连字符来走私 transfer-encoding 头部。
如果发现每次发送 transfer_encoding:zalupa 时服务器都以状态 400 响应,而发送 transfer_encoding:chunked 时挂起,那么我们可以说服务器可能确实将该头部作为传输编码的值进行处理。理论上,这可能是前端或后端服务器。
第一种情况不感兴趣,因为无论如何我们都可以发送非走私版本的头部;第二种情况正是我们要找的。由于一切都在 HTTP/2 上进行,第一种情况大多可以避免:HTTP/2 服务器以另一种方式确定请求主体的结束,这种方式与请求头部无关,并且从不期望主体采用 HTTP/1.1 的块格式(带有十六进制块长度的那种)。
具体检测技术的变化包括:
我们发送走私版本的 Transfer-Encoding: chunked(例如 transfer_encoding:chunked)以及不同的主体:有效的是 0\r\n\r\n,无效的是 999\r\n。
如果响应不同,我们可以确定后端服务器接收并处理了走私的头部。前端没有理由这样做:HTTP/2 不使用我们发送的块格式,因此它将是无效的。
我们期望后端对于无效请求挂起(即请求超时),因为它等待更多数据到达。
这是最可靠的检测变体:如果服务器在读取主体时挂起,可能出现了问题,因为 HTTP/2 请求中没有 HTTP/1.1 传输编码的用途。
我们再次发送走私版本的 Transfer-Encoding 以及不同的主体:0\r\n\r\n 作为有效主体,X\r\n\r\n 作为无效主体。
情况与上述相同,但我们期望后端至少会验证主体,而不是读取它。
我们发送走私版本的 Content-Length 头部,其值为 1 和 -1。
从前端的角度来看,这两个值都是无效的:HTTP/2 中有另一种确定主体长度的机制,并且在两种情况下实际上都没有发送请求主体。如果响应不同,我们推测是后端服务器解析了头部。
这种方法最不可靠:当 Content-Length 具有无效值且与实际长度不匹配时,前端可能发出不同的错误。
通过发送多对有效/无效请求,我们可以减少随机误报的机会。另一方面,如果我们看到无法将“有效”请求的响应与“无效”请求的响应分开,我们可以提前停止。
该工具尝试通过以各种方式修改头部来使用多种走私技术。这些技术都不是新的,同时也不明显。
为了走私一个头部,我们在其后追加一个空格。这是最常见和经典的方法。我们希望头部不被前端处理,而是原样发送到后端,后端会剥离空格。
该工具尝试各种字符作为空格:包括 、\t、\v、\x00 以及 Unicode 空格。
为了走私一个头部,我们将连字符(-)替换为下划线(_)。如果后端以某种方式受到 CGI 启发,它可能会将类似 Header-Name 的头部转换为 HEADER_NAME 形式;因此,连字符无论如何都会变成下划线。在确定如何解析主体时,这样的后端大概会从其头部分字典中请求 CONTENT_LENGTH / TRANSFER_ENCODING 的值,并且该值将在那里。
这个是 HTTP/2 特有的。由于 HTTP/2 是二进制协议,我们可以尝试在头部名称或值中发送换行符。标准禁止这样做,但我们希望找到仍然接受它的实现。
在 HTTP/2 -> HTTP/1.1 转换期间,头部会分成两个不同的头部,这意味着请求对于后端来说看起来会不同。
为了走私一个头部,我们在换行符后放置其名称和值:名称为“fake”、值为“fake\r\ntransfer-encoding: chunked”的头部变成了一个带有名称“Transfer-Encoding”和值“chunked”的头部。
假设后端使用某种高级语言,并且没有进行足够的头部验证。在这种情况下,它可能会在进行其他操作之前将名称转换为大写,并使用 Unicode 感知函数来执行此操作。幸运的是,TRANSFER-ENCODING 包含字母 S,而 S 是 ſ(\u017f)的大写形式。
类似地,我们可以搜索一个将 Transfer-Encoding 的值转换为小写的后端:我们发送 chunKed 而不是 chunked,并且使用 \u212a 代替 K。
当然,前提是前端将 UTF-8 头部名称/值传递给了后端。
要安装该工具,运行 go install github.com/neex/http2smugl@latest。
该工具包含两个子命令:request 和 detect。第一个仅用于构造 HTTP/2 请求:大多数客户端工具不接受无效头部,因此有一个按原样将用户输入发送到服务器的工具很方便。
另一个是 detect。它尝试各种 HTTP 请求走私技术来检测目标是否容易受到攻击。检测算法很复杂;如果您通读它并向发送评论,我将不胜感激。它在下面的相应部分中描述。
http2smugl request使用此子命令发送一个(可能有点畸形的)HTTP/2 请求。第一个参数是 URL,其他参数只是 name:value 格式的头部(注意冒号后没有空格)。支持反斜杠转义:您可以使用 \r、\n 和 \xXX 转义码。例如,要发送包含冒号的头部名称,请使用 \x3a(例如 name\x3awith\x3acolons:value)。
http2smugl detect此子命令尝试使用各种技术检测 HTTP 请求走私。要使用它,只需运行 http2smugl detect [HTTPS URL]。
该命令仅在可以检测到服务器解析了走私头部时输出内容。要理解其含义,请阅读相应部分。
已实现对 HTTP/3(quic)的实验性支持。但是,我不建议使用它,因为我没有发现任何与 HTTP/3 相关的漏洞。
要在 request 子命令中使用 HTTP/3,请在 URL 中提供 https+h3:// 协议而不是仅 https。detect 命令也支持此功能。
request 子命令还有一个 --try-http3 标志,当 URL 中未指定协议(仅主机名)时,该标志会更改行为。如果存在该标志,命令将尝试针对命令行或目标文件中的此类条目使用 https+h3 协议(以及 HTTP/2)。例如,http2smugl detect --try-http3 www.example.com 将同时尝试 HTTP/3 和 HTTP/2,但 http2smugl detect --try-http3 https://www.example.com/ 仍然只尝试 HTTP/2。
在本节中,我描述了一些情况,其中工具说响应是“可区分的”,但漏洞可能不存在。
Amazon 的 Elastic Load Balancer 实现了 HTTP 请求走私的多种缓解措施。虽然并非所有措施都默认拒绝请求,但 ELB 在发送带有“可疑”头部的请求后不会重用连接。因此,实际攻击是不可能的。
要检测您正在处理 ELB,您可以使用 Server 响应头。如果它被过滤掉,您可以发送一个带有 content__length 头部(注意两个下划线)且值为 -1 的请求:如果返回 400,则很可能是 ELB 或其他 WAF(见下文)。
Apache Traffic Server 主要在 Yahoo 使用。它以不寻常的方式处理 HTTP/2:它将其转换为内存中的 HTTP/1.1,然后重新解析生成的请求。因此,虽然头部在技术上可以“走私”,但无法导致漏洞:您可以将相同的字节发送到 HTTP/1.1 连接。
检测您正在处理 ATS 的最简单方法(除了 Server 头部)是发送 TRACE 请求。如果请求中存在 Max-Forwards: 0 头部,ATS 将默认返回对 TRACE 请求的响应,而无需将其转发到后端。
Microsoft IIS 似乎支持在 HTTP/2 主体内解码分块编码。这是一种奇怪的行为;然而,从安全角度来看,它是无害的。
要检测您正在处理 IIS,您可以发送一个带有“transfer-encoding:chunked”头部和不正确分块主体的请求。如果您在 Server 头部中看到 Microsoft-HTTPAPI 或类似内容,那么就是它了。
WAF 会尝试检测可疑头部——这是它们的工作。有时这会导致工具说“可区分”:WAF 可能会阻止像 content_length:-1 这样的请求,而允许 content_length:1,仅仅因为其过滤器偶然做出了这些决定。
如果您在 Server 头部中看到 WAF,则很可能是误报。
如果您对此主题有任何想法,请通过 Twitter @emil_lerner 或 Telegram @neexemil 联系我——或者在 GitHub 上提交 issue。