# Sshole：帳號停用了密碼，為什麼還能通過 SSH 驗證

> OpenSSH 10.6p2 修補一個只在 musl、未用 PAM 且開放密碼登入時成立的驗證繞過：條件齊備時，密碼欄是單一星號的帳號遇上超長密碼會被放行。本文拆解成因，並給一條確認自己有沒有暴露的檢查路徑。

原文：https://sheng.page/posts/sshole-musl-openssh-auth-bypass/ · 發布：2026-11-05 · 作者：Sheng

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 與原始研究為準。

## 成立的條件

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

| 元件 | 要成立的狀態 |
| --- | --- |
| libc | musl（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 認證在檢查什麼](/posts/game-rng-certification/)〉是同一種毛病：亂數來源和輸出要分開檢查。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`。

```sh
# 在要檢查的主機上跑，全是唯讀查詢
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 列的修補版，拿它示範檢查指令，看到的是修過的機器。

```text
/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、不登入，跑完容器就刪。

```sh
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）上的實際輸出：

```text
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）。

## 延伸閱讀

- [OpenSSH 10.6p2 發布說明](https://www.openssh.org/txt/release-10.6p2)：一手公告，列出影響條件與回報者，把這次定位成 workaround，並說明為何改成更頻繁發版。
- [Alpine Linux 安全公告：openssh-server 密碼驗證繞過](https://alpinelinux.org/posts/2026-11-05-openssh-password-authentication-bypass/)：各維護分支的修補版本、影響條件與止血步驟，寫明撰稿當下未指派 CVE。
- [sshole 原始研究](https://sshhole.com/)：回報者 Özgün Kültekin 的技術說明，含 musl 回傳值、shadow 慣例與 CVE-2016-6210 的關係。
- [LWN 報導](https://lwn.net/Articles/1105464/)：獨立報導，技術脈絡與時間線。
- [musl 郵件列表 2024-12 討論串](https://www.openwall.com/lists/musl/2024/12/31/2)：超長密碼與 `crypt()` 回傳星號的討論，當時沒有人接到 sshd。
- [OpenSSH `pick_salt()` commit（2026-10）](https://lists.mindrot.org/pipermail/openssh-commits/2026-October/013271.html)：原始研究說本次 bug 就在這段程式碼附近，不是本案的共同發現。
- [CVE-2016-6210（NVD）](https://nvd.nist.gov/vuln/detail/CVE-2016-6210)：2016 年的使用者列舉問題，本次的歷史背景，不是這次的編號。

> 想法與技術判斷出自 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（台北時間）