首页 > 开源 > OpenSSH 严重绕过漏洞“sshole”让任意超长密码都能登录 root

OpenSSH 严重绕过漏洞“sshole”让任意超长密码都能登录 root

OSChina资讯 2026-11-06 14:41 7 阅读 查看原文

一个本该永远无法登录的账号,只要输入超过 256 个字符的任意密码,就能进去。这个漏洞在 musl libc 里躺了十四年——从 2012 年埋下,到 2026 年才被发现。

发现者 Özgün Kültekin 给它起了个名字:*sshole。他还做了个网站 sshhole.com,把这个 bug 的来龙去脉从 1980 年一路讲到今天。


先看这个漏洞怎么形成的

1980 年,Bill Joy 在 BSD Unix 的 /etc/passwd 里给 daemon 账号的密码字段填了一个 *。* 不是一个哈希值,所以任何密码都算不出来跟它匹配——这个账号从此无法用密码登录。这个做法沿用至今。

1990 年代,密码哈希搬到了 /etc/shadow,格式是 $6$abc$k2P9…($6$ 表示 sha512,abc 是盐值,后面是哈希)。sshd 拿到你输入的密码,用同样的盐值算一遍,结果相同就放行。如果字段只是个 *,那就没什么可匹配的——你输什么都进不去。

但有个细节:单 Unix 规范里写着,如果 crypt() 无法完成计算,它返回 NULL。比如盐值要求了系统没有的算法。很多程序不检查 NULL,直接拿返回值跟存储字段比,一碰就崩。

2012 年,musl libc(Alpine Linux 用的那个)决定不让 crypt() 返回 NULL 了。算不出来怎么办?返回一个单字符。那个字符永远匹配不上真实密码,程序不会崩,只是看到"密码错误"。

他们选的那个字符是 *。

三周后,musl 加上了 sha512 crypt。问题是它越长的密码算得越慢,有人可以用超长密码把服务器拖住。于是 musl 把密码上限压到 256 字节,超了直接失败。

失败的时候返回什么?

还是 *。

那段代码注释里写得清清楚楚,一直写了十四年。


2016 年,漏洞真正成型

这一年 OpenSSH 修了个信息泄露问题(CVE-2016-6210):攻击者可以通过响应时间判断哪些账号存在——不存在的用户 sshd 立刻拒绝,真实用户要算一遍哈希慢几毫秒。

修法有点好笑。既然没有真实哈希可算,那 sshd 就抓另一个账号的盐值,照样把你输的密码哈希一遍,纯粹为了浪费同样的时间。"还是不行,只是慢一点的'不行'。"

这个改动顺带让 sshd 对"密码字段是 *"的账号也走了同样的路径。

到这里,三个条件凑齐了:musl 的 crypt() 返回 * + sshd 会给 * 账号算哈希 + 超长密码让 crypt() 失败。

结果就是:一个本该永远锁死的账号,只要你输入 257 到 1024 个字符的密码,crypt() 返回 *,跟 shadow 里的 * 一比——匹配成功,进。


为什么十年没人发现

Kültekin 在网站上列了几个原因:

没密码的裸机不受影响。 需要 shadow 里先有一个 *。

OpenSSH 的测试跑在 OpenBSD 和 glibc 上,musl 环境没人测 sshd。

大多数发行版走 PAM,而 PAM 对 * 账号根本不会调用 crypt()。

也许有人撞见过。 他写道:"也许有人把错误的东西粘进了密码框,半条 slack 消息或者一整个 ssh key,然后就那么进去了。你从来没往密码框里粘错过东西?了不起。"

2013 年就有 Debian 开发者提议让 glibc 抄 musl 这个做法,六年后 Debian 换到 libxcrypt,返回的是 *0 而不是 *,bug 就关了——差一个字符。

2024 年底 musl 邮件列表有个帖子讨论 busybox 的 passwd 在 257 字符密码下崩掉。同一个 *,同一个上限。但那个帖子里没人提 sshd。

2026 年 10 月 30 日,OpenSSH 的一次安全审查修了 pick_salt() 函数——bug 就在它下面四行。四天后 Kültekin 发现了它。


他是怎么找到的

按他自己的说法,方法很土:把 sshd 用各种配置编译出来(glibc / musl / 有 PAM / 无 PAM / 有 shadow / 无 shadow),灌了大概 17,600 个奇怪的登录进去——怪哈希、坏账号、超长密码。

然后他补了一句:

"抱歉。我有一群 agent swarm。很多个。这个就不细说了。"

所有构建对所有登录的判断都一致,除了 musl 无 PAM 那个。 它说了不该说的"是"。


影响范围

需要同时满足:

  1. sshd 跑在 musl 上(Alpine、OpenWrt、Buildroot、Yocto,以及你 pull 的很多 Docker 镜像)
  2. 不走 PAM(Alpine 的 openssh-server 默认就不用)
  3. 密码登录还开着(默认开着)
  4. 有一个密码字段是纯 * 的账号(key-only 用户、LDAP 用户、Alpine 上的 root)
  5. 还有另一个有真实密码的账号

Alpine 官方的 AWS 和 Azure 镜像要特别注意:默认用户就是 * 密码字段,而且带免密 doas。等于直接拿 root。

glibc 系统(Debian、Ubuntu、Fedora)、走 PAM 的、Dropbear、OpenBSD——都不受影响。


修复

OpenSSH 10.6p2 已修复(11 月 5 日发布)。

apk upgrade openssh

暂时升不了级,就把密码登录关掉:

echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/00-no-passwords.conf
rc-service sshd restart

想知道有没有被黑过? Kültekin 给了一条命令,查敏感机器上有没有"无密码账号曾经用过密码"的记录。如果 root@your-box 出现过——已经中招了。

目前没有发现被利用的迹象。


一点其他想法

这个 bug 有意思的地方在于,它几乎每个环节都是"合理的":

  • 1980 年用 * 标记禁登录账号——合理
  • 2012 年 musl 不想让 crypt() 返回 NULL 引发崩溃——合理
  • 2016 年 OpenSSH 用假哈希防时序泄露——合理

三个合理的决定叠在一起,变成一个能拿到 root 的漏洞。

HN 上 600 多条评论,讨论意外地集中在同一点上。有人说得直接:

crypt() 这种行为简直是"对着月亮嚎叫的疯狂",本世纪建的任何系统都不该再考虑用它,除非外面套上大量包装层把所有边界情况都修正归一。

还有一条评论我认为更准确:

我得出结论:当年那个"不在 crypt 里返回 NULL,好让有 bug 的代码不崩"的老传统,是个坏主意。

用错误处理的便利换掉错误处理的正确性,代价在十四年后才浮出水面。

参考来源