sslh:远程拒绝服务漏洞
#remote #CVE #DoS目录
1) 引言
sslh 是一种协议复用器,它允许在同一个网络端口上提供不同类型的服务。为了实现这一点,sslh 对连接到达的初始网络数据进行启发式分析,并将所有后续流量转发到本地系统上匹配的服务。一个典型的用例是在端口 443 上同时提供 SSL 和 SSH 连接(因此得名),以适应公司防火墙的限制。
2025 年 4 月,我们对 sslh 进行了审查,主要是因为它处理各种网络协议,并且是用 C 语言实现的,而 C 语言以容易出现内存处理错误而闻名。在此次审查中,我们查看了 sslh 的 v2.2.1 版本。本文报告中描述的问题的错误修复可以在 v2.2.4 版本中找到。
下一节 提供了 sslh 实现的概述。 第 3 节 描述了我们在审查期间发现的两个与安全相关的拒绝服务问题。 第 4 节 讨论了我们在审查期间收集到的一些与安全无关的发现和注意事项。 第 5 节 探讨了 sslh 对高网络负载攻击的总体弹性。 第 6 节 总结了我们对 sslh 的评估。
2) sslh 概述
sslh 实现所谓的 探测,用于在新的 TCP 或 UDP 会话启动时确定服务类型。这些探测会检查传入数据的前几个字节,直到可以做出肯定或否定的决定。一旦确定了特定的服务类型,所有后续流量都会被转发到本地运行的专用服务,而无需进一步解释数据。 sslh 只会探测 已积极配置 的协议,除非需要,否则不会调用其他探测。
sslh 支持三种不同的 I/O 模型来处理网络输入。使用哪种模型取决于编译时,因此可能存在多个 sslh 二进制文件,每个 I/O 类型一个。存在以下模型:
- 一种 fork 模型,在
sslh-fork.c中实现。在此模型中,为每个新传入的 TCP 连接创建一个单独的进程。fork 进程获得 TCP 连接的所有权,处理相关的 I/O,并在连接结束时退出。此模型不支持 UDP 协议。 - 一种 select 模型,在
sslh-select.c中实现。在此模型中,使用select()系统调用在单个进程中监视文件描述符。此模型还支持 UDP 协议:为此目的,来自同一源地址的所有数据被视为同一会话的一部分。为sslh检测到的每个新会话创建一个专用套接字。 - 基于 libev 的实现,在
sslh-ev.c中实现。此变体将 I/O 管理细节外包给第三方库。这同样以与前面描述的 select 模型类似的方式支持 UDP 协议。
我们在审查中重点关注了 sslh 中实现的各种探测。 sslh 以较低的权限运行,并启用了 systemd 加固,因此权限提升攻击向量的影响有限。尽管有这些保护,拒绝服务仍然是一个重要领域,我们也对此进行了研究。
3) 安全问题
3.1) 文件描述符耗尽触发段错误 (CVE-2025-46807)
作为我们对拒绝服务攻击向量调查的一部分,我们研究了当大量连接创建到 sslh 时会发生什么,以及因此导致文件描述符耗尽。虽然 sslh-fork 变体在处理文件描述符耗尽方面做得很好,但其他两个变体在该领域存在问题。这尤其会影响需要应用程序级别跟踪的 UDP 连接,因为在协议级别上没有连接的概念。
对于每个连接,sslh 会维护一个超时时间,在此之后如果服务类型未能确定,则终止连接。sslh-select 实现仅在有网络活动时检查 UDP 超时,否则为每个 UDP 会话创建的文件描述符会保持打开状态。因此,攻击者可以创建足够的会话来耗尽 sslh 默认支持的 1024 个文件描述符,从而使合法客户端无法再连接。
更糟糕的是,当遇到文件描述符限制时,sslh 会因段错误而崩溃,因为它 尝试解引用 new_cnx,而 new_cnx 在这种情况下是一个 NULL 指针。因此,此问题代表了一个简单的远程拒绝服务攻击向量。当管理员配置 udp_max_connections 设置(或命令行开关)时,也会发生段错误,因为在这种情况下也会达到 NULL 指针解引用。
为了重现此问题,我们测试了为 UDP 配置的 openvpn 探测。在客户端,我们创建了许多连接,每个连接只发送一个 0x08 字节。
我们没有非常彻底地检查 sslh-ev 实现,因为它依赖于第三方 libev 库。行为与 sshl-select 变体类似,尽管如此。UDP 套接字似乎从未再次关闭。
错误修复
上游已在 commit ff8206f7c 中修复了此问题,该 commit 是 v2.2.4 版本的一部分。虽然段错误已通过此更改修复,但 UDP 套接字仍可能在 sslh 处理更多流量之前保持打开状态,从而触发套接字超时逻辑。
3.2) OpenVPN 协议探测中的内存对齐访问错误 (CVE-2025-46806)
在 `is_openvpn_protocol()` 的 UDP 代码路径 中,可以找到像这样的 `if` 子句:
if (ntohl(*(uint32_t*)(p + OVPN_HARD_RESET_PACKET_ID_OFFSET(OVPN_HMAC_128))) <= 5u)
这会解引用一个 uint32_t*,它指向位于堆分配的网络缓冲区开头之后 25 字节的内存。在 ARM 等 CPU 架构上,这会导致 SIGBUS 错误,因此代表了一个远程 DoS 攻击向量。
我们在 x86_64 机器上通过编译 sslh 来重现此问题:
-fsanitize=alignment。通过发送至少 29 个 0x08 字节的序列,会触发以下诊断:
probe.c:179:13: runtime error: load of misaligned address 0x7ffef1a5a499 for type 'uint32_t', which requires 4 byte alignment
0x7ffef1a5a499: note: pointer points here
08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08
^
probe.c:185:13: runtime error: load of misaligned address 0x7ffef1a5a49d for type 'uint32_t', which requires 4 byte alignment
0x7ffef1a5a49d: note: pointer points here
08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08 08
错误修复
协议解析中此问题的通常修复方法是将整数数据 memcpy() 到本地堆栈变量中,而不是解引用指向原始网络数据的指针。这就是上游在 commit 204305a88fb3 中所做的,该 commit 是 v2.2.4 版本的一部分。
4) 其他发现和注意事项
4.1) TCP 流上未考虑短读取
几个探测在处理 TCP 协议时没有考虑短读取。例如,在 is_openvpn_protocol() 中,TCP 代码路径中存在以下代码:
if (len < 2)
return PROBE_AGAIN;
packet_len = ntohs(*(uint16_t*)p);
return packet_len == len - 2;
如果接收到的字节少于两个,则函数指示 PROBE_AGAIN,这是可以的。在将假定的消息长度解析到 packet_len 之后,只有当此时已接收到完整消息时,探测才会成功,否则函数返回 0,等于 PROBE_NEXT。
在 is_teamspeak_protocol() 和 is_msrdp_protocol() 中也发现了类似情况。虽然 TCP 上发生这种短读取的可能性不大,但它在形式上仍然是不正确的,并且可能在某些情况下导致协议检测的误报(尤其是在大量协议通过同一个 sslh 端口复用时)。
错误修复
根据经验,上游认为这目前在实践中不是一个问题,因为在此领域没有出现过错误报告。因此,这目前不是上游的优先事项。
4.2) 探测结果误报的可能性
一些探测函数依赖于非常少的协议数据来做出肯定判断。例如,is_tinc_protocol() 如果数据包以字符串开头,则指示匹配:" 0"。在 is_openvpn_protocol() 中,任何将数据包长度存储在前两个字节(网络字节序)的数据包都被认为是匹配的,这可能对许多网络协议都适用。
从安全的角度来看,这并不重要,因为这些数据包被转发到的服务必须能够处理发送给它们的所有数据,即使这些数据是为其他类型的服务准备的。但是,从正确的探测实现的角度来看,它可能在某些情况下导致意外行为(尤其是在大量协议通过同一个 sslh 端口复用时)。我们建议上游尝试基于更可靠的启发式方法来做出探测决策,以避免误报。
错误修复
与第 4.1 节类似,上游认为这目前不是用户的主要问题,因此在代码库中没有立即进行更改来解决此问题。
4.3) is_syslog_protocol() 中潜在未定义数据的解析
在 is_syslog_protocol() 中发现了以下代码:
res = sscanf(p, "<%d>", &i);
if (res == 1) return 1;
res = sscanf(p, "%d <%d>", &i, &j);
if (res == 2) return 1;
sscanf() 函数在这里不知道传入网络数据的边界。像 1 字节输入这样非常短的读取会导致 sscanf() 操作堆上分配的缓冲区中的未定义数据,该缓冲区在 defer_write() 中找到。
错误修复
作为快速错误修复,我们建议通过在有效负载末尾分配一个额外的字节来显式地将缓冲区零终止。然而,运行 sscanf() 来解析不受信任数据中的整数可能被认为有点危险,因此我们建议通常将其更改为更谨慎的代码。
上游已在 commit ad1f5d68e96 中修复了此问题,该 commit 是 v2.2.4 版本的一部分。此错误修复符合我们的建议,并且还增加了对从网络数据解析的整数的额外健全性检查。
5) sslh 对高网络负载攻击的弹性
像 sslh 这样的通用网络服务可能对资源耗尽攻击敏感,例如前面提到的 文件描述符耗尽问题。 sslh-fork 实现为每个传入的 TCP 连接生成一个新进程,这让人联想到不仅在每个进程范围内,而且在系统范围内消耗过多资源的可能性。通过创建大量连接到 sslh,可以实现 “fork bomb”效果。当“fork bomb”在 Linux 上本地执行时,即使在今天,如果没有严格的资源限制,通常仍会导致系统无法访问。远程实现类似这样的攻击将是一个主要的 DoS 攻击向量。
sslh-fork 为每个连接实现超时,该超时基于 select() 系统调用。如果在超时发生之前探测阶段未能做出决定,则连接将再次关闭。默认情况下,此超时设置为五秒。由于 sslh-fork 为每个新传入连接创建一个新进程,因此 sslh 打开的文件描述符没有 1024 的限制。理论上,攻击者可以通过创建过多的连接来尝试超出系统范围的文件描述符和/或进程限制。
然而,五秒的默认超时强制执行意味着攻击受到相当大的限制。在我们测试期间,我们无法创建超过大约 5,000 个并发 sslh-fork 进程。这会对系统产生相当大的负载,但在普通机器上不会暴露任何关键系统行为。
尽管目前的情况可以接受,但可以考虑提供一个应用程序级别的并行连接数量限制。对于 UDP,已经存在 udp_max_connections 设置,但对于 TCP 则没有。
错误修复
在与上游的讨论中,双方同意,实现适当的保护以防范此类拒绝服务攻击的最佳方法是由管理员负责,例如配置 Linux cgroup 约束。上游仍在考虑在未来添加一个 tcp_max_connections 设置来限制并行 TCP 连接的最大数量。
6) 总结
总的来说,我们认为 sslh 状况良好。攻击面很小,并且默认情况下已启用加固。随着两个远程 DoS 向量 3.1) 和 3.2) 的修复,在生产环境中使用 sslh 应该说是安全的。担心更复杂 DoS 攻击的用户还应考虑自定义他们的设置,以便在操作系统级别强制执行资源消耗限制。
如第 4.1) 和 4.2) 节所述,存在误报或漏报探测结果的风险。这些问题目前在实践中似乎并未大量出现,这是当前 sslh 实现中简洁性和效率之间的权衡。
7) 时间线
| 2025-04-25 | 我们通过电子邮件私下向 sslh 的作者报告了这些发现,并提供了协调披露。 |
| 2025-05-06 | 我们讨论了报告的问题细节和可能的 CVE 分配。目前这些问题已保密。 |
| 2025-05-08 | 我们从我们的池中为这些问题分配了两个 CVE,并与上游共享。 |
| 2025-05-25 | 上游作者告知我们,sslh GitHub 存储库中已发布了错误修复,并且即将发布的版本中将包含这些修复。 |
| 2025-05-28 | 发布了包含修复的 v2.2.4 版本。 |
| 2025-06-13 | 我们发布了本报告。 |