Skip to content
AI Ferryman Alex
Go back

沒設密碼的 Alpine 賬號,輸 300 個字的密碼就能登進去

四十多年來,密碼欄位裏沒設密碼的賬號,一直是輸什麼密碼都進不去的。你自己的 Alpine 機器上,現在還有幾個沒設密碼的賬號?密碼登錄還開着嗎?

密碼欄位裏的哈希、鹽值與星號

正常賬號的密碼欄位裏存的是一串哈希值,就是密碼算過一遍之後的亂碼,而密碼本身是不存的。這一串裏還存着一段鹽值,鹽值是一小段隨機字符,跟哈希值連在一起,算密碼的時候摻進去一起算。sshd 驗登錄的時候,就拿你輸的密碼摻着這個賬號的鹽值交給 crypt() 再算一遍,算出來的結果跟欄位裏那串一樣,才放你進去。

有一類賬號的欄位裏沒有哈希值,只有一個星號。1980 年,Bill Joy 給系統自帶的 daemon 賬號就這麼寫,Unix 從此一直這麼幹。因爲星號不是哈希值,輸什麼密碼都算不出它來,所以這種賬號用密碼永遠登不進去,只能用密鑰登錄。

只用密鑰的用戶,欄位裏也只有一個星號。從目錄服務來的用戶也一樣,他們的密碼哈希不存在本地,所以欄位裏就只有一個星號。Alpine 從 3.16 起的 root 賬號,欄位裏也只有一個星號。

三件事與一次登錄

這個星號用了四十多年,後來有三件事先後撞到了它上面。第一件出在 musl 身上,musl 是 Alpine 底層用的那個 C 語言庫。crypt() 算不出結果的時候,本來應該返回 NULL。可是很多程序拿到 NULL 根本不檢查,順手拿來就用,所以直接崩掉。

爲了不讓程序崩,musl 在 2012 年改了規矩:算不出來的時候不返回 NULL,改成返回一個單獨的字符。這個字符永遠不會是真密碼算出來的樣子,所以程序看到它,就只當密碼填錯了,還能接着往下跑。musl 挑中的這個字符,偏偏就是星號,跟欄位裏表示沒密碼的那個,是同一個字符。

第二件跟第一件只隔了二十天,同在 2012 年,還是 musl。musl 用 sha512 算法給密碼算哈希值,算出來的那串字符寫進欄位裏以 $6$ 開頭。這種算法算密碼的時候,密碼越長算得越慢,所以有人只要發一個超長的密碼過來,就能讓服務器一直耗在計算上。

musl 於是給長度封了頂,超過 256 個字節的密碼直接拒絕不算。麻煩就麻煩在,拒絕也算一種算不出來,所以 crypt() 拒絕的時候返回的,還是那個表示失敗的星號。

第三件在 OpenSSH 那邊,是 2016 年修另一個洞的時候帶進來的。那個洞出在計時上。你去問一個不存在的用戶,sshd 立刻就答不行。可是你去問一個真用戶,sshd 就得拿哈希值算一遍,所以要慢上幾毫秒。別人掐着這個時間差,就能一個一個試出哪些賬號是真的,這就是 CVE-2016-6210。

那個洞的修法,是讓 sshd 在沒有哈希值可對的時候也照樣算一遍:它借鹽值之前,要先挑一個有哈希值的賬號出來。挑好之後,就拿那個賬號的鹽值算你輸的密碼,算完再回答,這樣問誰都一樣慢。可是欄位裏只有一個星號的真賬號,也跟着走了這條借鹽值的路,因爲一個字的欄位裏,根本放不下它自己的鹽值。

三件事湊到一起,一次登錄就成了這樣。你給一個星號賬號輸 300 個字的密碼,輸的全是普通的英文字母和數字。一個字就佔一個字節,所以 300 個字就是 300 個字節,已經過了 256 的上限。

欄位裏沒有鹽值,所以 sshd 先從一個存着哈希值的賬號那裏借來鹽值,準備照常算。這種哈希值寫進欄位裏,是一串以 $5$ 或者 $6$ 開頭的長串,其中 $6$ 是前面說的 sha512 算法算出來的那種。可是密碼送到 crypt() 那裏,因爲長度超了,它根本沒算,就直接返回一個星號。而欄位裏存的也是星號,兩個星號一對上,sshd 就把你放進去了。

三件事湊成一次登錄

研究者從 257 個字一路試到 1,024 個字,每一個長度試下來,都是這個結果。也就是說,機器上只要有一個賬號設了真密碼、能借出鹽值,就夠了。這臺機器上所有的星號賬號,也就都可以用這中間任意一個長度的密碼登進去了。

把這個洞找出來的,是 Wakefield(一家公司)的 Özgün Kültekin。他試了大約 17,600 次登錄,這些登錄把他能搭出來的各種 sshd 配置組合都試了一遍。glibc 和 musl 都試了,glibc 是另一個 C 語言庫,Debian 那些系統底下用的就是它。

PAM 是大多數發行版驗登錄時要過的那一層,帶 PAM 和不帶 PAM 的,他也都試了。最後只有 musl 不帶 PAM 的那種,把不該放的登錄放了進來。他說,幫他試完 17,600 次登錄的,是一羣 AI agent。

他在 11 月 5 日公開了這個洞,Alpine 當天就發了公告。

這個洞成立的條件

要讓這個洞成立,機器還得同時滿足幾個條件。要麼跑的是 Alpine 那個沒編 PAM 的 openssh-server,要麼跑的是 PAM 版,但是 UsePAM 關着。UsePAM 是 sshd 要不要走 PAM 這一層的開關。密碼登錄也得是開着的,它默認就是開着的。

機器上還得有欄位裏是星號的賬號,還得有至少一個本地賬號存着真密碼的哈希值。真密碼的哈希值在欄位裏是一串長串,以 $5$ 或者 $6$ 開頭。少了存着這種哈希值的賬號,sshd 就沒有鹽值可借,前面那條借鹽值的路也就根本走不通。

Alpine 雲鏡像與 alpine 用戶

最該擔心的是 Alpine 自家的雲鏡像。官方給 AWS 和 Azure 用的那幾份 tiny-cloud 鏡像裏,有一個 alpine 用戶,他的欄位裏是星號,而且他用 doas 是不用密碼的。doas 是 Alpine 上相當於 sudo 的提權工具,能讓一個普通用戶以 root 的身份跑命令。

這些鏡像剛開機的時候是安全的,因爲機器上一個帶密碼的賬號都沒有,sshd 想借鹽值也借不到。可是機器上只要多出第一個 $6$ 哈希值,這個洞就通了。

多出這個哈希值的路不止一條。一條路是管理員在雲平臺的控制台裏給一個賬號設上密碼,密碼一設上,這個賬號的欄位裏就寫進了一個 $6$ 哈希值。另一條路是 user-data,也就是雲平臺在機器開機時交給它的一份啓動數據。這份數據加一個帶密碼的用戶,用戶一建出來,欄位裏也寫進了一個 $6$ 哈希值。

第一個 $6$ 哈希值一齣現,攻擊者就能先以 alpine 的身份登進來,再用 doas 升到 root,而日誌裏只多出一行登錄成功,跟一次正常登錄留下的那一行沒有任何區別。

root 本人反而不是最直接的目標。它從 Alpine 3.16 起,欄位裏也是星號,可是 OpenSSH 默認不讓 root 用密碼登錄,所以 root 只有在有人改過這個設置的時候,才用密碼進得去。不過就算 root 這道門守住了,也擋不住前面那些雲鏡像裏的 alpine 用戶,因爲他靠 doas 就能升到 root,根本用不着 root 自己的登錄。

不受影響的系統

反過來,不在這些條件裏的就不受影響。容器裏要是沒跑 sshd,就沒有登錄入口可以輸密碼。密碼登錄已經關了的機器也一樣,因爲長密碼根本走不到驗密碼那一步。用 PAM 版、而且 UsePAM 開着的,驗密碼走的是 PAM 那條路,所以不會走到 crypt() 那裏,也就碰不到它返回的那個星號。

Debian、Ubuntu、Fedora 這些 glibc 系統也不受影響。一個原因是 glibc 的 crypt() 算不出來的時候,返回的還是 NULL。再一個原因是它們的密碼登錄大多走 PAM,而 PAM 碰到星號賬號不會去調 crypt()。

dropbear 是另一個 SSH 服務器,用的是另一套代碼。OpenBSD 這個操作系統也不受這個洞影響,因爲它根本不跑 musl,這個洞靠 musl 的 crypt() 返回星號才成立的那一步,在它那裏不存在。欄位裏還有另一種單字符的寫法,寫的是感嘆號。這種賬號整個是被鎖死的,所以密碼和密鑰都登不進去,這次同樣沒事。

藏了十多年的原因與兩次擦肩而過

這個洞能藏十多年,就是因爲它要湊的條件太多,而且大多數系統平時根本不走這條路。再加上 OpenSSH 自己的測試跑在 OpenBSD 和 glibc 上,musl 那邊又不測 sshd,所以兩邊的測試都正好繞開了這條路。

其實有兩次,人已經站到它旁邊了。2024 年 12 月,musl 的郵件列表裏在吵一個問題,出在 busybox 這套小工具的 passwd 命令上。那個問題裏有 257 個字的密碼,有同一個星號,也有同一個長度上限,可是整串討論裏沒人提到 sshd。還有一次,就在研究者找到這個洞的四天前,OpenSSH 的一次安全審查剛修過挑鹽值的那個函數。這個洞不在修的那幾行裏,而在那幾行往下數四行的地方。

修復辦法與包版本覈對

修好的包已經發了,修好這個洞的 OpenSSH 上游版本是 10.6p2。注意,Alpine 的穩定分支用的是打進包裏的補丁來修,裝沒裝上要看包版本,光看版本號會看錯。你把 openssh 升上去,再重啓 sshd。因爲只升級包的話,已經在跑的進程不會自己換掉,就一直帶着這個洞應答登錄。

Alpine 修好的發行版是 3.21.10、3.22.8、3.23.8 和 3.24.4,而且打好補丁的包往回一直髮到 3.16。Alpine 寫公告時還沒給這個洞編 CVE 號,不過我不會等編號再去修機器。

查自己修沒修,別看 ssh -V 顯示的版本號。穩定分支的修復是打進包裏的補丁,所以 ssh -V 顯示的只是上游版本號,補沒補都一樣。就拿 Alpine 的 3.24 分支來說,它顯示的是 10.3p1,可公告裏給這個分支列的修好包版本是 10.3_p1-r2。你跑 apk info -v openssh 看裝的是哪個包版本,再跟公告給這個分支列的修好包版本對一下,對上了纔算修好。

拿裝好的包版本跟公告覈對

動手之前,先確認密鑰登錄是通的。手頭這個會話也先別斷,等新開一個登錄試過密鑰能進,再把舊的關掉。確認完,再說關密碼登錄的辦法:要是暫時升不上去,就先把密碼登錄關掉,再重啓 sshd。Alpine 的公告裏把現成的寫法給了出來,寫法就是往 /etc/ssh/sshd_config.d/00-no-passwords.conf 這個文件裏寫上一行 PasswordAuthentication no。

不過這個文件夾只有 3.19 起的默認配置纔會讀,更早的版本根本不看它。跑老版本的話,就把這一行直接寫進主配置 /etc/ssh/sshd_config。

日誌排查與自測

關好之後,再去翻一遍日誌,看這個洞是不是已經被人用過了。在 /var/log/messages 裏找 sshd 寫的 “Accepted password for”,後面跟的賬號要是根本沒設過密碼,那就是有人進來過了。你要是排不掉這樣的一次登錄,就不要再把這臺機器當成乾淨的,按已經被攻破來處理。

你也可以自己先試一把:拿你機器上一個沒設密碼的賬號,輸一個 300 個字的密碼去登一次。要是登進去了,那麼升級和重啓今天就得做,一天都別拖。

要是沒登進去,也別急着放心。一種情況是機器上還沒有能借的鹽值。再一種情況出在密碼長度上。前面讓你輸 300 個字,是因爲普通的英文字母和數字,一個字佔一個字節,300 個字就是 300 個字節。只有過了 256 個字節,crypt() 纔會拒絕,纔會返回星號。你輸的要是沒超過這個數,crypt() 就不會拒絕,也不會返回星號,那等於沒試。所以最後還是要回到包版本那一查,對上了纔算數。

這種藏在四十年的老規矩裏、要三件事湊齊才成立的洞,我還會接着拆。覺得這篇對你有用的話,歡迎訂閱這個博客,或者先收藏起來,我接着寫 AI、安全和開源這幾塊的實事。

來源:研究者的文章 https://sshhole.com/ ;Alpine 的公告 https://alpinelinux.org/posts/2026-11-05-openssh-password-authentication-bypass/