HAProxy中的严重漏洞(CVE-2021-40346): Integer Overflow启用HTTP走私

HAProxy脆弱性

JFrog安全研究团队不断在流行的开源项目中寻找新的和以前未知的漏洞,以帮助改善他们的安全状况。作为这项工作的一部分,我们最近发现了一个潜在的关键漏洞HAProxy一个广泛使用的开源负载平衡代理服务器,特别适用于非常高流量的网站,并被许多领先的公司使用。它也随大多数主流Linux发行版一起提供,并且通常默认部署在云平台中。JFrog Security负责任地披露了这个漏洞,并与HAProxy的维护者一起验证修复。

的弱点,cve - 2021 - 40346是一个整数溢出漏洞,可以进行HTTP请求走私攻击,CVSSv3得分为8.6。这种攻击允许攻击者在代理服务器不知道的情况下将HTTP请求“偷运”到后端服务器。走私的请求有不同的影响,取决于HAProxy的配置和后端web服务器的配置:

  • 绕过安全控制,包括在HAProxy中定义的任何acl
  • 获得对敏感数据的未经授权的访问
  • 执行未经授权的命令或修改数据
  • 劫持用户会话
  • 在没有用户交互的情况下利用反射的XSS漏洞

更多的

此漏洞已在HAProxy的2.0.25、2.2.17、2.3.14和2.4.4版本中修复修复和解决方案如果您正在使用HAProxy,但无法升级到任何新版本,请参阅底部的解决方案。

由于这个漏洞有点复杂,我们将从技术背景开始,然后深入到技术细节。

技术背景

如前所述,整数溢出漏洞可能导致HTTP请求走私攻击(HRS)。我们先简单介绍一下:

  • HTTP请求走私
  • HAProxy的HTTP请求处理阶段(简化)

HTTP请求走私

HTTP请求走私是一种攻击技术出现在2005年。它基于干扰前端服务器(即HAProxy)和后端服务器之间的HTTP请求处理。攻击者通常通过发送特制的请求来利用此技术,该请求在其请求体中包含额外的请求。在成功的攻击中,内部请求通过前端偷运(前端只将其视为请求的主体),但后端将其作为正常请求使用。

HTTP请求走私

在大多数情况下走私技术是通过在同一请求中提供长度相矛盾的Content-Length和Transfer-Encoding头,并旨在解析前端和后端服务器之间的不一致性来实现的。然而,在我们的案例中,攻击是通过利用整数溢出漏洞实现的,该漏洞允许在解析HTTP请求时(特别是在处理Content-Length报头的逻辑中)在HAProxy中达到意想不到的状态。

使这类攻击成为可能的一个重要条件是,当前端服务器将HTTP请求转发给后端时,它使用相同的已建立的TCP连接,而不是浪费时间打开和关闭套接字。请求被来回发送,由后端服务器决定请求在哪里结束,下一个请求在哪里开始。

HAProxy的HTTP请求处理阶段(简化)

HAProxy负载均衡器最基本的功能是代理从客户端到达某个后端服务器的HTTP请求。HTTP请求处理逻辑可以简化为两个阶段——初始解析和进一步处理(简化,重点放在Content-Length报头上):

阶段1:HTTP请求的初始最小解析:

  1. 找到一个Content-Length报头,它的长度值被保存在一边。这个大小决定了从客户端读取并发送到后端请求体的长度。
  2. 如果遇到额外的内容长度标头-如果它们有不同的值,请求是下降了。否则,它们将被忽略。
  3. 整个请求被解析成一个内部表示——存储为一个html块结构数组(一个块)对于每个标头(如请求主体等)将在阶段2中处理。

阶段2:主要处理请求

  1. 代码遍历html块数组以处理请求并准备将被转发到后端的请求
  2. 当遇到第一个Content-Length头块时,代码取其值字符串用于转发请求的内容长度报头字符串。
  3. 进一步的内容长度标头遇到的被忽略(跳过)

注意,阶段1确保Content-Length值是一致的(以后只传递一个报头和一个值)。

攻击场景—绕过http-request访问控制列表

这个场景演示了触发HTTP请求走私攻击来绕过HAProxy定义的ACL规则。

首先,我们考虑一个样例HAProxy实例,它拒绝对以开头的路径的请求/管理/或者包含HTTP报头美国广播公司与价值xyz

Http-request deny if {path_beg /admin/} Http-request deny if {req.hdr(abc) -m STR xyz}

攻击场景—绕过http-request访问控制列表

接下来,我们发送以下特制的消息给HAProxy:

POST /index.html HTTP/1.1主机:abc.com Content-Length: 60 GET /admin/add_user.py HTTP/1.1主机:abc.com abc: xyz .html Content-Length: 60

HAProxy内部发生了什么

让我们关注一下Content-Length0aaa……头。在解析的第1阶段,它被视为任何其他简单的标头,只是存储在它的html块结构中。该结构将报头名称长度编码为仅8位(应该小于256个字符),因此由于此报头名称为270字节,因此会导致unsigned整数溢出和它的名称长度保存在html块中为14(270模256)。其值长度存储为1-它应该是0(':'后面没有字符),但是从名称字段溢出的位流向值长度并将值长度设置为1。

然后,HAProxy看到特殊的报头Content-Length,其值为60,并将其用作主体长度(从数据包中读取剩余的60个字节,然后将它们发送到后端)。它读取和存储主体并完成阶段1。

然后,在阶段2 -迭代html块数组时,它遇到Content-Length0aaa…标头,读取14用于获取名称的字符并将其视为合法的Content-Length标头!它读取它的值(该值应该在名称之后开始,长度为1),即0,从而添加字符串内容长度:0在创建要转发的请求时。

接下来,它遇到Content-Length: 60报头,但忽略它(根据原始逻辑)。

结果请求将被发送到后端如下:

POST /index.html HTTP/1.1 host: abc.com content-length: 0 x-forwarded-for: 192.168.188.1 GET /admin/add_user.py HTTP/1.1 host: abc.com abc: xyz .html

接收到请求后,后端服务器正确地将POST请求解析为没有主体。然后,它期望下一个请求到达同一连接,从而处理得到以前被认为是POST的机构作为新的合法HTTP请求!如上所示,这个新请求绕过了HAProxy的ACL过滤,并由后端成功解析。

获取走私请求的HTTP响应

如上面的场景所述,HAProxy只知道正在转发的单个HTTP请求,因此只从后端服务器返回一个HTTP响应(第一个)给客户端。

如果我们也对接收走私请求的HTTP响应感兴趣,我们可以通过发送两个连续的请求来实现:

  • 第一个请求与前面的特制HTTP请求非常相似,但是会在后端服务器的输入缓冲区中留下一个未完成的请求,导致后端服务器在处理走私请求之前等待更多的输入。请求将未完成,因为我们不会使用标记HTTP GET请求头结束的双CRLF来结束它。(在走私POST请求的情况下,提供比走私请求的Content-Length短的正文)。此外,我们以不带换行符(CRLF)的部分报头行(DUMMY:)结束此请求,并且还从走私请求中省略了Host报头—这些将在本节末尾进行解释。
    POST /index.html HTTP/1.1主机:example.com Content-Length: 39 GET /admin/secret.html HTTP/1.1 DUMMY:长度:39
  • 第二个请求将是到达相同后端的简单合法GET请求。后端服务器将把此请求视为先前部分走私请求的延续。
    HTTP/1.1主机:example.com
  • 第二个请求将连接到第一个走私请求,它的双CRLF将导致走私请求的完成。这将导致走私的请求由后端处理,并返回将返回给客户端的HTTP响应。完整的走私请求将在后端服务器中看起来如下:
    GET /admin/secret.html HTTP/1.1 DUMMY:GET /index.html HTTP/1.1 Host: example.com

包含部分标头的原因假:Line用于保持请求有效。后端不需要HTTP请求行(例如:GET /index.html HTTP/1.1)内的标题部分。部分报头导致请求起始行被解析为DUMMY报头的值,从而使请求有效。从走私请求中省略主机头是为了避免完整走私请求的重复主机头-因为第二个请求已经有一个主机头。

攻击演示—ACL bypass

CVE_2021_40346_Demo

漏洞细节

关于CVE-2021-40346的其他详细信息补充了上面的解释

发生无符号整数溢出漏洞在排队的html块信息设置标题的名称和值长度htx_add_header函数,作为初始解析的一部分被调用:

block ->info += (value。Len << 8) + name.len;

为了解释这个函数和bug,理解块信息字段的工作方式是很重要的。这个信息来自htx-api文档抄在这里:

*块的信息表示:0 b 0000 0000 0000 0000 0000 0000 0000 0000  ---- ------------------------ --------- 类型的值(1 MB max)名称长度(头/拖车- 256 b马克斯 ) ---------------------------------- 数据长度(256 MB max)(身体、方法、路径、版本、状态、原因)支持类型:- 0000(0):请求起跑线上- 0001(1):响应起跑线上- 0010(2):头块- 0011 (3):end-of-headers标记……

htx_add_header函数的工作原理如下:

它接收表示报头名称和头值的字符串作为参数。它首先创建一个头类型为(0010 binary)的新块。然后,它将报头名称的长度和报头值的长度左移8位的总和添加到info字段。这符合上面的信息表示。然后,将头的名称以小写形式复制到块的有效负载中,紧接着,将值复制到块中。例如,header " Myheader: Myvalue "将表示如下:

Block ->info: (2 << 28) + (7<< 8) +(= = 0 x20000708数据块/载荷:到了头括号

如上面的演示所示,由于没有检查报头名称的长度,因此有可能传递一个名称超过最大255字节的报头,以溢出报头名称字段,并使阶段2处理看到与阶段1解析不同的报头名称。

自动发现

自动发现这种和类似的整数溢出漏洞的方法可能包括搜索一个模式,其中一个变量向左移动,然后将另一个变量添加到结果中,如下面的模式所示,并应用下面的条件。

结果= (a << shift_amount) + b

附加条件:添加的变量(B)从用户输入到达,它的大小不限于小于SHIFT_AMOUNT位所能容纳的,并且它的类型大于左移所创建的空间(比SHIFT_AMOUNT多位)。

与符合上述条件的其他类似发现相比,该代码出现在HTTP解析等有趣的领域可能有助于突出它。作为研究工作的一部分,JFrog利用其安全研究团队发现的漏洞来增强其即将到来的自动零日检测功能,包括这种类型的漏洞,其中整数溢出会导致逻辑错误,而不仅仅是内存损坏。

修复和解决方案

作为HAProxy顾问,最好的解决方案是升级到HAProxy版本2.0.25, 2.2.17, 2.3.14或2.4.4,它通过添加名称和值长度的大小检查彻底解决了这个问题。

如果升级是不可能的,添加以下行到HAProxy的配置应该减轻这种攻击的所有变体,我们遇到过:

Http-request拒绝{req.hdr_cnt(content-length) gt 1} http-response拒绝{res.hdr_cnt(content-length) gt 1}

上下文CVE扫描可以在扫描上传的工件时自动识别是否存在此类缓解措施,并相应地通知用户。

如果不确定使用的是哪个版本,请考虑使用SCA工具,例如x光以确定版本以及您的工件是否受到影响。

确认

我们要感谢HAProxy首席技术官Willy Tarreau和HAProxy安全团队及时、专业地处理了这个问题。