850 個 GitHub 賬號的 token 被盜,13,500 個分支被植入了偷密鑰的 workflow
從 9 月 15 日起,有人拿着被盜的 GitHub token,在 850 多個失陷賬號能碰到的每個倉庫裏,推入一個專門打印倉庫 secrets 的 workflow。Wiz 恢復出的植入分支超過 13,500 個。這件事裏沒有人攻破 GitHub,工具用的是偷來的鑰匙,和平臺本來就有的功能。
我自己維護開源項目。GitHub 賬號對我不只是一個登錄入口,它同時是我的名字和發佈用的鑰匙,所以我把 Wiz 這份報告從頭讀完了。讀完之後,我的注意力不在手法上,手法並不複雜。我在意的是,這些 token 被偷走的時間,可能比這次行動早得多。
拿一個普通倉庫爲例。這個倉庫的 Secrets 裏存着一把雲服務密鑰,平時是部署流程在用。某天倉庫裏多出一個分支,名字是 ci-sync- 後面跟六個隨機字符,裏面多了一個叫 sync-check 的 workflow 文件,提交信息寫的是 ci: sync config。
分支一推上去,workflow 就自動跑起來。它的活只有一件:把這個倉庫能讀到的 secrets 全部打印進自己的運行日誌。跑完之後,工具通過 GitHub 的 API 把日誌下載走,幾秒鐘後刪掉這條運行記錄。倉庫主人回頭去查,只看得到一個自己沒建過的分支,運行記錄已經沒有了。

GitHub 對日誌裏的 secret 有打碼,值原樣出現時會被遮住。這個工具先把值做一遍 base32 編碼,同一個值換一種寫法寫進日誌,打碼就認不出來了。數據從頭到尾走的都是 GitHub 自己的 API,沒有外部服務器可以封,也沒有陌生域名會出現在代理日誌裏。
這個 workflow 裏還寫了一行 permissions: {},看起來像是把自己的權限關小了。它限制的其實只是任務運行時臨時領到的那張令牌,而攻擊者根本不需要那張令牌。倉庫裏存着的 secrets 仍然可以照常讀取,那行聲明不影響它讀 secrets。
如果倉庫配了部署環境,工具會把環境逐個列出來,每個環境單獨跑一個任務,環境裏的 secrets 一起打印。團隊放在環境裏的通常是生產憑據,所以這一步碰到的,是能打開真實基礎設施的鑰匙。
這些 token 是從哪裏來的,Wiz 追了能追到的部分。一部分來自竊密木馬的日誌。那種木馬混在盜版下載和被投毒的工具裏,裝上之後會把瀏覽器存的密碼、CLI 配置文件、git 憑據和 SSH 私鑰一起打包帶走。
另一部分來自更早的供應鏈事件,包括今年 3 月的 Trivy 失陷。那次泄露的憑據當時沒有被用掉,而是被存了下來,幾個月後換了一批人拿出來用。3 月新聞裏的一次泄露,到秋天成了另一個行動手裏現成的工具。
同一批失陷賬號還出現在其他行動裏。10 月 6 日,其中 391 個賬號把名下約 1,461 個倉庫改名成盜版軟件的誘餌。這批賬號裏有 166 個從沒在 Gheist 裏出現過,說明失陷賬號的總數比這個行動用掉的還要多。
10 月 18 日,25 個 Gheist 賬號在九分鐘內各建了一個 Windows RDP Runner 倉庫,濫用 GitHub Actions 的免費算力。另有 57 個賬號在 5 月就是另一個行動 Megalodon 的受害者,這批賬號裏另有受害者身上還帶着 PolinRider 的痕跡。同一批賬號被不同的行動輪流使用,這在 Wiz 的記錄裏有據可查。
token 的問題在於它只認字符串,不認人。Gheist 的提交沒有簽名,作者一欄寫的是 token 的主人,GitHub 照常接受,因爲這就是 token 的設計用途:誰拿着,誰就是你。沒有設備覈對,也不會多問一句。
對賬號主人來說,這意味着失陷之後發生的事,平臺記錄裏都記在他的名下。別人可以拿他的賬號改倉庫名、發東西、動他的項目,事後需要向用戶解釋的人是他,不是拿走 token 的人。
GitHub 今年有一個控制確實擋住了它。從 7 月 28 日起,公開倉庫裏被標記爲可疑的 workflow 運行會被扣住,等協作者批准才執行。Wiz 檢查過的公開倉庫運行全部停在待批准狀態,一次都沒有跑起來。
但這個扣留只適用於公開倉庫。私有倉庫和 GitHub Enterprise Server 沒有這道關,審計日誌裏能看到 ci-sync 分支的運行跑完,之後被刪掉。而生產用的 secrets 大多在私有倉庫裏。
扣留機制在公開倉庫裏已經被證明有效,爲什麼不覆蓋私有倉庫和 Enterprise?這一問,GitHub 應該回答。這個問題不是要它發明新東西,而是問一個已經存在的控制,邊界爲什麼劃在這裏。
在企業組織里,行動走得更深。Wiz 看到工具在幾分鐘內輪換使用同一家組織多個用戶和自動化賬號的 token,先克隆私有倉庫,再植入 workflow。幾個小時後,這家組織的雲憑據被用來枚舉 AWS Secrets Manager,那是存放其他系統鑰匙的地方。
10 月 19 日,行動給自託管 runner 加了第二個任務。它在機器上搜 31 種 token 格式,包括各家 AI 服務的密鑰,另外還有私鑰、.env 文件、Terraform 狀態和 shell 歷史,再試圖通過 Docker socket 到宿主機上繼續搜。
這部分活幹得並不好。它找錯了 runner 的密鑰文件,正則寫壞了,想拿的 GCP 和 Azure token 也沒有拿到。我的看法是,手法粗糙不等於危害小。到了這個規模,載荷做得糙一樣收得走大量憑據,因爲真正起作用的是偷來的 token 和平臺的正常功能。
如果 ci-sync 分支在你的倉庫裏跑過,處理方式只有一個:這個倉庫能碰到的 secrets 全部按已經泄露處理,倉庫級、組織級、環境級的都算,還有這些 secrets 能打開的雲資源。刪掉分支沒有用,日誌在運行記錄被刪之前就已經被下載走了。
如果它跑在自託管 runner 上,要按宿主機和它的雲角色一起失陷來處理。涉事賬號裏大約三分之二現在已經不在 GitHub 上了,Wiz 的判斷是,這多半是處置的結果。賬號不在了,不改變已經跑完的那些運行拿走了什麼。

防禦上先看你自己手裏的 token。如果你在用經典的個人訪問令牌,尤其是創建時間很久、權限給得很寬的那種,先換掉。換成會過期的細粒度令牌,權限只給到確實需要的倉庫。或者把自動化改走 GitHub Apps,用應用自己的窄權限,不再借你的賬號名義。
如果你的 CI 裏存着長期雲鑰匙,值得換成 OIDC。它讓每次運行臨時換一張短期憑據,雲那邊會覈對這次運行來自哪個倉庫、哪個受保護的分支或環境。倉庫裏不再躺着一把長期有效的鑰匙,配置文件被偷走,也換不出憑據。
放生產 secrets 的環境,前面加一位人工審批人,workflow 要用到那個環境時,先等人確認。自託管 runner 用一次性的,跑完就銷燬,不給 Docker socket,也不讓它隨便訪問雲的元數據接口。
如果你的 token 本來就是短期的,Actions 裏沒有存長期 secrets,也沒有自託管 runner,這次行動和你的關係不大。你要做的只是花幾分鐘確認一遍,賬號上有沒有忘了的舊令牌還掛着。
上面這些事實都來自 Wiz 的報告。到我寫這篇的時候,GitHub 還沒有發佈它自己對這次行動的說明。私有倉庫裏到底有多少 secrets 真的被拿走,外面的人無法知道,因爲運行記錄被刪掉了。13,500 是 Wiz 恢復出的分支數,是已經確認的下限,不是總數。
這次行動總會結束,賬號會被清理,分支會被刪掉。會留下來的問題是,token 這種東西,一串字符可以在幾年裏代表一個人,而且不需要本人在場。這個問題不是靠抓到這一夥人解決的。你我能控制的部分,是自己名下還留着多少把長期有效的鑰匙。先從配置文件裏那些查起,我的也在內。
來源:Wiz 報告原文