Sshole:帳號停用了密碼,為什麼還能通過 SSH 驗證

OpenSSH 在 2026 年 11 月 5 日發布 10.6p2,修補一個密碼驗證繞過。條件很窄:musl libc、sshd 沒用 PAM(編譯時未含,或有含但設 UsePAM no)、開放密碼登入,而且目標帳號在 /etc/shadow 的密碼欄剛好是單一一個 *。Alpine 的 openssh-server 預設就是這個組合,首當其衝,條件沒湊齊就不會中招。看到 Alpine 就當全部淪陷,或看到版本號沒更新就當安全,都會判斷錯。

OpenSSH 把 10.6p2 定位成 workaround,根本原因在 sshd 之外,但從 sshd 能觸發,就在 sshd 擋。glibc、非 Linux、用 PAM 做密碼驗證的系統都不受影響。發得這麼急,release notes 的說法是 AI 工具找到的漏洞最近好幾次很快又被別人獨立找到,不回報的對手也可能找到,所以改成修好就發,不等下一個排定版本。資料查核至 2026-11-05 22:00(台北時間),以 OpenSSH 公告、Alpine 官方 advisory 與原始研究為準。

成立的條件

六個條件要同時成立,缺一個就走不通。

元件要成立的狀態
libcmusl(Alpine 預設)
sshd編譯時未含 PAM,或 openssh-server-pam 設 UsePAM no
密碼登入PasswordAuthentication yes(OpenSSH 預設)
目標帳號/etc/shadow 密碼欄是單一 *
另一個帳號至少一個本機帳號有 $5$ 或 $6$ 雜湊
攻擊輸入一段 257 到 1024 byte 的超長密碼

下列任一成立就不走這條路:PasswordAuthentication no、openssh-server-pam 且 UsePAM yes、帳號密碼欄以 ! 開頭,或 libc 是 glibc。

三個各自合理的行為,怎麼組成繞過

musl 的 crypt() 在密碼超過 256 byte 時回傳字串 * 當錯誤指示,用一個不可能是正常雜湊的值代表失敗,讓算壞的結果不會剛好對上真實雜湊。

/etc/shadow 裡密碼欄填 * 代表這帳號不開放密碼登入,金鑰專用帳號、Alpine 的 root 預設都是這樣,一樣是用一個不會是雜湊的值表示沒有可比對的密碼。

OpenSSH 為了擋使用者列舉(CVE-2016-6210:sshd 對不存在的帳號回應較快,時間差會洩漏哪些帳號存在),修補後對無效帳號借另一個真實帳號的 salt 算一次雜湊,讓處理時間一致。這也套用在密碼欄是 * 的真實帳號,所以機器上得另有一個帶 $5$ 或 $6$ 雜湊的帳號,crypt() 才有 salt 可借。

問題出在兩個 * 是同一個值。musl 其實防過這一手:拿 * 當 salt 呼叫 crypt(),它回傳 x,只要呼叫端用同一個字串當 salt 又拿來比對,就撞不到。OpenSSH 遇到密碼欄只有一個字元的帳號,改用借來的 $6$ salt 去算,再跟原本的 * 比。攻擊者送一段 257 到 1024 byte 的密碼,超過 musl 的 256 byte 上限,crypt() 回傳 *,剛好等於密碼欄的 *,一次失敗的雜湊計算就被當成一次成功的比對。

這和〈洗牌只有 8,640 萬種:遊戲 RNG 認證在檢查什麼〉是同一種毛病:亂數來源和輸出要分開檢查。musl、shadow、OpenSSH 各自稽核都正常,約定也都成立,漏洞長在交界,錯誤訊號和合法資料共用同一個值。OpenSSH 說根本原因在 sshd 之外,還是在 sshd 擋,bug 算誰的可以再談,守在可被利用那一面的人先把洞補起來。

線索其實一直都在

這條路需要的每一塊,公開紀錄裡都出現過。2013 年有人在 Debian 開 bug(#714219),想讓 glibc 的 crypt() 失敗時也回傳 *;Debian 後來改用 libxcrypt,失敗回傳 *0,差一個字元就關掉了。2024 年 12 月 musl 郵件列表討論過 BusyBox 的 passwd 遇到 257 字元密碼會失敗,講的就是這個回傳 * 的行為,但沒有人接到 sshd。2026 年 10 月 OpenSSH 修 pick_salt() 的 commit,依原始研究的說法,這次的 bug 就在那段程式碼下面幾行,研究者是在那之後四天找到的。

每一塊單獨看都不像漏洞,原始研究自己的結論也很克制:「就目前所知,沒有人注意到」。

確認自己有沒有暴露

看到 Alpine 就全部重裝之前,先確認:

  1. 這台是不是 musl。ldd 指向 sshd,看到 ld-musl 就是 musl。
  2. sshd 有沒有用 PAM。套件裡沒有 PAM 檔,或 sshd -T 的 usepam 不是 yes,才符合條件。
  3. 密碼登入是不是開著,看 sshd -T 的 passwordauthentication。
  4. 有沒有密碼欄是單一 * 的帳號。Alpine 3.22 用 adduser -D 建的帳號是 !,不在這條路上。
  5. 有沒有另一個帶 $5$ 或 $6$ 雜湊的帳號。sshd 借它的 salt 算那次雜湊,少了它這條路也走不成。

root 不一定能直接用密碼進來,還要看 PermitRootLogin。實測的機器是 without-password,root 就算密碼欄是 * 也不能用密碼登入。一般帳號若能登入、又有免密碼 doas,才可能再往上拿到 root,Alpine 的 AWS 與 Azure 官方 image 預設的 alpine 帳號就有免密碼 doas。

# 在要檢查的主機上跑,全是唯讀查詢
ldd "$(command -v sshd)" | grep -i musl
sshd -T 2>/dev/null | grep -iE '^(passwordauthentication|usepam|permitrootlogin)'
awk -F: '$2=="*"{print $1}' /etc/shadow

下面是在 OrbStack 的 Alpine 3.19(aarch64、openssh-server 9.6_p1-r3)上跑的實際輸出。這個 image 的 9.6_p1-r3 已經是 advisory 列的修補版,拿它示範檢查指令,看到的是修過的機器。

/lib/ld-musl-aarch64.so.1 (0xffff95de0000)
libc.musl-aarch64.so.1 => /lib/ld-musl-aarch64.so.1 (0xffff95de0000)
permitrootlogin without-password
passwordauthentication yes
root

這個 build 沒編 PAM,sshd -T 印不出 usepam,在設定檔加 UsePAM no 反而會被 sshd -t 擋下(Unsupported option UsePAM)。有編 PAM 的 build 會印出來,值看設定,設 UsePAM yes 才不受這條路影響。最後一行的 root 就是密碼欄為單一 * 的帳號。

確認中招或想先止血,升級 openssh 後重啟 sshd。修補版在算完雜湊後檢查密碼欄長度,3 個字元以下(*、! 這類標記)直接判失敗,雜湊照算,時序防護還在。Alpine advisory 特別提醒,只升級套件不會換掉正在跑的 daemon。容器裡跑 sshd 的(跳板、SFTP、Git),要拉新的 base image 重建再部署。

還沒辦法升級就先把 PasswordAuthentication 設成 no 再重啟 sshd。改完用 sshd -T -C user=…,host=…,addr=… 確認,Match 區塊的覆寫要帶 -C 才看得到,不帶只會顯示全域值。如果目的是關掉所有密碼式登入,有開 PAM 的機器還要關 KbdInteractiveAuthentication,或直接設 AuthenticationMethods publickey,不然密碼還能從 keyboard-interactive 進來。保留現有連線、先測好金鑰登入,別把自己鎖在門外。

發行版多半會回植到舊分支,看實際安裝的套件版本,別只看上游版本號。Alpine 3.16 到 edge 各有對應的 -rN。

想回頭查有沒有被走過這條路,看 /var/log/messages 的 Accepted password for。密碼欄是 * 的帳號出現這行,就是被走過了。目前沒有已知在野利用的可靠證據。

編號與歸屬的界線

截至這幾份一手來源撰稿當下,沒有指派新的 CVE。Alpine advisory 寫「No CVE identifier had been assigned at the time of writing」,OpenSSH 公告與原始研究也沒給。文中的 CVE-2016-6210 是 2016 年的使用者列舉問題,只是歷史背景,不是這次的編號。看到某個 2026 年的 CVE 掛在這題上,和一手來源對不上就先存疑。

回報者是 Wakefield(wakefield.co)的 Özgün Kültekin。披露串下有人回覆說記得十年前有很相似的 SSH 問題,但沒附舊案、也沒認領這次,不能當成已有第二位獨立發現者。2024 年那串 musl 討論同理,和這次的 sshd 繞過是兩件事。

在容器裡自己重現

文中幾個關鍵行為可以用拋棄式容器自己跑。只做函式庫呼叫、設定檢查與反組譯,不起 sshd、不登入,跑完容器就刪。

docker run --rm alpine:3.22 sh -c '
apk add --no-cache openssh-server openssh-dbg gdb build-base >/dev/null 2>&1
cat /etc/alpine-release
apk info -v 2>/dev/null | grep -E "^(musl|openssh-server)-[0-9]"

# 1. musl crypt():同一個 * 當 salt 回 x;借來的 $6$ salt 遇到超長密碼回 *
cat > /tmp/c.c <<\EOF
#include <crypt.h>
#include <stdio.h>
#include <string.h>
int main(void) {
	char p[300];
	memset(p, 65, 5);   p[5] = 0;   printf("crypt(5B, \"*\")         -> %s\n", crypt(p, "*"));
	memset(p, 65, 256); p[256] = 0; printf("crypt(256B, \"$6$abc$\") -> len %zu\n", strlen(crypt(p, "$6$abc$")));
	memset(p, 65, 257); p[257] = 0; printf("crypt(257B, \"$6$abc$\") -> %s\n", crypt(p, "$6$abc$"));
	return 0;
}
EOF
gcc /tmp/c.c -o /tmp/c && /tmp/c

# 2. adduser -D 建的帳號是 !,不是 *
adduser -D demo && grep ^demo: /etc/shadow | cut -d: -f1,2

# 3. 沒編 PAM 的 build 不認 UsePAM
ssh-keygen -A >/dev/null 2>&1
echo "UsePAM no" > /etc/ssh/sshd_config.d/01-pam.conf
sshd -t 2>&1 | head -1
rm /etc/ssh/sshd_config.d/01-pam.conf

# 4. Match 覆寫要帶 -C 才看得到
printf "PasswordAuthentication no\nMatch Address 127.0.0.1\n\tPasswordAuthentication yes\n" >> /etc/ssh/sshd_config
sshd -T | grep -i ^passwordauthentication
sshd -T -C user=demo,host=localhost,addr=127.0.0.1 | grep -i ^passwordauthentication

# 5. 修補版在 strcmp 前多了 strlen 與 cmp #0x3(aarch64 反組譯)
gdb -batch -ex "disassemble sys_auth_passwd" /usr/lib/ssh/sshd-auth 2>/dev/null | grep -E "xcrypt|strlen|#0x3|strcmp"
'

在 OrbStack(aarch64)上的實際輸出:

3.22.7
musl-1.2.5-r13
openssh-server-10.0_p1-r11
crypt(5B, "*")         -> x
crypt(256B, "$6$abc$") -> len 93
crypt(257B, "$6$abc$") -> *
demo:!
/etc/ssh/sshd_config.d/01-pam.conf line 1: Unsupported option UsePAM
passwordauthentication no
passwordauthentication yes
   0x000000000000a67c <+148>:	bl	0x80a9c <xcrypt>
   0x000000000000a688 <+160>:	bl	0x7200 <strlen@plt>
   0x000000000000a694 <+172>:	cmp	x0, #0x3
   0x000000000000a6a4 <+188>:	bl	0x7fe0 <strcmp@plt>

x86_64 上的位址和指令不同,看 strlen 是否夾在 xcrypt 與 strcmp 之間就好。

補充筆記

  • 條件要全中:musl、sshd 無 PAM、開放密碼登入、目標帳號密碼欄單一 *、另有 $5$ 或 $6$ 帳號可借 salt、密碼 257 到 1024 byte。
  • 成因是兩個 * 撞在一起:musl 的錯誤回傳 * 等於 shadow 的停用 *,錯誤訊號和合法資料共用同一個值。
  • 10.6p2 是 workaround,根本原因在 sshd 之外但在 sshd 擋;glibc、非 Linux、用 PAM 的系統不受影響。
  • 自查:musl、PAM、PasswordAuthentication、* 密碼欄、可借 salt 的帳號,root 再看 PermitRootLogin;升級後要重啟 sshd,驗設定要用 sshd -T -C。
  • 沒有確認的新 CVE,CVE-2016-6210 只是背景,也沒有在野利用的可靠證據。回報者是 Özgün Kültekin(Wakefield)。

延伸閱讀

想法與技術判斷出自 Sheng,和 Claude 一起起草 · Grok 研究,Codex(gpt-6-astra)二審,Grok 調語氣;檢查指令在 OrbStack 的 Alpine 3.19(openssh-server 9.6_p1-r3)與 3.22(musl 1.2.5-r13、openssh-server 10.0_p1-r11)實測,aarch64,資料查核至 2026-11-05 22:00(台北時間)