Qualys Security Advisory
regreSSHion: RCE in OpenSSH's server, on glibc-based Linux systems (CVE-2024-6387)
摘要 SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, 2005 年版)
All it takes is a leap of faith
-- The Interrupters, "Leap of Faith"
初步说明:OpenSSH 是世界上最安全的软件之一;此漏洞是其近乎完美的实现中一次偶然失误。其纵深防御设计和代码是典范和灵感来源,我们感谢 OpenSSH 开发者的杰出工作。
我们在 OpenSSH 服务器(sshd)中发现了一个漏洞(信号处理程序竞争条件):如果客户端未能在 LoginGraceTime 秒内(默认为 120 秒,旧版 OpenSSH 中为 600 秒)完成身份验证,则 sshd 的 SIGALRM 处理程序会被异步调用,但该信号处理程序调用了多个非异步信号安全的函数(例如 syslog())。此竞争条件在默认配置下影响 sshd。
经过调查,我们意识到此漏洞实际上是 CVE-2006-5051(“OpenSSH 4.4 之前版本中的信号处理程序竞争条件允许远程攻击者导致拒绝服务(崩溃),并可能执行任意代码”)的回归,该漏洞由 Mark Dowd 于 2006 年报告。
此回归是由 2020 年 10 月(OpenSSH 8.5p1)的提交 752250c(“为 OpenSSH 修订的日志基础设施”)引入的,该提交意外地从 sigdie() 函数中移除了“#ifdef DO_LOG_SAFE_IN_SIGHAND”,而 sigdie() 直接被 sshd 的 SIGALRM 处理程序调用。换句话说:
此漏洞在基于 glibc 的 Linux 系统上可被远程利用,因为 syslog() 本身会调用异步信号不安全的函数(例如 malloc() 和 free()):这是一个以 root 身份执行的未认证远程代码执行,因为它影响 sshd 的特权代码,该代码未经沙箱化且拥有完全权限。我们尚未调查其他 libc 或操作系统;但 OpenBSD 显然不受影响,因为其 SIGALRM 处理程序调用 syslog_r(),这是 OpenBSD 于 2001 年发明的 syslog() 的异步信号安全版本。
为了远程利用此漏洞(据我们所知,CVE-2006-5051 此前从未被成功利用),我们从 Michal Zalewski 于 2001 年发表的一篇具有远见的论文《Delivering Signals for Fun and Profit》中汲取了灵感:
https://lcamtuf.coredump.cx/signals.txt
然而,我们立即面临三个主要问题:
从理论角度,我们必须找到一条有用的代码路径,如果该路径在适当时间被 SIGALRM 中断,会使 sshd 处于不一致状态,然后我们必须在 SIGALRM 处理程序内部利用此不一致状态。
从实践角度,我们必须找到一种方法在 sshd 中到达这条有用的代码路径,并最大化在正确时间中断它的机会。
从时机角度,我们必须找到一种方法进一步增加在正确时间中断这条有用代码路径的机会,并且是远程实现。
为了专注于这三个问题,而不必立即对抗所有现代操作系统保护(特别是 ASLR 和 NX),我们决定先利用旧的 i386 上的 OpenSSH 版本,然后基于此经验,再针对较新版本:
首先,“SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3”,来自“debian-30r6-dvd-i386-binary-1_NONUS.iso”:这是第一个默认启用权限分离,并已修补那个时代所有关键漏洞(特别是 CVE-2003-0693 和 CVE-2002-0640)的 Debian 版本。
为了远程利用此版本,我们使用 SIGALRM 中断对 free() 的调用(在 sshd 的公钥解析代码中),使堆处于不一致状态,然后在 SIGALRM 处理程序中对 free() 的另一次调用期间利用此不一致状态。
在我们的实验中,平均需要约 10,000 次尝试才能赢得此竞争条件;即,600 秒(LoginGraceTime)内接受 10 个连接(MaxStartups),平均约需 1 周才能获得远程 root shell。
其次,“SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3”,来自“ubuntu-6.06.1-server-i386.iso”:这是最后一个仍受 CVE-2006-5051 影响的 Ubuntu 版本(“OpenSSH 4.4 之前版本中的信号处理程序竞争条件”)。
为了远程利用此版本,我们使用 SIGALRM 中断对 pam_start() 的调用,使 PAM 的某个结构处于不一致状态,然后在 SIGALRM 处理程序中对 pam_end() 的调用期间利用此不一致状态。
在我们的实验中,平均需要约 10,000 次尝试才能赢得此竞争条件;即,120 秒(LoginGraceTime)内接受 10 个连接(MaxStartups),平均约需 1-2 天才能获得远程 root shell。
最后,“SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2”,来自“debian-12.5.0-i386-DVD-1.iso”:这是当前的 Debian 稳定版,并且受 CVE-2006-5051 回归的影响。
为了远程利用此版本,我们使用 SIGALRM 中断对 malloc() 的调用(在 sshd 的公钥解析代码中),使堆处于不一致状态,然后在 SIGALRM 处理程序中对 malloc() 的另一次调用期间(更准确地说,在 syslog() 内部)利用此不一致状态。
在我们的实验中,平均需要约 10,000 次尝试才能赢得此竞争条件,因此 120 秒(LoginGraceTime)内接受 100 个连接(MaxStartups)大约需要 3-4 小时。最终,平均约需 6-8 小时才能获得远程 root shell,因为我们只有一半时间能正确猜测 glibc 的地址(由于 ASLR)。
这项研究仍在进行中:
在我们开始 amd64 工作几天后,我们注意到 OpenSSH 的公共 Bugzilla 中有以下关于 sshd 的 SIGALRM 处理程序中死锁的错误报告:
https://bugzilla.mindrot.org/show_bug.cgi?id=3690
因此,我们决定立即联系 OpenSSH 开发者(告知他们此死锁是由一个可利用的漏洞引起的),暂停了我们的 amd64 工作,并开始编写此公告。
But that's not like me, I'm breaking free
-- The Interrupters, "Haven't Seen the Last of Me"
此 OpenSSH 版本的 SIGALRM 处理程序调用 packet_close(),后者调用 buffer_free(),然后调用 xfree() 进而调用 free(),而 free() 不是异步信号安全的:
因此,我们开始阅读此 Debian 的 glibc(2.2.5)的 malloc 代码,以查看对 free() 的第一次调用是否可以被 SIGALRM 中断,并在 SIGALRM 处理程序中对 free() 的第二次调用期间(上述第 341-344 行)被利用。由于该 glibc 的 malloc 未针对 Solar Designer 于 2000 年首创的 unlink() 技术进行加固,我们很快在 chunk_free()(由 free() 内部调用)中发现了一条有趣的代码路径:
为了利用此代码路径,我们安排 sshd 的堆具有以下布局(chunk_X、chunk_Y 和 chunk_Z 是 malloc() 分配的内存块,p、s、f、b 分别是它们的 prev_size、size、fd 和 bk 字段):
-----|---+---------------|---+---------------|---+---------------|----- ... |p|s|f|b| chunk_X |p|s|f|b| chunk_Y |p|s|f|b| chunk_Z | ... -----|---+---------------|---+---------------|---+---------------|----- |<------------->| user data