CVE-2026-123158:一个星号“\*”的十年歧义,让 Alpine 的 SSH 大门被 300 个“a”敲开
原创
网络安全民工
网络安全民工
网络安全民工
2026年11月6日 19:14
广东
去阅读
一、漏洞定性:一个星号,两种含义,一次握手
2026 年 11 月 5 日,OpenSSH 发布紧急版本 10.6p2,修复了一个让运维和安全团队同时皱眉的漏洞:CVE-2026-123158,昵称“sshole”。它的利用方式简单到近乎反直觉:向一个本应禁止密码登录的账户,发送一段超过 256 字节的密码(实测 257 至 1024 字节,例如 300 个重复的字母 A),就可能直接拿到 shell。
| 项 | 值 |
|---|---|
| 漏洞类型 | 密码认证绕过(CWE-394:意外状态码或返回值) |
| CVSS 3.1 | 8.1 High(AV:N/AC:H/PR:N/UI:N) |
| 影响组件 | OpenSSH < 10.6p2,运行在 musl libc + 无 PAM 形态下 |
| 修复版本 | OpenSSH 10.6p2;Alpine 3.16–edge 全分支回溯包 |
| 认证要求 | 无需凭证(但需知道一个有效账户名且网络可达 SSH 端口) |
| 公开 PoC | 已有多个,GitHub 上存在可运行仓库 |
| 在野利用 | 截至 11 月 5 日未确认,但利用门槛极低,公开实验室已就绪 |
“High”而非“Critical”的原因写在向量里:AC:H(攻击复杂度高)。漏洞要求五条本地条件同时成立,缺一不可。一旦条件满足,利用却是确定性的——Alpine 的公告直白地写道:“一次连接,每次都成功,不需要重试。”
二、根因:两个“\*”在十年间被撞成了同一个字符串
这个漏洞的根源不是 OpenSSH 某一行代码写错了,而是三个各自合理的设计选择在时间中靠拢,最终语义冲突。
2012 年,musl 给 crypt() 加了“报错星号”。 musl 的 SHA-256/SHA-512 crypt() 将密码长度限制为 256 字节,防止超长密码拖垮哈希资源。超长时,它不返回 NULL,而是返回字面字符串 \"*\" 表示失败。glibc 在同样场景下返回 NULL 或 *0,不会与 shadow 中的 * 相等。
2016 年,OpenSSH 为防用户名枚举,让 * 账户也走哈希。 CVE-2016-6210 的修复要求 sshd 对所有账户——包括没有密码的 * 账户——都调用 crypt(),并借用其他账户的 salt 来统一响应耗时。这使 * 账户进入了“哈希比较”的路径。
2022 年,Alpine 把 root 改成 root:*。 目的是让 root 仍能用密钥登录,同时禁止密码登录。这让“系统上存在 * 账户”从个别现象变成了 Alpine 的默认事实。
当攻击者向一个 * 账户发送超过 256 字节的密码时,musl 的 crypt() 返回 \"*\",shadow 中存储的也是 \"*\",字符串比较相等,sshd 记录 Accepted password 并给出 shell。OpenSSH 维护者 Damien Miller 称修复是“workaround”,因为“根因在 sshd 之外”;musl 维护者 Rich Felker 也同意这是调用方的问题。两边都修了,但都不认领 bug。
一个容易被忽略的技术细节:根据 GitHub 上 PoC 仓库的逆向分析,利用通常需要在同一条 SSH 连接内完成两次尝试。第一次时,pick_salt() 的 getspnam() 循环会覆盖 musl 的全局静态缓冲区,导致 crypt() 返回 * 后比较失败;第二次时,salt 缓存命中,循环被跳过,缓冲区干净,strcmp(\"*\", \"*\") == 0,登录被接受。这也是为什么部分公开报告说“一次成功”,而技术分析观察到“首次失败、第二次成功”——两者指向同一个根因。
三、五条件模型:谁真正踩雷
漏洞要求五条条件同时成立,缺一不可:
条件一:musl libc,且 sshd 未走 PAM。 Alpine 默认的 openssh-server 包以 --without-pam 构建,直接命中。即使安装了 openssh-server-pam,如果显式设置 UsePAM no,仍会回到无 PAM 路径。
条件二:PasswordAuthentication yes。 这是 OpenSSH 的默认值,除非被手动关闭。
条件三:存在一个密码字段为 * 的账户。* 表示“该账户不匹配任何密码”,常用于仅密钥登录的用户、目录服务账户,以及 Alpine 3.16 起默认的 root。锁定账户(!)不受影响,区别在于 * 账户仍会进入哈希比较路径。
条件四:至少一个其他本地账户拥有 $5$ 或 $6$ 格式的真实密码哈希。 sshd 需要从这个“donor”账户借用 salt 来完成哈希计算。一台装过任何真实密码账户的机器,几乎必然满足。
条件五(技术细节):利用通常需要在同一条 SSH 连接内完成两次尝试,以绕过 pick_salt() 的静态缓存污染。
不受影响的范围是清晰的:Debian、Ubuntu、RHEL/Fedora、SUSE 等使用 glibc 的发行版不受影响;任何启用 PAM 的 sshd 不受影响;使用 Dropbear 的嵌入式系统(如 OpenWrt 默认)不受影响;不运行 sshd 的容器不受影响。
需要自查的是 musl 世界:Alpine 首当其冲。Alpine 官方 tiny-cloud 镜像内置的 alpine 用户密码字段为 *,且拥有免密 doas 提权权限。一旦机器上出现任意一个 $6$ 哈希,攻击者就能以 alpine 身份登入,再用 doas 升到 root。一个低权限账户的认证绕过,在暴露的云主机上直接演变为整机失陷。
四、Python 检测脚本(安全验证版)
以下脚本仅验证认证绕过是否成立,不发送任何命令执行载荷。适合在授权环境中确认漏洞是否存在。
#!/usr/bin/env python3# -*- coding: utf-8 -*-\"\"\"CVE-2026-123158 SSHole 认证绕过检测(只读,无命令执行)用法: python cve_2026_123158_check.py <target> <username> [port]仅尝试使用超长密码登录,验证是否成功,不执行任何命令。\"\"\"import sysimport paramikodef check(target, username, port=22): print(f\"[*] 目标: {target}:{port}\") print(f\"[*] 目标账户: {username}\") # 超长密码(257-1024 字节) password = \"A\" * 300 client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect( hostname=target, port=port, username=username, password=password, timeout=10, allow_agent=False, look_for_keys=False, ) print(\"[!] 漏洞存在:使用 300 字节密码成功登录\") print(\"[!] 该账户本应禁止密码登录(shadow 字段为 *)\") client.close() return True except paramiko.AuthenticationException: print(\"[+] 认证失败,目标可能不受影响或条件不满足\") return False except Exception as e: print(f\"[-] 连接失败: {e}\") return Falseif __name__ == \"__main__\": if len(sys.argv) < 3: print(f\"用法: {sys.argv[0]} <target> <username> [port]\") sys.exit(1) target = sys.argv[1] username = sys.argv[2] port = int(sys.argv[3]) if len(sys.argv) > 3 else 22 result = check(target, username, port) sys.exit(0 if result else 1)
说明:该脚本使用 paramiko 库发起 SSH 连接,仅验证认证是否被绕过,不执行任何远程命令。如果返回成功,说明目标存在漏洞;如果返回 AuthenticationException,说明条件不满足或目标已修复。
五、Nuclei 模板(安全指纹版)
由于 SSH 协议的交互性,Nuclei 无法直接验证认证绕过。以下模板仅做产品指纹与配置探测,不发送超长密码或任何利用载荷。
id: CVE-2026-123158-sshole-detect-safeinfo: name: CVE-2026-123158 SSHole - Safe Fingerprint author: security-research severity: high description: | 检测目标是否运行 OpenSSH 且可能为 musl 环境。不发送超长密码, 不尝试认证绕过。实际验证需结合主机侧检查。 reference: - https://www.cve.org/CVERecord?id=CVE-2026-123158 - https://sshhole.com/ classification: cvss-metrics: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H cvss-score: 8.1 cwe-id: CWE-394 tags: cve,cve2026,openssh,alpine,detectcode: - engine: - bash args: - \"-c\" pattern: | timeout 5 bash -c 'exec 3<>/dev/tcp/{{Hostname}}/22; head -1 <&3' 2>/dev/null || echo \"SSH_UNREACHABLE\" matchers: - type: word part: response words: - \"SSH-\" - type: word part: response words: - \"OpenSSH\" case-insensitive: true condition: or
运行方式:nuclei -t CVE-2026-123158-sshole-detect-safe.yaml -u target.com -code
说明:该模板仅读取 SSH 横幅,确认 OpenSSH 的存在与版本线索。无法从外部判定 musl、PAM、* 账户和 donor 哈希这四条关键条件,因此实际漏洞确认必须依赖主机侧的配置检查。
六、修复与加固:升级、重启、看包版本
第一步:升级。 上游 OpenSSH 10.6p2。修复逻辑是“当存储的密码哈希短于 4 个字符时拒绝匹配”,把 * 这种禁用标记排除出比较路径。Alpine 已将修复回溯至 3.16 至 edge 全部分支。
# Alpineapk update && apk upgrade -U -arc-service sshd restart
两个必须注意的坑:
坑一:升级不重启 sshd。apk upgrade -U -a 只替换磁盘上的二进制,运行中的守护进程仍加载着旧代码。必须执行 rc-service sshd restart,否则漏洞在内存中继续生效。
坑二:banner 会骗人。 打过补丁的机器,sshd -V 或握手 banner 可能仍显示旧版本号。排查应以包版本为准:apk info openssh,而不是只看 SSH 横幅。
如果暂时不能升级,在服务端关闭密码认证即可封堵利用通道:
# 3.19+ 支持 drop-in 目录echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf# 3.16-3.18 需直接写入 /etc/ssh/sshd_configsshd -t && rc-service sshd restart
必须先确认密钥登录可用,否则会把自己锁在门外。
注意:在未启用 PAM 的 musl 系统上,不要将 /etc/shadow 中的 * 改为 !,否则会导致合法用户无法登录。
七、入侵排查:核心 IoC 是“\* 账户的 Accepted password”
如果服务器满足上述条件且曾暴露于不可信网络,需要判断是否已被利用。最值得关注的信号是:密码字段为 * 的账户,却出现了 SSH 密码认证成功的日志。
# Alpinegrep \"Accepted password\" /var/log/messages# Debian/Ubuntugrep \"Accepted password\" /var/log/auth.log# systemdjournalctl -u sshd --since \"7 days ago\"
提取潜在目标账户:
awk -F: '$2==\"*\"{print $1}' /etc/shadow
交叉比对:如果上述账户在日志中出现 Accepted password,应视为强失陷信号。随后检查异常用户、doas/sudo 配置、authorized_keys 植入、近期修改的系统文件、异常外联与监听端口。Accepted password 本身不能单独证明漏洞被利用,需要结合账户密码字段、认证配置、登录来源和补丁状态进行关联分析。
八、结语:一个“\*”的歧义,防守不能只押“升级”一个点
这个漏洞没有炫技的内存破坏,也没有复杂的利用链。它只是三个各自合理的设计选择,在十年里慢慢靠拢,把一个“函数报错的星号”和“账户禁用的星号”撞成了同一个字符串。攻击者要的只是一段够长的密码和一个已知的用户名。
对防守方而言,glibc 系统天然免疫,但 musl 世界的排查必须落到五条件上:是否无 PAM、是否开密码登录、是否有 * 账户、是否有 donor 哈希、以及同连接两次尝试的缓存状态。跑 Alpine 容器和云镜像的团队,优先级很清晰:升到 10.6p2 或对应回溯包、重启 sshd、用包版本而非 banner 确认、临时关密码认证、再回头查日志看是否已经被打过。 这个洞的窗口很窄,但一旦命中,门是直接敞开的。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论