• 命令行工具cURL 8.4.0 已正式发布 修复了高危安全漏洞CVE-2023-38545
  • 发布于 2个月前
  • 367 热度
    0 评论
命令行工具 cURL 8.4.0 已正式发布了,该版本修复了高危安全漏洞(CVE-2023-38545)——SOCKS5 堆溢出漏洞。该漏洞导致 cURL 在 SOCKS5 代理握手过程中溢出基于堆的缓冲区,创始人 Daniel Stenberg 称其严重程度为 “HIGH”。这可能是目前遇到的最严重的 cURL 安全问题,建议用户和开发者立即更新。

让我们一起来看看 Daniel Stenberg 关于这个漏洞的介绍。

背景
从 2002 年 8 月开始,cURL 便支持 SOCKS5 了。SOCKS5 是一种代理协议,是一个通过特定 “中间者” 实现网络通信的简洁协议。此协议经常被用于通过 Tor 网络进行通讯,也常被组织和公司内部用于上网。SOCKS5 提供了两种不同的主机名解析方式。一种是客户端在本地完成域名解析并将已解析的地址传给代理,另一种是客户端将整个域名传递给代理,让代理在远端完成解析。

2020 年初,我开始处理一个 cURL 中长期悬而未决的问题:将连接 SOCKS5 代理的功能从同步调用转化为异步状态机。当一个应用需要并行处理大量经由 SOCKS5 的传输时,这种改变变得尤为重要。

2020 年 2 月 14 日,我向主分支提交了这一重大改进。并在 7.69.0 版本中正式发布,这也是首次带有这项增强功能的版本。但遗憾的是,这也使其成为首个受到 CVE-2023-38545 漏洞威胁的版本。

一个不太明智的选择
当网络数据准备好并需要处理时,这个状态机会被连续调用,直至任务完成 —— 也就是连接建立。

在函数开头,我写了如下代码:
bool socks5_resolve_local =
  (proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;
此布尔变量用来决定是否由 cURL 解析主机名,或是简单地将名字传递给代理。这个赋值每次状态机运行时都会执行。
状态机首先进入 INIT 状态,这次我们要讲述的主要问题也正是出现在这里。这个问题实际上是从之前的函数中继承过来的,当时它还没有被转化为一个状态机。
if(!socks5_resolve_local && hostname_len > 255) {
  socks5_resolve_local = TRUE;
}

SOCKS5 规定主机名字段的最大长度为 255B,这意味着 SOCKS5 代理不能解析长度超出这个限制的主机名。当 cURL 检测到过长的主机名时,它做出了一个不理想的决策,即转而选择在本地进行解析,因此将相应的变量设置为了 TRUE。


这段代码实际上是从之前版本遗留下来的。我认为这种模式转换的方式是不对的,因为如果用户选择了代理进行远程解析,cURL 应当严格执行或者直接失败,而不是简单地更改解析模式,即使在看似 “正常” 的情境中,这样的切换也可能不会正常工作。
之后,状态机就会更改其状态并继续执行。

问题如何产生
如果状态机由于缺乏数据无法继续运行,例如 SOCKS5 服务器响应缓慢,那么它会先停止。只有当有新的数据可以处理时,它才会重新开始工作。但,问题出在 socks5_resolve_local 这个本地变量上。每次状态机启动时,这个变量都会被重新设置,而它并不会记住之前因为主机名过长而进行的更改。这意味着它会再次尝试让代理远程解析这个过长的名字。

cURL 试图在内存中为这个过长的主机名创建一个协议帧,然后将这个名字复制到这个帧中。但由于主机名过长,复制操作导致了缓冲区溢出。具体溢出的情况取决于主机名的实际长度和缓冲区的大小。

目标缓冲区
cURL 创建协议帧的内存区域与其下载缓冲区是同一个。传输开始前,这个下载缓冲区会被重新用于创建协议帧。默认情况下,这个缓冲区大小是 16kB,但也可以根据应用的需求进行调整。例如,cURL 工具的缓冲区大小被设置为 100kB,而最小允许的缓冲区大小是 1024B。

如果设置的缓冲区小于 65541B,就有可能会发生上述的溢出。缓冲区越小,溢出的风险就越大。

主机名的长度
URL 的主机名理论上没有大小限制。但 libcurl 不允许处理超过 65535B 的主机名。而 DNS 系统最多只接受 253B 的主机名。因此,超过 253B 的主机名是非常罕见的。实际上,超过 1024 字节的主机名几乎是不可能的。

要触发这个问题,一般需要一个恶意用户输入一个特别长的主机名,从而利用这个缺陷进行攻击。为了能在内存中产生溢出,这个主机名的长度需要超过目标缓冲区的大小。

主机名内容问题
在 URL 中,主机名部分只能使用特定的字节集合。有些字节是明显无效的,这会让 URL 解析器直接拒绝接受。如果 libcurl 使用的是 IDN 库,那么该库也可能因为无效的主机名拒绝它。所以,只有在主机名使用了恰当的字节集时,这个缺陷才会被触发。

如何被攻击
如果攻击者控制一个 HTTPS 服务器,而一个利用 libcurl 的客户端正通过一个 SOCKS5 代理访问它,那么攻击者可以通过 HTTP 30x 反馈给应用一个特制的重定向命令。

这种 30x 的重定向会带有一个格式如下的 “Location” 头信息:
Location: 
https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/
这里,主机名的长度超出了 16kB,最大可达 64kB。
如果这个使用 libcurl 的客户端启动了自动跟随重定向的功能,并且 SOCKS5 代理的响应 “足够慢”,这就可能让该客户端把超长的主机名复制到了一个较小的缓冲区内,进而影响到相邻的堆内存。

这样,堆内存的缓冲区就发生了溢出。

如何修复
cURL 不应该因为主机名太长而从代理的远程解析改为本地解析。正确的做法应该是直接返回一个错误提示。从 cURL 8.4.0 版本开始,它确实已经这样做了。

链接:https://daniel.haxx.se/blog/2023/10/11/curl-8-4-0/
现在,我们还专门为这个问题增加了一个测试场景。

鸣谢
此问题由 Jay Satiro 报告,并由他进行分析和修复。这是至今 cURL 项目支付的最高赏金:4,660 美元。另外,根据 IBB 的规定,cURL 项目还收到了 1,165 美元。

重写代码?
如果 cURL 不是用 C 语言,而是用一种内存安全的语言写的,这系列的缺陷就不会存在了。但将 cURL 改用其他语言重写并不在我们的计划中。我猜测,这个漏洞的新闻会引发对此的新一轮质疑,我也有些无奈,只能再次尝试解释这个决定了。我认为,唯一可能并且明智的做法是:

允许、采纳并支持那些用内存安全语言编写的依赖。
慢慢地,逐步替换 cURL 的某些部分,比如通过引入 hyper 这样的工具。
不过,这样的开发现在几乎停滞不前,并清晰地展示了其中的挑战。未来相当长的一段时间内,cURL 仍将使用 C 语言编写。

对于那些对此表示不满的人,我诚挚地邀请他们参与进来,一起努力。
用户评论