Linux CVE 資訊安全 SSH Tailscale

Tailscale SSH 把 `-i` 當帳號就開出 root shell:TS-2026-009 讓 getent 的 --no-idn 攤開整份 passwd

TS-2026-009 揭露 Tailscale SSH 在 Linux 上把使用者名稱直接餵給 getent(1),用 `-i` 當帳號就會被解析成 --no-idn 旗標,getent 把整份 passwd 印出來、第一筆是 root,Tailscale 順勢開出 root shell,autogroup:nonroot 直接繞光。本文拆解 argv 注入根因、靜態連結為何逼 Tailscale 選 getent,以及升級 1.98.9 之後該做的事。

Tailscale 在 2026 年 7 月釋出的安全公告 TS-2026-009 是一個典型到不能再典型的 argv 注入:Tailscale SSH 在 Linux 節點上把 SSH 連線帶進來的使用者名稱,直接當成參數餵給 getent(1),攻擊者只要把帳號設成 -i,就會被 getent passwd 解讀成 --no-idn 旗標,於是這支工具乾脆把整份 /etc/passwd 印出來,第一筆通常是 root。Tailscale 拿回這行 UID 0 的紀錄後直接開起 root shell,autogroup:nonroot 這類 ACL 完全沒有機會生效。修復版本是 Tailscale 1.98.9,回報者是 Anthropic 與 Ada Logics。

漏洞本身聽起來像 25 年前的教材,但值得寫是因為裡面藏了幾層設計選擇:為什麼一個以「安全連線」為賣點的產品會選 shell out 給 getent?為什麼修法是「禁止減號開頭的帳號」而不是「換成 getpwnam」?為什麼 Tailscale 同一波公告一次爆四個 CVE?

-i 是怎麼被 getent 認出來的

getent 是 glibc 附的一支小工具,用途是「幫我從 NSS 資料庫查一筆」。它接受第一個參數當資料庫名(passwdgrouphosts 等等),後面接想查的鍵,同時支援幾個旗標,例如 -s 指定 NSS service、-i 展開為 --no-idn 關掉國際化網域名稱轉換。旗標跟鍵在 argv 裡沒有位置區分,靠的是慣例:以 - 開頭就當旗標處理。

Tailscale SSH 拿到連線請求時要決定「這個帳號在系統裡對應哪個 UID」。做法是 fork 一支 getent passwd $LOGIN_NAME,然後解析第一行輸出取得 UID、gid、home 路徑、shell。問題在於 $LOGIN_NAME 沒有經過任何跳脫就攤在 argv 上。當 $LOGIN_NAME-i 時,getent 的參數解析器一看:這不是資料庫名後面的鍵,這是 --no-idn 旗標,於是進到「印出整份資料庫」的預設路徑,把 /etc/passwd 按檔案順序整包印出來,第一行永遠是 root。

Tailscale 讀到第一行 root:x:0:0:root:/root:/bin/bash,判斷 UID 0 存在、shell 存在,就開起互動 shell 給對方。整條路徑上完全沒有觸發任何 ACL 檢查,因為 Tailscale 認為「請求端宣稱的帳號本來就是 -i,這是有效帳號,只是解析出來剛好對應 root」。

autogroup:nonroot 為什麼擋不住

Tailscale ACL 提供 autogroup:nonroot 這個群組給管理員宣告「這批 tailnet 成員只能 SSH 進非 root 帳號」。這是零信任網路裡標準的權責分離:即便某個裝置被撬開,那個裝置對應的身份也不該拿到系統管理員權限。

ACL 檢查的實作是「解析請求者宣告的帳號名稱,比對是不是 root」。這個檢查在收到連線的第一時間就跑,但比對的對象是原始請求裡的字串——-i——不是後續 getent 幫忙查出來的 UID 0。字串比對通過(-i 顯然不等於 root),ACL 於是放行,剩下的解析交給下游流程。

同一波公告的 TS-2026-006 走的是另一條類似路線:直接把帳號寫成 0,也就是 UID 而不是名字。ACL 只認名字、後端 sshd 卻認 UID,兩層對「使用者是誰」的判斷不對齊,結果是 0@host 直接連進 root。兩個 bug 講的是同一件事:ACL 執行點與實際落地權限之間,中間夾了一段任意解析。

為什麼 Tailscale 挑 getent 而不挑 getpwnam

第一直覺是罵一句「幹嘛不用 getpwnam?」getpwnam(3) 是 glibc 提供的 C 介面,直接吃字串回結構體,天生沒有 argv 這一層。Go 標準函式庫的 os/user.Lookup() 也走同一條路,甚至更安全。

Tailscale 的 daemon 是 Go 寫的,而它為了在各種發行版跟 Alpine 這類 musl 環境維持單一 binary,長期偏向純 Go 的 netgo build。純 Go 的 os/user.Lookup() 掉回自家實作,只會讀 /etc/passwd,讀不到 SSSD、LDAP、systemd-homed 這些額外 NSS 來源。企業環境要嘛用 SSSD 把 AD 帳號 shim 進來,要嘛用 LDAP 集中管理帳號,這些帳號在 /etc/passwd 裡根本不存在,純 Go 查詢直接把合法使用者擋在門外。

getent 是 glibc 附帶的動態連結執行檔,走的是完整 NSS 堆疊。Tailscale 選它是為了「讓 SSSD、LDAP、systemd-homed 這類第三方 NSS 都能正常運作」,代價是接了一段 argv 語意。理論上這段程式碼應該用 -- 隔開參數與鍵,例如 getent passwd -- $LOGIN_NAME;實務上就是漏了。argv 注入這條老路過去在 chroot、find、xargs、sudo 上都出過同型 bug,coreutils 上游也才剛剛在 chroot --userspec 上處理過類似型態的漏洞。

「禁止減號開頭」不是完整的修法

Tailscale 1.98.9 的修補走的是黑名單:帳號名以 - 開頭就直接拒絕,同時封鎖純數字帳號避免 TS-2026-006 那條路。這在當下有效,也符合最省事的部署節奏(daemon 端一行檢查、不用動 getent 呼叫方式),但它做的是限縮輸入空間,沒有解決 argv 語意本身。

比較徹底的做法有兩個。一是繼續用 getent,在呼叫時明確加上 -- 分隔符:getent passwd -- "$name",讓 getent 的參數解析器碰到 -- 之後停止把後面的字串當旗標。二是改走 CGO 打開的 os/user.Lookup(),透過 NSS API 拿到帳號資訊,完全繞開 argv 這一層——代價是 Tailscale 得為每個平台維護不同的 build,或者在 musl 環境接受另一份行為。以工程慣例衡量,加 -- 是最低成本、也最結構性的修法,值得作為後續版本的方向;黑名單只能算過渡。

升級之外還該做的事

跑 Tailscale SSH 的系統應該直接升到 1.98.9 或更新版本,這一步沒有例外。升級之後再做三件事:

第一,翻 SSH 存取紀錄,找 login 名稱裡有 - 開頭、純數字、或看起來像旗標名的紀錄。這些字串在正常環境不該出現,出現就等於有人嘗試過。Tailscale 節點的日誌預設在 journalctl -u tailscaled,需要留意事件時間跟 ACL 修訂時間的關聯。

第二,重新審視 tailnet 的 ACL。假如原本靠 autogroup:nonroot 擋非管理員拿 root,這個假設在 1.98.9 之前的所有版本都不成立。實務上更安全的模型是明確寫死哪些身份可以連進哪個帳號,例如把 "users": ["alice@example.com"] 對應到具體目標帳號,而不是靠 autogroup:nonroot 這種概括性 deny。

第三,把 Tailscale SSH 當作便利工具、不是唯一防線。同一台伺服器上保留 OpenSSH 監聽在僅限特定來源(例如管理員 bastion IP)連得到的介面,關鍵伺服器另外走 hardware key 加 PAM 條件式規則。Tailscale SSH 的價值是免管理金鑰,但這個便利建立在 daemon 的每個決策都正確;當 daemon 一個 argv 拼錯就能 bypass ACL,就不該把它當唯一入口。

三十年還在原地反覆出現的 argv 陷阱

TS-2026-009 這種 argv 注入不是新型攻擊,1990 年代 unix 工具就有類似案例,過去二十多年間 chroot、find、xargs、sudo 都出過同一族的問題。工具鏈跟語言演進了,argv 語意沒變,開發者選擇 shell out 給 CLI 的動機也沒變——第三方 NSS 模組、離線環境、跨平台一致性,種種現實逼著這條路徑一直被選中。

真正該做的是把「呼叫外部工具時如何傳使用者資料」列為 code review 的固定檢查項目,尤其在 daemon、agent、controller 這類長期跑的服務裡。凡是 argv 就要問「有沒有加 --」,凡是 stdin 就要問「有沒有 shell metacharacter」,凡是環境變數就要問「上游是不是可控」。這些檢查在單個 PR 看很囉唆,但少一個就等於一個 CVE。

VPS 上跑的服務越來越多是分散式團隊自架的 Go 或 Rust daemon,它們大多在架構層自我描述「安全」,卻不會因此免疫於 25 年前就存在的 argv 陷阱。NCSE Network 在臺灣是方電訊機房提供搭載 Intel Xeon Gold CPU 與 NVMe SSD 的 VPS 主機,除了效能與線路,也支援獨立防火牆與純 IPv6 環境,適合搭配 Tailscale、WireGuard 這類 overlay 網路把管理面收斂到內網、把公網 SSH 埠關掉。相關方案細節可以在 ncse.tw 查看。

需要穩定的雲端主機?

NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。

查看 VPS 方案 →