返回所有文章
發佈於 · 作者 Renaud Deraison

一次工作階段,一百個儲存庫

Mandiant 的 AI Risk and Resilience 2026 報告描述了一名攻擊者:他接管了某 SaaS 供應商一個正在執行的 AI 編碼助理工作階段,藉此讓對方裝上被下毒的 PyPI 套件,竊走 GitHub OAuth 權杖,並把 Shai-Hulud 蠕蟲散布到約 100 個內部儲存庫。報告沒有說工作階段是怎麼被奪走的。在 Bromure Agentic Coding 的工作區裡,它也不需要說:那個工作階段裡的權杖是 `ghp_` 誘餌,想把它送出去就會讓虛擬機暫停,而把一台機器變成一百個儲存庫的那些推送,會停在主機端的代理上。

有人接管了一個正在執行、已通過驗證、而且受信任的編碼工作階段,就在某位 開發者自己的機器上。之後發生的一切,一路到一百個儲存庫,全都出自那台機器 當下所握有的東西。

某家軟體即服務公司的開發者問他的編碼助理,某個再平常不過的問題該用哪個套件。 助理說了一個。開發者說好,而這正是擁有助理的全部意義,剩下的交給 pip install

那時攻擊者早就握著這個工作階段,打出那則建議的人就是他。

Mandiant 在 AI Risk and Resilience Report 2026 中描述了這個案例,這份報告由 Google Cloud 的事件應變部門於本月發布; The Hacker News 在 9 月 16 日作了報導。 攻擊者劫持了開發者工作站上一個正在執行的編碼助理工作階段。再走五步,他 就抵達了大約一百個內部程式碼儲存庫,以及公司自己的套件命名空間,第二位員工 在那裡拉下了被下毒的版本。

一環一環看這條鏈

一台工作站,六個步驟這一步花掉什麼1攻擊者接管一個執行中的助理工作階段公開的案例研究未揭露手法工作階段2助理推薦了被下毒的軟體開發者依設計接受了它開發者的同意3被入侵的 PyPI 套件裝上竊密程式平常的相依流量,平常的安裝登錄檔的信任4GitHub OAuth 權杖被從工作站讀走關鍵轉折:底下每一步都靠它存放的權杖5Shai-Hulud 擴散到約 100 個內部儲存庫儲存庫機密與產品原始碼被帶走寫入權限6公司自有命名空間裡的套件被下毒第二位員工拉下該版本並被感染發布權限
Mandiant 描述的六個步驟,以及每一步花掉的憑證。Mandiant 沒有公開第 1 步的手法。在它之後的每一步,攻擊者用的都是開發者機器早已握有的授權:工作階段本身的信任,接著是套件管理器的信任,再來是 GitHub 的信任,最後是公司的發布權限。

Mandiant 偏偏扣住了你最想知道的那一段。公開的案例研究沒有說入侵發生在什麼 時候,也沒有說攻擊者是怎麼拿到一個執行中工作階段的控制權。被竊的瀏覽器 cookie、惡意擴充套件、共用機器上被入侵的 CLI,報告一個也沒挑。想在裡面找到 修補方式的人,只能空手離開。

第 2 到第 6 步完全沒有任何漏洞利用。套件安裝就照套件安裝的方式運作,以開發者 身分執行的程式碼就去讀開發者磁碟上擺著的權杖。帶著有效權杖的 git push 就 推得上去,帶著有效發布權限的發布就發布得出去。只有一步是入侵,另外五步都是 在花錢。

乘數是憑證,而且還在變大

Shai-Hulud 是那隻自我複製的供應鏈蠕蟲,從 2025 年起一路吞食維護者帳號;它會 在這樣的故事裡當作酬載出現,是因為第 5 步正是作者當初造它的目的。GitGuardian 在八月拆解了一個近期變種, 發現它會掃過 469 個憑證所在位置, 比早期版本的 189 個更多:開發環境、CI/CD 工具、雲端設定、AI 工具設定、套件 管理器的設定檔、shell 歷史、.env 檔案、IDE 設定、CLI 快取。這隻蠕蟲不是去 獵捕漏洞,而是去讀一整個檔案櫃。

GitGuardian 的分析用一句話講完了這個類型:

「攻擊者已經不再試圖打破信任關係,而是開始使用那些本來就讓這些關係得以運作 的憑證。」

一台工作站之所以變成一百個儲存庫,是因為它握有價值一百個儲存庫的授權。那個 數字出自一份清單,而要往清單裡放什麼,由你決定。

那個工作階段握著的是誘餌

Bromure Agentic Coding 讓代理在你 Mac 上的 Linux 虛擬機裡執行,而那台機器不 握有任何真正的機密。你不需要打開任何開關才有這個效果。工作區存放憑證的方式 本來就是如此。

你真正的 GitHub 權杖加密留在主機上。虛擬機收到的是一份保留結構的假貨ghp_ 後面接 36 個字元,總共 40 個,所以 gh 自己的前綴與長度檢查都會順利 通過。Bromure 把它匯出成 GH_TOKEN,並寫進 ~/.git-credentialsgh 的 設定裡;那是整台機器裡唯一的 GitHub 憑證。主機上的代理會在請求離開虛擬機之後 才在線路上換上真正的值,而且只在目的地符合該憑證所鑄給的主機時才換。每一份 假貨都是決定性的,由真值加上每次安裝專屬的 32 位元組鹽,經 HKDF-SHA256 推導 而來,所以會對自己金鑰取指紋的客戶端,在不同工作階段之間看不出任何輪換。

拿第 4 步來對付它看看。竊密程式抵達,掃過那 469 個位置,找到環境變數,找到 ~/.git-credentials,找到 gh 的設定,然後把找到的東西寄出去。每一次讀取都 成功,每一個檔案都躺在蠕蟲預期的地方。攻擊者最後拿到的,是一串四十個字元、 在任何地方都無法通過驗證的字串,除非它再穿回某一台特定的 Mac。

第 5 步和第 6 步都需要第 4 步交出一個能用的權杖,而第 4 步交出的是誘餌。

竊取本身就是警報

一份假權杖只有一個正當的去處。不會有任何無辜的請求,把鑄給 github.comghp_ 誘餌送往別的主機,所以代理會掃描每一個外送請求的標頭與內容,找出任何 離開自身範圍的假貨。它跑的是 Aho-Corasick 自動機,便宜到足以對準全部流量。

當代理找到一個,它做的不只是記錄:

  • 它以 HTTP 451 拒絕該請求,一個位元組都到不了目的地;
  • Bromure 當場把虛擬機暫停
  • 警示會給你 關機保留以供調查(先把磁碟、家目錄與共用資料夾匯出以 便鑑識)或自負風險的 繼續
  • Security Timeline 多出一列紅色的 Credential brokering
  • Bromure 把該工作區標記為已遭入侵,於是下次啟動會抹掉磁碟映像與持久家目錄。 你的權杖、SSH 金鑰與工作區設定都能挺過這次抹除。

這些你都不用去開;偵測器一直開著。第 4 步的外送嘗試本身就是讓工作階段停下來 的那件事,所以這起事件的結局是一台暫停的虛擬機和一筆時間軸紀錄,而不是一百 個儲存庫加一份案例研究。

一百個儲存庫就是一百次推送

假設你連保險都想多上一層。第 5 步是蠕蟲往儲存庫裡寫,而主機會把寫入分類。

工作區的護欄為每個服務帶著一份寫入政策。對 GitHub 而言, 這份政策不只讀 REST API,也讀 git over HTTPS:git pushgit-receive-pack 抵達,算成寫入;git fetchgit-upload-pack 抵達,算 成讀取。唯讀會拒絕這些推送。寫入前先詢問是新工作區的預設值,它會把每 一次推送都攔下來,交給主機端一個寫明操作名稱的對話框。兩種設定下 fetch 都能 通過,所以代理照樣能工作。

做這個判斷的是主機上的代理,在虛擬機之外。一個工作階段已經屬於別人的代理, 既關不掉這份政策,也繞不過去,因為政策根本不在代理那一側執行。一隻想要一百個 儲存庫的蠕蟲,會收到一百次拒絕,或者問你一百次。

而且套件終究得先送到

第 3 步正是 Mandiant 給出具體建議的地方:拿密碼學校驗碼與允許清單去驗證 AI 推薦的第三方相依套件,並讓相依流量走你自己掌控的儲存庫。

在工作區裡,每一次套件擷取本來就會經過主機,因為代理是虛擬機通往網路的唯一 路徑。Bromure 在 pypi.orgfiles.pythonhosted.org 攔下 PyPI,並在那裡 套用主機的政策。虛擬機裡的 pip.conf 可以把代理送出來的東西收得更緊,但放不 鬆。有三層會替 Python 套件發言:

  • 年齡門檻,預設開啟,最短兩天,會拒絕比門檻更新的版本;理由是剛發布的 版本,正是最可能剛被劫持的那一個。PEP 503 索引不帶時間戳,所以 Bromure 會 即時去查發布時間;
  • OSV 檢查,對 api.osv.dev 查詢,免費且不需金鑰,會擋下任何帶有你所選 嚴重度以上安全公告的版本;
  • socket.dev 的受害套件過濾,對惡意軟體、已知惡意軟體、搶註相似名稱與 流氓安裝腳本觸發。

封鎖會以 HTTP 451 回來,內容開頭是 Bromure Supply-Chain Security blocked this request: 接著是原因,而 pip 會 一字不改地印出來。你看得到安裝為什麼失敗,代理人也看得到,它往往能自己去釘一 個較舊的版本。那個較舊的版本,正是年齡門檻一直想把它導向的那一個。

一般的工作站第 3 步:套件裝了上去登錄檔送出被下毒的版本路徑上沒人判斷來源第 4 步:權杖被讀走真權杖在環境、設定與歷史裡外送帶走一份能用的憑證第 5 步:蠕蟲開始推送權杖有效,所以推得上去約 100 個儲存庫,外加命名空間在 Bromure 工作區裡第 3 步:擷取先被審年齡、OSV、socket.dev,都在主機451,一個位元組都沒進虛擬機第 4 步:同樣讀得到469 個位置全是 ghp_ 誘餌送一份出去:451,虛擬機暫停第 5 步:推送被分類git-receive-pack 被拒或跳詢問git fetch 照樣過;工作繼續
同樣三個吃重的步驟,放在兩台機器上。左邊是一般工作站:真權杖在磁碟上,竊取成功,推送也推得出去。右邊是 Bromure Agentic Coding 工作區:主機在被下毒的版本抵達虛擬機之前就拒絕它,同樣的竊取只換到一份誘餌並讓機器暫停,而推送由主機分類,被劫持的工作階段在這件事上沒有投票權。

Mandiant 的三項控制,以及它們住在哪裡

報告以三項建議收尾這個案例研究。每一項都指向一個 Bromure 工作區早已把界線 畫好的位置。

驗證 AI 推薦的相依套件

Mandiant 要求對助理建議的任何東西都做校驗碼與允許清單。你的工作區會在 虛擬機看到登錄檔回應之前,就先對它套用政策。代理會把太新的版本從版本清單 裡拿掉,所以在代理人眼中它們還不存在;若是直接指定了太新的版本,它會拒絕, 並在錯誤訊息裡引出該套件的真實年齡。信譽服務的金鑰留在主機上,不在那台 執行安裝的機器裡。

讓原始金鑰與長效權杖離得遠遠的

這就是把線路上的界線寫成建議的樣子。虛擬機裡沒有任何檔案、環境變數或行程 握有真正的 API 金鑰、OAuth 權杖、AWS 機密或 SSH 私鑰。Bromure 把訂閱的 OAuth 權杖加密存在主機上,並在到期前約五分鐘於主機端更新,所以客體端不帶 任何更新權杖。兩者之間的通道只往一個方向走:主機把假貨寫進去,而虛擬機沒有 任何呼叫可以回頭索取真的權杖。

讓相依流量走你自己掌控的東西

代理是虛擬機對外的唯一路徑。每一個 HTTPS 請求都經由虛擬通訊端送到主機,由 主機終結 TLS、檢查請求,再用 macOS 自己的 TLS 堆疊重新送出。正是這條唯一 的路徑,讓另外兩項控制從建議變成強制。沒有任何套件擷取能跳過檢查,也沒有 任何憑證能不經替換就離開。

還有,刻意把波及範圍縮小

一百個儲存庫會倒,是因為一台機器帶著一百個儲存庫的授權。把工作區切成一個 專案一個,或一道憑證界線一個。每一個對應唯一一台虛擬機,沒有兩個共用一台, 而且各自只帶那個工作區的工作所需要的憑證。任何一台機器上的清單,都在一個 設定面板裡由你決定。

沒辦法修補的那一部分

Mandiant 用一句描述形狀而非事件的話,框住了這個類型:「一個被下毒的資料來源、 模型相依或擴充掛鉤,都可能把一個受信任的代理變成內部偵察、橫向移動,或是自 沙箱自主逃逸的通道。」

三個不同的入口,一句話,而且三個都適用。入口決定攻擊者怎麼進來;某位開發者 機器裡的東西,決定他能走多遠。這次的攻擊者是從被劫持的工作階段進來的。下一個 會從別的地方進來,而報告沒有替你指出任何一扇該鎖的門。

為入口之後的那一步做設計。假設這個工作階段已經屬於別人——這正是案例研究藉著 始終不解釋第 1 步,強加給你的假設——然後再問,那個工作階段能碰到什麼。

這起事件的其中一個版本結束在第 3 步:終端機裡一個 451,Security Log 裡一行 紀錄。另一個版本結束在第 4 步:一台暫停的虛擬機,加上時間軸上一列紅色紀錄, 記著有一份憑證試圖離開。兩個版本都到不了第 5 步,而那一百個儲存庫就在第 5 步。

把代理放在一台只握著誘餌的機器上。 安裝 Bromure Agentic Coding,然後儘管讓人把工作階段拿走。