https://sshhole.com/

SSHHole 这个名字挺有意思,字面上有点像 SSH + Hole(SSH 漏洞/洞穴)。我们重点从技术角度看它:它到底解决什么问题、底层原理是什么、适合什么场景,以及有没有安全隐患。

一、先说结论:这是一个 OpenSSH 身份认证绕过漏洞

高危安全漏洞 CVSS 8.1

SSHHole 实际上是一个安全研究者发布的漏洞分析网站,介绍的是 OpenSSH 在使用 musl libc、且不使用 PAM 的特定环境下,可能绕过 SSH 密码认证的问题。

对应漏洞编号:CVE-2026-123158。

它最让人意外的地方是:

一个本来应该禁止密码登录的 Linux 账户,反而可能接受任意一个足够长的密码。

这里的足够长,指的是 257~1024 字节。

主要受影响的环境:

  • Alpine Linux
  • OpenWrt
  • Buildroot
  • Yocto
  • 基于上述环境构建的一些 Docker 镜像

注意,不是所有这些系统都必然受影响,还需要满足后面介绍的其他条件。

来源:漏洞作者的原始分析 SSHHole、CVE-2026-123158 分析。

二、漏洞到底是怎么产生的?

这个漏洞很有学习价值,因为它不是某个算法被破解了,也不是传统意义上的缓冲区溢出。

而是三个组件的行为恰好组合在了一起:

  1. Unix/Linux 的密码锁定机制。
  2. musl libc 的 crypt() 函数。
  3. OpenSSH 为了防止用户枚举而做的一个安全处理。

我们一步步拆开。

1. Linux 中的 * 是什么意思?

先看 /etc/shadow。

正常情况下,一个 Linux 用户的密码字段可能长这样:

alice:$6$salt$xxxxxxxxxxxxxxxx:...

其中:

  • $6$:表示 SHA-512 crypt 密码哈希算法。
  • salt:盐值。
  • 后面是密码哈希结果。

用户登录时,系统会:

  1. 获取用户输入的密码。
  2. 使用相同的 salt 和算法计算哈希。
  3. 将计算结果与 /etc/shadow 中保存的哈希比较。
  4. 相同则认证成功。

但是,有些账户的密码字段不是哈希,而是:

root:*

这里的 * 是一个特殊标记。

它的含义是:这个账户不能通过 Unix 密码认证。

例如,某些系统会给只允许 SSH Key 登录的用户设置这样的字段。

正常情况下,无论用户输入什么密码,都不应该通过认证。

2. musl libc 的 crypt() 做了一件特殊的事情

先介绍一个概念:crypt()。

它是 Linux/Unix 中用于密码哈希计算的函数。

接口大致是:

char *crypt(const char *key, const char *salt);

正常情况下,输入密码和 salt,返回计算后的哈希字符串。

但如果计算失败,传统行为是返回 NULL。

而 musl 在 2012 年做了一个特殊处理:

计算失败时,不返回 NULL,而是返回一个 *。

为什么这么做?

因为一些程序没有正确处理 NULL,可能直接崩溃。

musl 希望通过返回一个不可能匹配正常密码的特殊字符串,让调用方能够继续工作。

问题来了:

Linux 的账户锁定标记也是 *。

于是两个原本完全不同的语义,使用了同一个字符串。

两个不同含义,却使用了同一个值

  • /etc/shadow:*,账户禁止密码登录
  • musl crypt():*,密码哈希计算失败

危险的语义冲突

3. 为什么超过 256 字节会触发?

musl 的 SHA-512 crypt 实现还有一项限制:

密码长度超过 256 字节时,拒绝进行计算。

这是为了避免攻击者通过超长密码消耗过多服务器计算资源。

于是:

输入密码长度 <= 256 字节
    ↓
正常计算哈希

输入密码长度 > 256 字节
    ↓
拒绝计算
    ↓
返回 "*"

注意这里是字节,不是字符。对于 ASCII 字符,一个字符就是一个字节;对于 UTF-8 中文,一个汉字通常占 3 个字节。

到这里,问题的关键条件已经出现了。

但是,光有 musl 还不够。

4. OpenSSH 在 2016 年引入了一个关键行为

这一步是整个漏洞链条最有意思的部分。

2016 年,OpenSSH 修复了一个叫 CVE-2016-6210 的漏洞。

原漏洞是什么?

攻击者可以通过观察 SSH 登录响应时间,判断某个用户名是否真实存在。

例如:

用户名不存在:
    很快返回认证失败

用户名存在:
    需要计算密码哈希
    稍慢才返回认证失败

攻击者可以通过时间差进行用户名枚举。

OpenSSH 的解决方案是:

即使用户不存在,或者没有可用的密码哈希,也尽量执行一次密码哈希计算。

它会借用其他账户的 salt。

这样无论用户名是否存在,认证过程的耗时就更加接近。

但这个设计也让原本不应该参与密码验证的 * 账户,进入了哈希计算路径。

现在把前面的知识结合起来。

三、完整攻击链条

假设有两个用户:

alice:*
bob:$6$abc$xxxxxxxxxxxxxxxx

其中:

  • Alice 没有可用的密码哈希,禁止密码登录。
  • Bob 有一个正常密码哈希。

攻击过程:

第一步  攻击者尝试登录 alice
        OpenSSH 发现 Alice 没有正常密码哈希。
   v
第二步  OpenSSH 借用 Bob 的 salt
        为了让认证耗时接近正常用户,仍然调用 crypt()。
   v
第三步  攻击者提交超过 256 字节的密码
        musl 的 SHA-512 crypt 拒绝处理超长密码。
   v
第四步  crypt() 返回 `*`
        这原本是计算失败的特殊标记。
   v
第五步:漏洞触发  OpenSSH 比较两个字符串
        `* == *`
        认证意外成功!

这就是 SSHHole 的核心。

它不是破解了密码,而是让密码验证过程中的错误返回值,意外地变成了正确的匹配结果。

四、是不是所有 Alpine Linux 都受影响?

不是。必须同时满足以下条件:

条件要求
操作系统使用 musl libc
SSH 服务OpenSSH sshd
PAM没有使用 PAM
密码认证PasswordAuthentication yes
目标账户/etc/shadow 中密码字段为 *
其他账户至少有一个正常的 $5$ 或 $6$ 密码哈希

这六项必须同时满足。

有几个特别值得注意的情况:

1. Ubuntu、Debian、Fedora

它们通常使用 glibc,不受这个漏洞影响。

2. Alpine Docker 镜像

如果只是:

FROM alpine:3.22

RUN apk add python3

容器里没有运行 sshd,就不属于这个漏洞的直接攻击面。

这点非常重要:使用 Alpine 作为基础镜像,不等于容器就存在 SSH 漏洞。

3. OpenWrt

OpenWrt 很多默认环境使用 Dropbear,而不是 OpenSSH。

Dropbear 不受这个漏洞影响。

但如果你自己安装了 OpenSSH,就需要检查。

4. Alpine 云服务器镜像

这是一个比较危险的场景。

Alpine 官方部分 AWS、Azure 云镜像中,alpine 用户可以通过 doas 获得 root 权限。

如果攻击者绕过 SSH 登录,就可能进一步获得 root 权限。

以上条件及受影响范围参考 漏洞影响范围与排查说明。

五、怎么修复?

已有官方修复版本

2026 年 11 月 5 日,OpenSSH 发布了 10.6p2 修复版本。

Alpine 也对各个发行分支进行了补丁回移。

1. Alpine Linux

执行:

apk upgrade openssh
rc-service sshd restart

注意:Alpine 稳定分支采用补丁回移,不一定需要升级到 10.6p2 这个上游版本号。

具体修复版本:

Alpine修复版本
3.2410.3_p1-r2
3.2310.2_p1-r1
3.2210.0_p1-r11
3.219.9_p2-r1
3.209.7_p1-r6
3.199.6_p1-r3
3.189.3_p2-r4
3.179.1_p1-r7
3.169.0_p1-r6

2. 暂时不能升级怎么办?

关闭 SSH 密码认证:

echo 'PasswordAuthentication no' \
  > /etc/ssh/sshd_config.d/00-no-passwords.conf

sshd -t && rc-service sshd restart

前提是你已经确认 SSH Key 登录正常。

建议保留当前 SSH 会话,另外开一个终端测试新连接,避免把自己锁在服务器外面。

3. 怎么检查自己的服务器?

如果是 Alpine,可以执行:

# 查看 OpenSSH 版本
apk info -v openssh-server

# 查看 SSH 认证配置
sshd -T | grep -Ei \
'^(passwordauthentication|usepam|permitrootlogin)'

# 查找密码字段为 * 的用户
awk -F: '$2 == "*" {print $1}' /etc/shadow

# 查找正常 SHA-256 / SHA-512 哈希的用户
awk -F: '$2 ~ /^\$[56]\$/ {print $1}' /etc/shadow

这几个命令可以帮助判断是否满足漏洞条件。

如果已经暴露,建议检查 SSH 日志中是否存在不应该发生的密码登录:

grep 'sshd.*Accepted password for' /var/log/messages*

尤其关注密码字段为 * 的用户。

如果确认发生过异常登录,需要按主机已被入侵处理,而不只是升级软件包。检查新增用户、SSH 公钥、提权操作,并轮换相关凭据。

六、这个漏洞有什么值得学习的地方?

我觉得这个漏洞比普通的安全漏洞更值得研究。

它体现了几个软件工程中非常经典的问题。

1. Sentinel Value(哨兵值)导致的语义冲突

这是整个漏洞最核心的设计问题。

两个不同模块:

  • Linux 用 * 表示账户不能密码登录。
  • musl 用 * 表示密码哈希计算失败。

两个模块各自看起来都没有问题。

但当它们组合在一起,原本代表失败的值却成为了成功的条件。

这属于典型的 In-band Signaling(带内信号)。

也就是使用正常数据空间中的某个特殊值,来表达额外的控制信息。

类似的问题在其他地方也经常出现:

  • 用 -1 表示数组查找失败。
  • 用 0 表示某个数值无效。
  • 用空字符串表示数据不存在。
  • 用 NULL 同时表达不同类型的异常。

现代语言中,很多时候会用 Result、Option、异常类型等方式,让错误状态与正常数据明确分离。

2. 安全修复可能引入新的安全问题

OpenSSH 2016 年修复 CVE-2016-6210,是为了避免用户名枚举。

它引入了借用其他用户 salt 的机制。

这本来是合理的安全措施。

但它改变了原先的代码执行路径,使得特殊账户也进入了密码哈希计算逻辑。

这说明安全修复不能只看原始漏洞是否解决,还要考虑对整个系统行为的影响。

3. 测试环境覆盖不足

作者在文章中提到,他构建了多种组合:

  • glibc / musl
  • 使用 PAM / 不使用 PAM
  • 使用 shadow / 不使用 shadow

并进行了大约 17,600 次异常登录测试。

最终只有 musl + 无 PAM 的组合出现异常。

这里最值得注意的是:OpenSSH 的测试主要覆盖 OpenBSD 和 glibc 环境,而没有充分覆盖 musl 的组合。

这就是典型的跨组件兼容性测试盲区。


七、我对这个项目的评价

这篇文章有三个特别好的地方:

  1. 历史脉络清晰:从 1980 年的 Unix 账户锁定标记,一直追溯到 2026 年的漏洞发现。
  2. 原理解释直观:不是简单贴 CVE 描述,而是把 musl、OpenSSH、Alpine 的代码行为串起来。
  3. 有在线 Demo:可以直接输入不同长度的密码,观察 Debian/glibc 与 Alpine/musl 的差异。

还有一个值得关注的细节:

作者提到自己使用了 Agent Swarms,构建不同环境、生成大量异常登录输入,做差分测试(Differential Testing)。

这很有意思。

传统安全研究人员可能会人工阅读代码,寻找可疑逻辑;而这里采用了:

构建多种运行环境
       ↓
生成大量异常输入
       ↓
让不同环境执行相同测试
       ↓
比较认证结果
       ↓
发现只有特定组合出现异常
       ↓
深入分析代码路径

这其实是 AI Agent 在软件安全研究中一个很有价值的应用方向。

我的一个判断: 这个案例真正值得记住的,不只是 OpenSSH 的一个漏洞,而是一个跨越 libc、认证服务、操作系统账户管理三个层次的组合缺陷。单独审计每个组件,很可能都看不出问题。

原文建议收藏:*sshole 官方技术分析。

如果继续深入,我建议下一步直接研究 OpenSSH 的 auth_password()、xcrypt() 和 pick_salt() 这几个函数,结合 musl 的 crypt_sha512() 源码,把这个漏洞从源码级别走一遍。这会比单纯看 CVE 描述更有收获。