【吃瓜速修】OpenSSH严重漏洞:长密码绕过认证
原创
泼猴
泼猴
表哥带我
2026年11月6日 11:46
广东
去阅读
OpenSSH 十年暗洞:一条 257 字节的密码,怎么把 \* 号账户全变成后门
昨天(11 月 5 日)OpenSSH 项目组扔了个 10.6p2 的补丁版出来,没有预告,没有发版投票,Damien Miller 在 release notes 里原话是\"别读了,如果你跑的是 musl libc 又没开 PAM,现在就去把 PasswordAuthentication 关了\"。
这个洞代号 \*sshole——注意开头那个星号,不是玩梗,是字面意义上的星号。漏洞的全部魔力,就发生在 /etc/shadow里那个 *字段和 musl libc 的 crypt()函数之间。
潜伏时间:接近十年。
事件时间线
| 时间 | 事件 | 关键细节 |
|---|---|---|
| ~2016 年 | musl libc 的 crypt()实现落地\"超长密码返回 *作错误标记\"逻辑 |
设计本意是防止 DOS,因为 DES/crypt 类算法本来就只吃前 8 字节;musl 选择直接返回 *而不是截断 |
| 同期 | OpenSSH 在无 PAM 构建下,遇到 shadow 字段为 *(无密码账户)时,会借另一个账户的 salt来做 hash 比对 |
这段\"借 salt\"逻辑本来是为了让 key-only 账户不至于在密码认证阶段直接崩掉 |
| 2026-11-05 | 研究者 Özgün Kültekin(Wakefield)把报告递到 OpenSSH 安全邮箱 | OpenSSH 当天确认、当天出 10.6p2、Alpine 安全组同步发公告 |
| 2026-11-05 | Alpine Linux 发布全套补丁包,从 3.16 到 edge 全分支覆盖 | 连已经 EOL 三天的 3.21 都专门回了移植补丁 |
| 2026-11-06 | NVD 收录 CVE-2026-123158,CVSS 3.1 初评 8.1(High) | 评分还在动,NVD 分析挂起中 |
这个洞到底是怎么炸的
正常流程应该是这样
sshd 没开 PAM 的时候,密码认证是它自己处理的:
- 用户输入密码
hello123 - sshd 调
crypt(输入密码, 存储的hash),把输入密码用存储 hash 里的 salt 重新 hash 一遍 - 比对新算出来的字符串和 shadow 里存的字符串,一样就放行
问题出在两个\"各管各\"的决策撞车
撞车点 1:某个账户(比如你 adduser建的、只发 SSH key 的运维账户)在 /etc/shadow里长这样:
deploy:*:19800:0:99999:7:::
那个 *不是密码,是\"这个账户没有密码,谁都别想用密码登进来\"的意思。这是 Unix 几十年的老惯例。
撞车点 2:sshd 看到 *太短,里面没有合法 salt,它不会直接拒绝,而是从同机器另一个带 $5$或 $6$hash 的账户那里借一个 salt 过来,再拿这个 salt 去 hash 攻击者提交的密码。
撞车点 3(最骚的):musl 的 crypt()有个规定——当传入的密码超过 256 字节时,它不 hash 了,直接返回字符串 *表示\"输入非法,老子不干了\"。
于是攻击链就闭合了:
攻击者发一个 300 字节的密码
↓
sshd 看到目标账户 shadow 字段是 *
↓
从隔壁某真实账户借个 $6$salt$xxx 过来
↓
调 musl 的 crypt(300字节垃圾, 借来的salt)
↓
musl 一看:你这密码超 256 字节了,返回 \"*\"
↓
sshd 把 crypt() 返回的 \"*\" 和 shadow 里存的 \"*\" 做字符串比对
↓
两个 \"*\" 一模一样 → 认证通过 → 拿到 shell
不需要知道任何密码,不需要爆破,一次连接,必中。
Alpine 安全组的原文写得很直白:\"It takes one connection, works every time, and needs no retries.\"
谁在目标上
OpenSSH 开发者 Damien Miller 自己讲得很清楚:
❝
\"This vulnerability does not exist on non-Linux systems, Linux/glibc systems and Linux systems that use PAM for password authentication.\"
| 你的系统 | 中招没 |
|---|---|
| Alpine Linux(默认 musl + 无 PAM 的 openssh-server) | 中 |
| OpenWrt / Buildroot / Yocto 里编了 OpenSSH + musl + 无 PAM | 大概率中 ,看具体固件配置 |
| Debian / Ubuntu / RHEL / CentOS / Fedora(glibc + PAM) | 不中 |
| macOS / \*BSD / Windows | 不中 |
任何已经 PasswordAuthentication no的机器 |
不中(密码路径直接关了) |
用 openssh-server-pam且 UsePAM yes的 Alpine |
不中 |
密码字段是 !的锁定账户 |
不中(那种是彻底锁死,不是 *) |
| 容器里没跑 sshd 的 Docker 用户 | 不中 |
但光看发行版不够,你还得同时满足下面四个条件才真的可被打:
- sshd 确实在用 musl 的
crypt()自己验密码(没走 PAM) PasswordAuthentication yes(这是 OpenSSH 默认值,很多人从来没改过)- 机器上至少存在一个 shadow 字段为
*的账户——Alpine 基础镜像从 3.16 开始root默认就是root:* - 机器上至少存在一个带
$5$(SHA-256)或$6$(SHA-512)hash 的真实账户——这个几乎是必然,因为管理员总得给自己设个登录密码
最阴的一个场景:Alpine 官方的 tiny-cloud 镜像(AWS AMI / Azure VHD / qcow2)里带一个叫 alpine的默认用户,密码字段是 *,而且配置了免密 doas 到 root。这意味着攻击者一旦通过这个洞登进 alpine,doas 一下就是 root。Alpine 官方自己都承认了:首启安全,但只要你后来给任何账户设了密码(cloud-init 加用户、控制台改密码),这台机器立刻变成敞开门的状态。
本地自检
纯环境探测
#!/bin/bash
# sshhole-check.sh — CVE-2026-123158 自检
# 用法: sudo bash sshhole-check.sh
set -u
echo \"===== sshhole (CVE-2026-123158) self-check =====\"
# 1. 是不是 musl 系统
MUSL=0
if ldd --version 2>&1 | grep -qi musl; then
echo \"[1/4] libc: musl detected\"
MUSL=1
else
echo \"[1/4] libc: $(ldd --version 2>&1 | head -1) — not musl, likely not affected\"
fi
# 2. OpenSSH 版本
echo \"[2/4] sshd version: $(ssh -V 2>&1)\"
# 3. sshd 实际生效配置
echo \"[3/4] sshd effective config:\"
sshd -T 2>/dev/null | grep -Ei 'passwordauthentication|usepam|permitrootlogin' | sed 's/^/ /'
# 4. shadow 里有几个 * 账户、有没有 $5$/$6$ hash
STAR_ACCTS=$(awk -F: '$2 == \"*\" {print $1}' /etc/shadow)
HASH_ACCTS=$(awk -F: '$2 ~ /^\\$[56]\\$/ {print $1}' /etc/shadow)
echo \"[4/4] shadow field analysis:\"
echo \" accounts with '*' (key-only/no-pass): $(echo \"$STAR_ACCTS\" | grep -c . )\"
[ -n \"$STAR_ACCTS\" ] && echo \" -> $(echo $STAR_ACCTS | tr '\
' ' ')\"
echo \" accounts with \\$5\\$/\\$6\\$ hash: $(echo \"$HASH_ACCTS\" | grep -c . )\"
# 综合判定
AFFECTED=0
if [ \"$MUSL\" = \"1\" ]; then
PAM=$(sshd -T 2>/dev/null | awk '/^usepam/{print $2}')
PW=$(sshd -T 2>/dev/null | awk '/^passwordauthentication/{print $2}')
NSTAR=$(echo \"$STAR_ACCTS\" | grep -c .)
NHASH=$(echo \"$HASH_ACCTS\" | grep -c .)
if [ \"$PAM\" = \"no\" ] && [ \"$PW\" = \"yes\" ] && [ \"$NSTAR\" -ge 1 ] && [ \"$NHASH\" -ge 1 ]; then
AFFECTED=1
fi
fi
echo
if [ \"$AFFECTED\" = \"1\" ]; then
echo \">>> RESULT: VULNERABLE — patch now or set PasswordAuthentication no\"
else
echo \">>> RESULT: not exploitable under current config (still patch when you can)\"
fi
把这段存成 sshhole-check.sh,chmod +x之后 sudo 跑。输出 VULNERABLE的,别犹豫,往下看修复那节。
测试
如果你手头上有一台自己的、可以随便重装的Alpine 虚拟机,想亲眼验证这个绕过,流程是这样:
# ===== 在目标测试机上(Alpine 虚拟机,root 执行)=====
# 1. 建一个 key-only 账户(密码字段自动是 *)
adduser -D -s /bin/sh deploy
usermod -p '*' deploy
# 注意:这台机器上你自己 root 登录时设过密码,所以 $6$ hash 已经存在
# 2. 确认 sshd 配置
grep -E '^PasswordAuthentication|^UsePAM' /etc/ssh/sshd_config
# 应该看到 PasswordAuthentication yes,且没有 UsePAM yes
# ===== 从另一台机器(攻击机)发起验证 =====
# 3. 生成一个 300 字节的超长密码字符串
LONGPASS=$(python3 -c \"print('A'*300)\")
# 4. 用超长密码尝试以 deploy 登录(deploy 根本没设密码)
sshpass -p \"$LONGPASS\" ssh \\
-o StrictHostKeyChecking=no \\
-o PreferredAuthentications=password \\
-o PubkeyAuthentication=no \\
deploy@<目标IP> 'id; cat /etc/os-release | head -2'
第 4 步如果打印出 uid=...(deploy)和 Alpine 的版本信息——你就复现了。正常情况下,deploy这个账户你根本不知道密码(也确实没密码),ssh 一定会打 Permission denied。但在受影响的机器上,一次就进。
修复
路线 A:能治本
Alpine 已经给全分支出了包,直接:
apk update
apk upgrade openssh
rc-service sshd restart # 关键!不重启 sshd,跑的还是老进程
各分支修复版本号对一下:
| Alpine 分支 | 修复后 openssh 版本 |
|---|---|
| edge | 10.6\_p2-r0 |
| 3.24 | 10.3\_p1-r2 |
| 3.23 | 10.2\_p1-r1 |
| 3.22 | 10.0\_p1-r11 |
| 3.21 | 9.9\_p2-r1 |
| 3.20 | 9.7\_p1-r6 |
| 3.19 | 9.6\_p1-r3 |
| 3.18 | 9.3\_p2-r4 |
| 3.17 | 9.1\_p1-r7 |
| 3.16 | 9.0\_p1-r6 |
\\升级完必须 rc-service sshd restart\\。Alpine 安全组专门强调了这件事:包升级不会替换正在跑的守护进程,你不重启等于没修。
路线 B:治标不治本,但马上生效
如果你暂时升不了级——比如固件在路由器里、Yocto 镜像要重新烧录——先把密码认证路径掐了:
# 保险起见,先确认你的 SSH key 能登进去,另开一个会话留着别关
mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
# 验证配置语法,再重启
sshd -t && rc-service sshd restart
# 看一眼结果,应该输出 passwordauthentication no
sshd -T | grep -i passwordauthentication
做这步之前,千万确认你自己的公钥已经在目标机器的 authorized\_keys 里,并且另开一个终端测试过新 key 登录。手一抖把密码和 key 都关了,云主机只能去控制台救援。
路线 C:检查有没有被打过
Alpine 给了排查清单,我搬过来:
#日志里有没有\"用密码登了一个本来没密码的账户\"这种鬼话
grep 'Accepted password for' /var/log/messages | grep -E ' (alpine|root|deploy|git|) '
# 查 doas / sudo 的免密记录
cat /etc/doas.conf 2>/dev/null
cat /etc/sudoers 2>/dev/null
ls /etc/sudoers.d/ 2>/dev/null
# 看最近有没有被新建账户、被塞 authorized_keys
awk -F: '$3>=1000{print $1\":\"$3}' /etc/passwd
find /home -name authorized_keys -newer /etc/hostname -exec ls -la {} \\;
任何一条对得上、你又没法确定是自己干的—— 别犹豫,整机重装,别想着\"我先清一下后门\"。这种级别的未授权访问,留着就是定时炸弹。
几句题外话
这个洞最值得琢磨的地方,不是它有多难,而是它有多平淡。
musl 的作者做了一个完全合理的决定:密码太长就拒绝,返回 *。OpenSSH 的作者做了一个完全合理的决定:shadow 里的 *是个占位符,别因为它崩了,借个 salt 继续走流程。两边代码都没问题,十年里无数人 review 过,但两个\"合理\"撞在一起,就变成了\"发 300 个 A 就能登录\"。
这不是第一回,也不会是最后一回。2024 年的 regreSSHion 是信号处理竞态,同一年的 XZ 后门是压缩库被供应链污染,今天这个是 libc 错误返回值和 shadow 占位符的语义撞车。sshd 的核心协议逻辑早就被磨得很光滑了,真正出事的地方全在它和周边世界的接缝上。
对运维的实际启发就一条:如果你跑的是 musl 系发行版(Alpine、OpenWrt、各种嵌入式),PasswordAuthentication no不应该是\"最佳实践\",应该是默认值。key-only 不是更安全的选项之一,是唯一选项——这次被打穿的,恰恰就是那些\"我已经用 key 登录了,密码字段反正就是个 *,无所谓\"的机器。
对此你怎么看在评论区发表你的意见吧
其他推荐阅读
【吃瓜】如何看待手机中的数据采集拉低性能
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论