输入 300 个 A,就能 SSH 登进\"没密码\"的账号?
原创
pippybear
pippybear
安全无界
2026年11月6日 09:06
上海
去阅读
在 musl libc(Alpine、OpenWrt、Buildroot、Yocto 以及大量 Docker 镜像)上,不走 PAM 的 OpenSSH,会让 shadow 里密码字段为
*的账号接受任何 257~1024 字符的\"密码\"。root、纯密钥的部署账号、LDAP 账号、Alpine 云镜像的默认用户,都在影响范围内。
近日,安全研究员 Ozgun Kultekin(Wakefield)公开了这个被称为\*sshole的问题。披露页面称,这个行为自2016 年起就存在于 musl 平台的 OpenSSH 中,已经潜伏近十年。
说明:本文信息主要来自披露页面(sshhole.com)及其引用的公开提交记录。截至撰文,页面未标注本问题的 CVE 编号,修复版本以页面所述的 OpenSSH 10.6p2 为准,请以 OpenSSH、Alpine 官方公告为准。
一、漏洞原理
这个漏洞不是某一行代码写错了,而是几个各自合理的设计,在十年间叠在一起,最后产生了一个认证绕过。
1. 背景:\* 代表\"没有任何密码能匹配\"
早期 Unix 把密码哈希直接放在 /etc/passwd。1980 年 Bill Joy 给 daemon 账号的密码字段写了一个 ,表示这个账号不允许登录。 不是合法哈希,没有任何密码能算出它,这个约定沿用至今。
现在哈希放在 /etc/shadow,形如 $6$salt$hash($6$ 表示 sha512)。登录时 sshd 用同样的 salt 对用户输入做哈希,比对结果是否一致。字段是 \* 时,正常情况下永远比不上。
2. 第一块拼图:musl 的 crypt() 失败时返回 \"\*\"
按 POSIX,crypt() 失败应返回 NULL,但很多程序不检查 NULL,直接拿返回值比较,结果崩溃。
2012 年 8 月,musl 改成:crypt() 失败时不再返回 NULL,而是返回一个\"不可能匹配\"的字符串,字符选的是 \*。提交说明的意思是,\"总是成功但返回不可匹配哈希\"比\"出错失败\"更好。
3. 第二块拼图:sha512crypt 的 256 字节上限
20 天后,musl 加入 sha512crypt。由于 sha512crypt 的耗时随密码长度增长,攻击者可以用超长密码拖住服务器。所以 musl 加了一个 KEY\_MAX 256 的上限:密码超过 256 字节,直接判定失败,即返回\"\*\"。
4. 第三块拼图:sshd 借别人的 salt 来\"拖时间\"
2016 年,OpenSSH 修复 CVE-2016-6210(通过响应时间差枚举用户名):对不存在的用户,sshd 以前直接拒绝,比真实用户快几毫秒,攻击者可据此判断用户是否存在。
修法是:没有真实哈希可比对时,sshd借用另一个账号的 salt,照样对你输入的密码算一遍哈希,只为消耗同样的时间,最终仍然拒绝。
页面指出,这个\"没有真实哈希\"的分支,除了不存在的用户,也包括 shadow 字段为\*的真实用户。
5. 三块拼图叠加
对于 shadow 字段为 \* 的账号,攻击者提交一个 257~1024 字符的密码:
- sshd 借用另一个账号的
$6$salt,调用crypt()处理这个密码; - 密码超过 256 字节,musl 的 sha512crypt 失败,
crypt()返回\"*\"; - sshd 把返回值
\"*\"与 shadow 里的\"*\"比较,相等; - 认证通过,登录成功。
6. 为什么十年没被发现
披露页面给出的原因:
- 全新系统没设置任何密码时不受影响,需要系统里还有一个带真实哈希的账号;
- 触发需要一个超长的\"错误密码\",日常不会碰到;
- OpenSSH 的上游测试跑在 OpenBSD 和 glibc 上,musl 下没有 sshd 的测试;
- 大多数发行版走 PAM,而 PAM 对
*字段根本不会调用crypt()。
页面还提到,Debian 在 2013 年就有人建议 glibc 借鉴 musl 的 \"\*\" 做法;2024 年 12 月,musl 邮件列表上也有人讨论 busybox passwd 在 257 字符密码下的异常,但都没有联系到 sshd。研究者表示,他是在对 sshd 做大规模差分测试(glibc、musl,PAM、非 PAM,shadow、非 shadow,约 17,600 次异常登录)时,发现\"musl 且无 PAM\"这个组合与其他所有组合行为不一致,从而定位到问题。
二、影响范围
触发条件(需同时满足)
| 条件 | 说明 |
|---|---|
| 运行在 musl 上 | Alpine、OpenWrt、Buildroot、Yocto,以及大量基于 Alpine 的 Docker 镜像 |
| sshd 不走 PAM | Alpine 的 openssh-server 默认不使用 PAM,或配置了 UsePAM no |
| 开启密码登录 | PasswordAuthentication 默认开启 |
存在 * 账号 |
shadow 密码字段为 *:root、纯密钥用户、LDAP 用户、Alpine 云镜像默认用户 |
| 存在其他真实密码账号 | 供 sshd 借用 salt |
满足以上条件时,该 \* 账号接受任何 257~1024 字符的密码。
危害程度
- 在 Alpine 官方的 AWS、Azure 镜像上,默认用户
alpine带有免密doas,绕过认证等于直接拿到 root; - 如果 root 账号本身是
*且允许 root 登录,则直接获得 root; - 任何
*账号被登录后,攻击者都能以该账号身份落地,作为横向移动的起点。
不受影响的环境
- glibc 系统(Debian、Ubuntu、Fedora 等);
- 走 PAM 认证的系统;
- Dropbear;
- OpenBSD。
容易被忽略的暴露面
- 容器镜像:很多业务镜像基于 Alpine,并顺手装了
openssh-server方便调试,且从未清理; - 嵌入式与网络设备固件:OpenWrt、Buildroot、Yocto 构建的设备;
- 云上 Alpine AMI / 镜像:默认用户 + 免密提权的组合最危险;
- 暴露在公网或内网跳板上的 22 端口。
三、如何排查
第 1 步:判断主机是否在影响面内
# 是否 muslldd --version 2>&1 | grep -i musl # OpenSSH 版本(页面称 10.6p2 修复)ssh -V # sshd 的生效配置:是否走 PAM、是否允许密码登录、root 登录策略sshd -T | grep -Ei 'usepam|passwordauthentication|permitrootlogin' # 列出密码字段为 * 的账号awk -F: '$2==”*”{print $1}' /etc/shadow # 是否存在带真实哈希的账号awk -F: '$2 ~ /^\\$/{print $1}' /etc/shadow
判定:musl、usepam no、passwordauthentication yes、存在 \* 账号,同时满足即为受影响。
第 2 步:查日志中\"不该出现的密码登录\"
核心思路:\ 账号在正常情况下不可能通过密码登录成功。只要日志里出现 Accepted password for <某个 \ 账号>,就非常可疑。
# Alpine 默认使用 busybox syslog;有 journald 的系统改用 journalctl -u sshdLOG=/var/log/messages for u in $(awk -F: '$2==”*”{print $1}' /etc/shadow); do grep ”Accepted password for $u ” ”$LOG”done
关注点:
- 来源 IP 是否陌生、是否来自公网;
- 登录时间是否异常;
- 配合
last、lastb、wtmp 查看登录会话; - 日志轮转和保留周期是否足够:该行为自 2016 年存在,但实际能追溯的窗口通常很短,日志里没有记录不能证明没被利用。
注意:sshd 日志不会记录密码内容,因此无法直接从日志里识别\"超长密码\",只能靠\"\* 账号出现密码登录成功\"这个异常来判断。
第 3 步:对自有主机做黑盒验证(仅限已授权资产)
sshpass -p ”$(python3 -c 'print(”A”*300)')” \\ ssh -o PubkeyAuthentication=no <锁定账号>@<主机> id
能登录成功,就说明该主机存在此问题。请只在你拥有或获得明确授权的系统上测试。
第 4 步:批量盘点(镜像和基础设施层)
- 扫描所有基于 Alpine 的镜像:
apk info openssh-server、Dockerfile 中的apk add openssh; - 用 Trivy、Grype 等工具对镜像里的 openssh 包版本做一次扫描;
- 检查云上 Alpine AMI、跳板机、堡垒机模板;
- 业务容器通常不需要 sshd,盘点出来后优先评估是否直接移除。
第 5 步:如果发现已被利用
按\"已失陷\"处理,不建议原地修复:
- 检查各用户
~/.ssh/authorized_keys是否被追加公钥; - 检查新增用户、
/etc/passwd与/etc/shadow变更; - 检查 cron、启动脚本、
/etc/doas.conf、/etc/sudoers; - 检查异常进程、异常外连、可疑文件;
- 轮换该主机上的所有凭据、密钥和 token;
- 重建主机或镜像,并保留取证数据。
四、修复与缓解
首选:升级
apk upgrade openssh
披露页面称OpenSSH 10.6p2已修复该问题。请同时关注 Alpine 等发行版的安全公告,确认各自修复版本。
暂时无法升级:关闭密码登录
echo ”PasswordAuthentication no” > /etc/ssh/sshd_config.d/00-no-passwords.confrc-service sshd restart
其他加固建议
- 如果业务允许,改用 PAM 认证路径;
- 用安全组、防火墙、堡垒机限制 22 端口的来源;
- 对云镜像默认用户做收敛:移除不必要的免密提权;
- 对不需要 sshd 的容器,直接去掉 openssh-server;
- 把\"musl + 非 PAM + 允许密码登录\"加入基线检查,纳入镜像准入和主机巡检。
五、小结
这个问题的有趣之处在于,它没有任何一个\"明显的错误\":
- 1980 年,
*表示不可登录; - 2012 年,musl 让
crypt()失败时返回\"*\",避免程序崩溃; - 2012 年,musl 为防 DoS 给密码设了 256 字节上限;
- 2016 年,OpenSSH 为防用户枚举,借别人的 salt 做假哈希;
- 2022 年,Alpine 为兼容 key 登录,把 root 的锁定标记从
!改成*。
每一步单看都合理,叠在一起,就变成了\"超长密码可以登录没有密码的账号\"。对安全团队来说,它也提醒我们:libc 和认证之间那一层\"没人看\"的胶水代码,值得被测试和审计。
参考资料
- \*sshole 披露页面:https://sshhole.com/
- musl 提交:crypt 失败时返回不可匹配哈希(2012):https://git.musl-libc.org/cgit/musl/commit/?id=b3c4cc121f70faea45389fe7ddc1127ed5cbd8bb
- musl 提交:加入 sha256/sha512 crypt(2012):https://git.musl-libc.org/cgit/musl/commit/?id=88bf5a8a8d7d796f63cca8589f4de67aa8345f1a
- musl
crypt_sha512.c(密码长度上限):https://git.musl-libc.org/cgit/musl/tree/src/misc/crypt\_sha512.c?id=88bf5a8a8d7d796f63cca8589f4de67aa8345f1a#n189 - OpenSSH 提交:为无效用户确定合适的 salt(2016,对应 CVE-2016-6210):http://lists.mindrot.org/pipermail/openssh-commits/2016-July/005428.html
- OpenSSH 提交:修复 bcrypt 哈希的伪造 salt 生成(2026-10-30):http://lists.mindrot.org/pipermail/openssh-commits/2026-October/013271.html
- Alpine aports:root 锁定标记由
!改为*(3.16):https://gitlab.alpinelinux.org/alpine/aports/-/commit/fbe926afc76de76e07d078f266badc4144dde8e3 - Debian Bug #714219(建议 glibc 采用
*作为失败返回):https://bugs.debian.org/714219 - musl 邮件列表:busybox passwd 与 257 字符密码讨论(2024-12):https://www.openwall.com/lists/musl/2024/12/31/2
- Single UNIX Specification:
crypt()返回值定义:https://pubs.opengroup.org/onlinepubs/7908799/xsh/crypt.html
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论