一、事件概览
2026 年 11 月 5 日,OpenSSH 项目发布 10.6p2,紧急修复一个可绕过 SSH 密码认证的严重漏洞。此时距离 10.6 正式版(10 月 6 日)发布仅一个月,属于典型的"计划外安全版本"。
漏洞编号 CVE-2026-123158,代号 "sshole",由 Wakefield 的 Özgün Kültekin 报告。MITRE 给出的 CVSS 3.1 评分为 8.1(HIGH),向量 AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H,CWE-394(对返回值/状态码的误判)。
一句话概括:在 musl libc + 未启用 PAM 的 Linux 上,只要系统里存在一个密码字段为 * 的账户,且同时存在另一个持有 $5$/$6$ 哈希的账户,攻击者提交一段 257~1024 字节的任意密码,就能以那个"本应禁止密码登录"的账户身份登录。一次连接即可,无需重试,命中率 100%。
二、漏洞现状
- 补丁已发布:上游 OpenSSH 10.6p2 于 2026-11-05 发布;Alpine 率先完成回补,修复版本一路回到已停止维护的 3.16 分支。
- 细节与 PoC 公开:研究者放出原理站点 sshhole.com 与两个复现环境(
leonjza/sshhole-lab、predkambrij/sshhole-repro),漏洞可被低成本验证,不存在"仅理论可利用"的缓冲期。 - 影响面被显著放大:Alpine 官方基础镜像在 Docker Hub 的累计下载量以十亿计,CI Runner、云主机、边缘设备、路由器固件大量基于 musl。腾讯云安全中心给出的风险等级为高风险,并确认"漏洞细节、POC 已公开"。
- 不是一个新 bug:触发链的两半分别成形于 2012 年和 2016 年,之后十年无人察觉,2026 年 11 月才被系统性测试发现。
- OpenSSH 官方定性:认为"根因在 sshd 之外"(即 musl 的
crypt()返回值语义),但既然能经由 sshd 利用,就在 sshd 侧加了规避补丁。
三、原理:一个 "*" 引发的血案
1. /etc/shadow 里的 * 是什么
现代 Linux 把密码哈希存放在 /etc/shadow,形如 $6$abc$k2P9...:$6$ 表示 SHA-512,abc 是盐(salt),后面是哈希值。而 * 不是哈希,它是"此账户永不允许用密码登录"的标记——1980 年 Bill Joy 在 BSD 的 /etc/passwd 里为 daemon 账户写下这个符号,沿用至今。
与之容易混淆的是 !:! 表示账户被锁定,在未启用 PAM 时连公钥登录也会被拒绝;* 则允许公钥登录、只禁止密码。Alpine 从 3.16 起把 root 的密码字段从 ! 改成 *,正是为了让云镜像能用密钥登录 root。
2. 无 PAM 时 sshd 如何校验密码
当 sshd 编译时未启用 PAM(Alpine 的 openssh-server 就是如此),密码校验走传统路径:调用 C 库的 crypt(用户提交的密码, 盐) 算出哈希,再与 /etc/shadow 中存储的字段逐字符比较,相等即通过。字段是 * 时本没有盐,理论上任何密码都无法匹对。
3. 没有盐,就借别人的
2016 年修复 CVE-2016-6210(用户枚举时间侧信道)时,OpenSSH 引入了一个"填充耗时"的逻辑:如果目标账户没有可用的盐,就借用系统中另一个账户的盐,照样做一次哈希运算,让"用户不存在"与"用户存在但密码错"的响应时间一致。这个改动本意是防御计时攻击,却让 * 账户第一次"拿到了别人的盐"。
4. musl 的两次关键改动
- 2012 年 8 月(commit b3c4cc12):musl 认为
crypt()失败返回NULL会让调用方崩溃,于是把失败返回值改成字符串"*"。 - 20 天后(commit 88bf5a8a):musl 加入 SHA-512 crypt 实现,为防止"超长密码拖慢服务器"的 DoS,把密码长度上限设为 256 字节,超过即视为失败。
两条改动单独看都合理,叠加后产生了一个致命的等价关系:超长密码 → crypt 失败 → 返回 "*" → 与 shadow 里的 "*" 完全相等。
5. 两条线交汇
alice 的密码字段 : *
crypt(257~1024 字节密码, bob 的盐) -> * (musl 长度超限,返回失败标记)
比较结果 : * == * → 认证通过
攻击者不需要知道任何密码,也不需要知道 alice 的任何信息之外的东西;只要能把 257 字节以上的字符串送进密码框,就能以 alice 身份登录。字数越长越稳,1024 字节以内都能成功。
四、受影响范围
漏洞需要以下四个条件同时成立,缺一不可:
- OpenSSH 版本 < 10.6p2;
- 运行在 musl libc 环境(Alpine、OpenWrt、Buildroot、Yocto,以及大量基于 Alpine 的容器镜像);
- 未使用 PAM(Alpine 的
openssh-server默认如此;openssh-server-pam配UsePAM no也算); PasswordAuthentication yes(OpenSSH 默认值,除非你手动关过),并且系统中至少有一个*账户 + 至少一个带$5$/$6$哈希的账户。
常见的 * 账户来源:Alpine 3.16+ 的 root(默认受 PermitRootLogin prohibit-password 保护,但可通过 doas 提权)、用 adduser 创建但未设密码的"仅密钥"用户、usermod -p '*' 的用户、通过 musl-nscd 接入的 LDAP/AD 用户(在本地没有 shadow 条目)、以及官方 tiny-cloud 镜像里的 alpine 用户。
自查三步
# 1. 确认 sshd 是否基于 musl
ldd $(command -v sshd) | grep -i musl
apk info 2>/dev/null | grep -i musl
# 2. 确认 PAM 与密码登录状态
sshd -T | grep -Ei '^(passwordauthentication|usepam|permitrootlogin)'
# 3. 列出密码字段为 * 的账户
awk -F: '$2 == "*" {print $1}' /etc/shadow
三步全部命中(musl + usepam no + passwordauthentication yes + 至少一个 * 账户 + 至少一个真实哈希账户)即为受影响。
不受影响的情况
- glibc 发行版(Ubuntu、Debian、CentOS、RHEL、Arch 等,且密码校验走 PAM);
PasswordAuthentication no;openssh-server-pam且UsePAM yes;- 密码字段为
!的锁定账户(但这类账户同时也无法用密钥登录); - 系统中不存在任何
$风格的密码哈希; - 容器内不运行 sshd(绝大多数应用容器);
- 由 cloud-init 写入
PasswordAuthentication no的云镜像。
特别提醒:Alpine 官方云镜像
官方 tiny-cloud 镜像(qcow2 / VHD / AMI)在 3.18.11 ~ 3.24.3 之间,内置一个密码字段为 * 且拥有免密 doas 权限的 alpine 用户。首次启动时是安全的,但只要机器上出现任意一个 $6$ 哈希(例如管理员设置了控制台密码,或 user-data 里添加了 hashed_passwd 用户),漏洞立即变为可利用,攻击者可以 alpine 身份登录并直接 doas 提权到 root。
五、修复方案
1. 首选:升级并重启 sshd
# Alpine
apk upgrade -U -a
rc-service sshd restart
# 确认版本
ssh -V
apk info -v openssh | head -1
Alpine 各分支的修复版本如下(2026-11-06 时点):
- edge:
10.6_p2-r0 - 3.24:
10.3_p1-r23.23:10.2_p1-r13.22:10.0_p1-r11 - 3.21:
9.9_p2-r13.20:9.7_p1-r63.19:9.6_p1-r3 - 3.18:
9.3_p2-r43.17:9.1_p1-r73.16:9.0_p1-r6
最容易踩的坑:apk upgrade 只替换磁盘上的二进制,不会重启已经在运行的 sshd 进程。不执行 rc-service sshd restart(或 systemctl restart sshd),漏洞依然存活。
2. 不能立即升级:关掉密码登录
关掉 PasswordAuthentication 是等价有效的临时缓解——漏洞只在密码认证路径上触发。以下命令以 root 执行,先确认自己的密钥登录可用,并保持当前会话不关闭,直到新会话验证成功:
mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sed -i.bak -e '1i PasswordAuthentication no' \
-e 's/^\(\s*PasswordAuthentication[^y#]*\)yes/\1no/i' \
/etc/ssh/sshd_config
sshd -t && rc-service sshd restart
sshd -T | grep -i passwordauthentication
最后一条命令应输出 passwordauthentication no。注意 Alpine 3.19 起才读取 sshd_config.d 目录,更老的版本靠第二条 sed 直接改主配置。回滚方式:删除 drop-in 文件并把 sshd_config.bak 拷回。
3. 其他发行版与容器
- OpenWrt / Buildroot / Yocto:升级
openssh包到含 patch 的版本,或同样关闭密码认证。Buildroot/Yocto 需要确认上游 recipe 已回补。 - Docker:把基础镜像更新到 Alpine 3.21.10 / 3.22.8 / 3.23.8 / 3.24.4 或更新版本后重新构建;容器内不跑 sshd 的不受影响,但派生出"带 sshd 的运维镜像"要逐个排查。
- 云镜像:新购实例优先选上述已修复版本;存量实例逐台
apk upgrade并重启 sshd。 - 自编译 OpenSSH:升级到 10.6p2,或手工合并上游 commit
4353fdc4。
4. 顺手加固
PermitRootLogin prohibit-password,避免 root 成为可登录的*目标;- 确实需要锁定的账户用
!而不是*(代价是该账户同时失去密钥登录能力,按需选择); - 条件允许时改用
openssh-server-pam+UsePAM yes,从根上绕开这条crypt()路径; - SSH 端口只对跳板机/可信网段开放,配合 fail2ban 或 nftables 限速;
- 把 sshd 的认证日志接入集中审计,便于事后追溯。
六、如何判断是否已被利用
漏洞利用会在日志里留下"某账户密码登录成功"的记录,而这些账户本不该出现这种记录。CERT-FR 与研究者的建议是同一条命令:
awk -F: '$2 == "*" {print $1}' /etc/shadow \
| xargs -r -I{} grep "Accepted password for {}" /var/log/messages
任何命中都高度可疑。进一步排查:doas/sudo 审计日志、~/.ssh/authorized_keys 的近期改动、新增用户、异常 cron 与 systemd unit、可疑出站连接。无法排除的机器应按已失陷处理(隔离、取证、重装、轮换密钥)。
七、几个常见误区
- "我用 Ubuntu/Debian,肯定没事":方向对(glibc + PAM),但请确认没在容器里跑 Alpine 版 sshd,也没自编译 musl 版本。
- "改了端口 / 加了防火墙就安全":不是缓解,只是降低被扫到的概率。
- "我把密码登录关了,为什么还提示受影响":那正是缓解措施生效的表现,可安心等维护窗口升级。
- "升级了包就完事":必须重启 sshd 进程,否则补丁不生效。
- "空密码账户最危险":恰恰相反,空字段与任何
crypt()结果都不匹配;真正危险的是*字段。
八、小结
"sshole"是一次典型的组件边界缺陷:musl 为容错改了返回值语义,OpenSSH 为防计时攻击加了盐借用,两者在"字段为 *、密码超长"这一狭窄组合下正好对齐,把一个"永不允许密码登录"的账户变成了"任何超长密码都能登录"。它提醒我们三件事:跨库语义变更要端到端验证;时间侧信道的防御补丁本身需要安全评审;升级之后必须重启服务。
对 Alpine 用户来说,动作很小:apk upgrade -U -a + rc-service sshd restart;来不及升级,就 PasswordAuthentication no。两件事做任意一件,洞就关上了。