目录

1) 引言

OpenSMTPD 是 OpenBSD 项目提供的服务器端 SMTP 协议的实现。几个月前,SUSE 的一位同事开始为其打包 openSUSE Tumbleweed,这促使了对该软件包进行代码审查

在研究 OpenSMTPD 提供的本地 API 时,我们发现了一个简单的本地拒绝服务 (DoS) 攻击向量,该向量允许未经授权的用户导致所有 smtpd 服务关闭。有关该问题的详细信息将在第 2 部分中提供。有关 OpenSMTPD 中包含的 setuid-root 和 setgid 实用程序的其他说明将在第 3 部分中找到。对 OpenSMTPD 中面向网络代码的快速概述将在第 4 部分中提供。

请注意,OpenSMTPD 存在两个源代码存储库。最新代码可在OpenBSD CVS 存储库中找到,而便携版本可在 GitHub 上找到。便携版本为各种 UNIX 系统(包括 Linux 和其他 BSD 系统)提供跨平台支持。本报告基于便携式OpenSMTPD 版本 7.7.0p0

我们已将本地拒绝服务问题报告给上游,但由于沟通问题,直到发布前两天才收到回复。目前,上游 Bug 修复已可用,并将包含在即将发布的 7.8.0 版本中,但似乎一个独立的内存泄漏问题仍未解决。

2) 通过 UNIX 域套接字进行的本地拒绝服务攻击 (CVE-2025-62875)

OpenSMTPD 包含 smtpctl 程序,该程序通过位于 /var/run/smtpd.sock 的 UNIX 域套接字与 smtpd: control 守护进程实例进行通信。在研究用于此目的的协议时,我们注意到一个简单的本地拒绝服务攻击向量,影响了所有 OpenSMTPD。

UNIX 域套接字 smtpd.sock 的文件模式为 0666,因此系统上的所有用户都可以写入,允许任何人创建到 smtpd 的本地连接。

在守护进程的代码中,位于 mproc_dispatch() 中,在处理此类 UNIX 域套接字连接时发生任何错误,进程将通过 fatal() 退出。导致这种情况的两个常见错误情况是 ibuf_read() 中的 readv() 返回错误,或者消息头中的消息长度值错误,该值在 imsg_parse_hdr() 中检测到。在启用文件描述符传递的连接上使用的 msgbuf_read() 调用路径中存在类似的错误条件(请参阅 imsgbuf_read())。

因此,发送具有错误头部的格式错误的邮件足以让客户端在守护进程端触发 fatal() 的调用。一旦调用 fatal()smtpd: control 实例将结束执行,并导致整个 smtpd 实例随之关闭。

我们从上游了解到,调用 fatal() 的原因是 smtpd.sock 同时用于两种不同的目的:

  • 来自其他受信任 smtpd 守护进程实例的连接。
  • 来自使用 smtpctl 程序的任意其他客户端的连接。

调用 fatal() 是针对第一类连接进行的。这些连接在启动时建立,如果在处理来自其他 smtpd 守护进程实例的数据时发生任何错误,则假定 OpenSMTPD 本身存在 Bug,并且关闭所有守护进程是可行的措施。

第二类连接在此逻辑中未被考虑,因此允许未经授权的客户端触发导致本地拒绝服务的代码路径。因此,上游 Bug 修复由一个添加的 if 子句组成,该子句将第二类连接排除在 fatal() 的调用之外。

常规连接关闭时的内存泄漏

在研究可能的拒绝服务问题 Bug 修复时,我们注意到代码中的一个注释,它指向连接失败时未解决的清理问题。

ibuf_read_process(struct msgbuf *msgbuf, int fd)
{
    /* <<< SNIP >>> */
fail:
    /* XXX how to properly clean up is unclear */
    if (fd != -1)
        close(fd);
    return (-1);
}

这让我们想知道,如果在错误条件下清理不明确,那么在正常操作期间是否也可能不明确?毕竟,无论连接是正常结束还是出现错误,都需要进行适当的清理。碰巧,OpenSMTPD 中常规连接关闭和错误连接关闭的清理逻辑确实是等效的。

为此,我们测试了连续创建和关闭大量连接到 smtpd 的 UNIX 域套接字时会发生什么。结果确实是 smtpd: control 实例使用的内存会不断增长。因此,似乎这里存在内存泄漏,独立于上面描述的主要问题。我们没有对此进行更详细的分析,但上游已意识到此问题并正在进行分析。

这个独立的问题意味着,即使在应用了上游 Bug 修复来解决本报告中的主要问题后,未经授权的用户仍然可以触发 smtpd: control 守护进程中的内存泄漏。影响也将是本地拒绝服务,但执行起来需要更长的时间,因为内存泄漏很小,在我们测试中,半小时内只消耗约 100 兆字节。下一节还描述了一种可能的临时规避方法。

通过调整套接字权限来规避

我们最初建议一个不同的补丁来解决本地拒绝服务问题,即收紧 smtpd.sock UNIX 域套接字的权限。我们错误地认为非 root 用户连接到此套接字没有有效的用例。在发布前不久,我们从上游了解到,这种情况实际上有一个有效的用例:非 root 用户可以使用 sendmail 接口来排队邮件,而 sendmail 接口会利用此套接字。

尽管我们建议的补丁在这种情况下会导致回归,但它减少了攻击面,并提供了针对上一节所述内存泄漏问题的保护。因此,对于 OpenSMTPD 的一些用户来说,如果不需要上述用例,使用此补丁可能是一个明智的选择,至少要等到上游也提供内存泄漏问题的修复。

复现器

我们提供了一个简单的 Python 脚本来重现该问题。该脚本创建一个到 smtpd.sock 的连接并发送一个过大的头部长度。如果复现器有效,smtpd 守护进程进程将立即全部退出。

受影响的版本

首次进入 7.7.0 版本的提交 3270e23a6eb 中,引入了对消息解析代码的重大更改,包括调用 fatal()。在我们的测试中,对于所有基于此版本的软件包,触发该问题都很容易。

尚不清楚旧版本是否也可能受到此问题变体的影响。我们只验证了简单的复现器在 OpenSMTPD 7.6.0 版本上无效。

受影响的系统

我们已验证该问题影响以下系统,这些系统都提供 OpenSMTPD 版本 7.7.0:

  • Arch Linux(截至 2025-09-29 完全更新)
  • Debian 13
  • Fedora 42
  • Gentoo Linux 使用 7.7.0p0 ebuild
  • OpenBSD 7.7
  • NetBSD 10.1(使用来自pkgsrc 的软件包)

在 FreeBSD 14.2 上,仅提供较旧的 OpenSMTPD 版本 7.6.0,我们未能重现该问题。

CVE 分配

在没有上游正式确认的情况下,我们不愿为该问题分配 CVE。但情况似乎很明确,当我们被要求在distros 邮件列表上提供 CVE 时,我们分配了 CVE-2025-62875,并将其告知了上游。

在与上游建立联系后不久,上游就采纳了这个 CVE,并用它来记录他们的 Bug 修复。

上游 Bug 修复

上游已经发布了主要问题的 Bug 修复提交。包含此 Bug 修复的 OpenSMTPD 版本 7.8.0 即将发布,并且已在上游邮件列表上宣布

3) 关于 setuid 和 setgid 二进制文件的说明

最初审查 OpenSMTPD 的原因在于软件包中存在 setuid 和 setgid 二进制文件。以下子部分简要总结了审查结果。

lockspool

/usr/libexec/opensmtpd/lockspool 是一个可供所有人访问的 setuid-root 二进制文件,用于同步对用户 spool 的并行访问。

OpenSMTPD 便携版中的lockspool 代码相当复杂,并且基于一些可能不成立的假设。在多用户场景下,此代码可能导致轻微的本地拒绝服务。OpenBSD CVS 存储库已包含一个简化的锁定算法,该算法不受此问题影响。

我们已将此问题单独告知上游,而不是通过 UNIX 域套接字拒绝服务问题。在此问题上,我们很快得到了答复,上游将 CVS 存储库的更改合并到了 OpenBSD 便携版存储库。此更改将包含在 OpenSMTPD 7.8.0 版本中。我们也将此更改移植到了 OpenSMTPD 的 openSUSE 打包中。

smtpctl

/usr/sbin/smtpctl 是一个可供所有人访问的 setgid 二进制文件,在 _smtpq 组上下文中运行。该程序利用这些特殊组权限在 smtpd 服务未运行时将邮件存储在目录
/var/spool/smtpd/offline 中。

_smtpq 组权限仅用于此明确定义的用途,并且在不再需要时会立即放弃额外的权限。我们在此程序方面未发现任何问题。

4) 关于面向网络的 OpenSMTPD 代码的说明

在我们发现本报告中描述的本地安全问题后,我们认为至少对 OpenSMTPD 中实际的面向网络 SMTP 协议解析代码进行粗略了解是一个好主意。我们在这些部分中未找到任何实质性的安全问题,但这是我们对代码印象的简要总结:

  • 协议解析是用纯 C 实现的,因此容易出错。但是,尽管在处理各种消息类型时存在一些冗余,但该实现已对此进行了控制。
  • 许多解析是手动完成的,没有第三方库的帮助,包括域名结束和电子邮件地址验证等内容。
  • 在未加密的连接上拒绝明文密码传输,这是一个良好的安全立场。
  • 处理网络数据的守护进程以有限的服务用户凭据运行,并且还被放入chroot 监狱,这减少了攻击面。
  • 该守护进程默认记录所有错误的 SMTP 协议消息,包括攻击者控制的数据,这有点奇怪。然而,我们在 BSD 和 Linux 系统上查看的日志系统能够以安全的方式处理此问题(例如,终端转义序列被转义或剥离)。

5) 时间线

2025-09-15 我们已将此问题报告给security@openbsd.org,并提议协调披露。我们很快收到一个简短的回复,表示该主题已转交给相关人员。
2025-09-29 在两周内没有收到更详细的回复后,我们发送了一封后续邮件,询问问题的确认以及是否希望协调披露。我们要求在 2025-10-02 之前回复,否则我们将按我们自己的意愿发布该发现。
2025-10-02 在仍无回复的情况下,我们决定部分发布该问题,通过向我们的打包中添加一个补丁,该补丁可保护 UNIX 域套接字权限。
2025-10-23 我们联系了distros 邮件列表,向其他发行版提供有关该问题的预警。我们建议将禁运期延长至 2025-10-31。
2025-10-24 distros 邮件列表的一位成员要求分配 CVE,因此我们决定分配 CVE-2025-62875,并告知上游此情况以及 distros 邮件列表上的当前禁运。
2025-10-27 我们将我们最初忘记做的建议补丁分享给了 distros 邮件列表。
2025-10-29 一位 OpenSMTPD 开发人员终于回复了我们的报告,解释说信息在此之前已在内部丢失。上游确认了该问题,并告知我们最迟将在 2025-11-03 准备好 Bug 修复版本。
2025-10-30 通过与上游的进一步讨论,我们了解了调用 fatal() 的真实意图以及我们建议的补丁引起的回归。另一方面,我们告知了上游我们偶然发现的其他内存泄漏问题。
2025-10-31 上游发布了其主要问题的 Bug 修复
2025-10-31 我们更新了报告,加入了上游的最新信息并发布了它。

6) 链接