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

他們換掉的是位址,不是套件

2026 年 8 月 31 日的十四個小時裡,部分送往 registry.coder.com 的請求,是由攻擊者加進 Coder 自家 Cloudflare 來源池的伺服器回應的。主機名沒錯,憑證通過驗證,模組帶著一直以來的名稱與版本,而回傳的程式碼開始四處尋找 SSH 金鑰、OIDC 權杖與資料庫密碼。Coder 手上沒有攻擊者的日誌,所以它的公告要你自己去檢查對外流量。這個問題,大多數跑著編碼代理的機器都答不出來。

主機名是 Coder 的。憑證通過驗證。模組帶著你的範本早就釘住的名稱與版本。8 月 31 日的十四個小時裡,其中一些回應的伺服器屬於別人。

一位平台工程師在週一早上更新工作區範本。terraform initregistry.coder.com 抓下範本指名的模組(Docker、Zed、aider),跟上個月同一個 地方。Terraform 印出它慣有的一片綠色,建置成功。那天早上、那整個月,沒有任何 工具示警。

9 月 1 日,Coder 發布了 GHSA-vx42-ghc9-gw65, 評為重大,CVSS 9.0。用它自己的話說:「一名身分不明的惡意行為者取得了 Coder 的 Cloudflare 基礎設施存取權,並在 Coder 模組登錄檔所使用的來源池中加入了未經授權 的 IP 位址。」在 8 月 31 日(週一)07:35 至 21:45 UTC 之間,部分對 Coder 模組的 請求是由攻擊者控制的機器回應的。Coder 打造自架的雲端開發環境; BleepingComputer 列出 Dropbox、Palantir、Square、Mercedes-Benz、KKR、EnBW、美國政府單位與國防承包商 都在它的使用者之列。

沒有人動過套件

一支謹慎團隊所有的供應鏈習慣,檢查的都是產出物。名字拼對了嗎。發布者是不是他 所宣稱的那個人。這個版本是昨天才發的嗎。鎖定檔的雜湊還對得上嗎。升版之前先讀 差異。

攻擊者改的是:當你的客戶端去問位址時,由哪些機器回答;產出物本身他沒碰。他沒有 發布任何套件,沒有接管任何維護者帳號,沒有註冊任何相似名稱,也沒有編輯 Coder 登錄檔裡的任何東西。名稱一如既往地解析成功。TLS 交握是對著一張為真實主機名簽發 的有效憑證完成的。模組就叫 dockerzedaider,因為那正是 Coder 範本會要 的模組,而且它帶著範本早就釘住的版本號。

不一樣的是回傳的內容。被下毒的副本夾帶一個叫 dlp.sh 的 shell 腳本,為 docker、 aider、rstudio-server、windows-rdp 與 zed 各模組各自建了一份,公告裡也各自附了 雜湊。公告把這個酬載描述為「設計用來辨識若干憑證,並將其外傳到一台遠端伺服器, 該伺服器的網域名稱與 Coder 經常使用的某個網域極為相似」的程式碼。那個網域是 coder-infra[.]com,8 月 28 日註冊,比任何人用到它早了三天,解析到 199.91.220.205,回呼位於 http://www[.]coder-infra[.]com/cli/check

它下手的清單,讀起來像一份開發平台信任的盤點:佈建器的環境變數與機密、使用者的 OIDC 權杖、SSH 金鑰、外部認證憑證,以及 coderd 的設定變數,含資料庫密碼與認證 供應商憑證。

你不需要做任何不尋常的事就會落在那個時間窗裡。依公告所言:「使用 Coder 主要模組 登錄檔(registry.coder.com)作為範本來源,且在這段程式碼可取得的期間內更新過元件 的使用者,可能已安裝了惡意套件。」建立範本、升一版範本、跑一次 dry run,或在關掉 模組快取的情況下建置工作區,都是再普通不過的週一工作。

8 月 31 日 07:35–21:45 UTC — 十四小時的時間窗建置開口要terraform init新範本、升一版,或一次 dry run名稱解析registry.coder.comCloudflare,真正的大門池裡的一個 IP攻擊者加進去的,供應改過的dlp.sh回呼位址coder-infra[.]com8/28 註冊,8/31 啟用客戶端檢查了什麼,又發現了什麼主機名Coder 自己的,不是相似網域憑證有效,簽給真名,在真的邊緣節點模組名稱docker, zed, aider —正是你要的那幾個版本範本裡早就寫著的那一版。沒有新東西四項檢查全部通過。是哪台伺服器回應的,正是客戶端所有檢查都不看的那一件事。
攻擊者把自己的 IP 位址加進 registry.coder.com 背後的來源池,於是主機名、憑證、模組名稱與釘住的版本全都是真的,只有位元組來自別的地方。一次被下毒的建置與一次乾淨的建置,在可觀測層面只差一件事:一條連往三天前才註冊的網域的對外連線。

「檢視防火牆、代理、DNS 與 VPC 流量日誌」

這是 Coder 補救步驟的第五項,也是整份公告裡最有啟發性的一行。第一到第四項是 Coder 幫得上忙的事:找出時間窗內被快取模組的 SQL、清掉它們的 SQL、修補過的版本 (2.37.0、2.36.4、2.35.7 與 2.34.9,全都在 9 月 1 日釋出),以及一份該輪替的憑證 清單。第五項伸到這一切之外,因為能說出誰中招的那些伺服器屬於攻擊者。Coder 看不 到它們,而且直說了:它無法確定地指認每一個被入侵的部署。

於是問題回到你身上,而這是一個關於證據的問題。跑過那個模組的機器上,有任何東西 連過 coder-infra[.]com 嗎?對於一個正式環境的 VPC,多數團隊一小時內就能回答, 因為平台團隊裡已經有人擁有流量日誌。

現在,換成你的編碼代理工作的那台機器再問一次。代理會跑 terraform initnpm installmake、一次容器建置、一輪測試、一支它在儲存庫裡找到的腳本、一個 某模組下載回來的二進位檔。這每一件事都會開啟連線,而沒有人把它們寫在你一週後能 搜尋得到的地方。代理自己的逐字記錄留著模型發出的工具呼叫,卻沒有任何一行記錄 Terraform 模組裡的 shell 腳本對網路做了什麼,因為模型根本沒看見那件事發生。

第五步的答案沒辦法事後再買。要嘛在請求離開機器的當下就有東西把它記了下來,要嘛 根本沒有東西可搜。

那支腳本本來會找到什麼

就照酬載的說法,讓它跑。它掃過環境找憑證,讀走 SSH 金鑰,抓走 OIDC 與外部認證 權杖,撿起裡面帶著資料庫密碼的設定變數,然後把整包送往 coder-infra[.]com

Bromure Agentic Coding 給每個工作區一台跑在 Apple hypervisor 上的專屬 Ubuntu 虛擬機,並以一個主機端代理作為客體通往網路的唯一出口。在那裡跑 同一輪掃描,它會帶回完整一套憑證,而且全是假的。

ANTHROPIC_API_KEY 裡放的是 sk-ant-api03-brm-… 誘餌,GH_TOKEN 是長度正確的 ghp_ 字串,~/.git-credentials~/.docker/config.json~/.kube/config 裡放的是相對應的佔位值。格式都保留著,所以 ghdockerkubectl 都會照收 不誤。代理把你真正的值加密存在你的 Mac 上,等請求離開虛擬機之後才在線上換進去, 而且只在請求要去的正是這份憑證所簽發的那台主機時才換。沒有開關能關掉這件事;在 這個 app 裡,憑證本來就是這樣運作的。

SSH 金鑰是最乾淨的例子:虛擬機裡根本沒有金鑰可讀。SSH_AUTH_SOCK 指向一座跨越 虛擬 socket、通往主機上代理程式的橋,而那個協定裡有一種請求叫「幫我簽這個挑戰」, 沒有任何一種請求叫「把金鑰交給我」。ls ~/.ssh/id_* 回來是空的,而 git push 照常運作。

AWS 值得自成一段,因為 Terraform 面對的正是它。虛擬機的 ~/.aws/config 指向一個 輔助程式,它吐出的是你真實的 access key ID,配上一把假的 secret key。 Terraform、AWS 的各種 SDK、boto3 與 aws CLI 都不必額外設定就會讀這個輔助程式, 接著用假鑰簽署請求,產出一個格式完整、但 AWS 會拒絕的簽章。主機在出去的路上剝掉 那個簽章,用真正的密鑰重新簽一次。一支在環境裡翻找 AWS 素材的腳本,帶走的是一個 key ID 和四十個什麼都認證不了的字元。

接著它得把戰利品送去某個地方,而工作區的對外防火牆就坐在那裡。把 Unmatched traffic 設成 Deny,虛擬機就只能連到你列出的主機,別的都不行。強制執行發生兩 次,在客體構不到的兩個位置:虛擬網路交換器按目的 IP 與從 DNS 側錄到的主機名比對 每一條流量,跨所有協定;代理再依 TLS 伺服器名稱比對一次。沒有人會把一個三天大的 網域放進那份清單。規則的修改不必重開機就能送達執行中的工作階段,所以你可以在 建置還在跑的時候補上 deny any coder-infra[.]com

如果流量還是出去了,可能是工作區跑在預設的全部放行姿態下,也可能是目的地看起來 太普通,那麼誘餌就會做它的第二份工作。代理會掃描每一個對外請求的標頭與內容,尋找 一份正要送往「當初並非為它簽發之處」的假憑證。一個寄往 coder-infra[.]comsk-ant-api03-brm-… 字串毫無模稜兩可之處。代理會在任何一個位元組抵達目的地之前 以 HTTP 451 擋下,暫停虛擬機,並發出一則點名該憑證與兩端主機的警示。Bromure 把 這個工作區標記為已遭入侵,在你抹掉它的磁碟與家目錄之前,它不會再開機。

第五步的答案在事情發生的當下就到手,而不是一週後從日誌裡拼回來。

在跑過那個模組的機器上terraform apply → dlp.sh以建置自身的權限執行它收集什麼佈建器的環境變數與機密SSH 金鑰、外部認證憑證使用者的 OIDC 權杖coderd 設定 — 資料庫密碼對外:一個 HTTPS POST,來自整天都在發 HTTPS 請求的建置主機「我們中招了嗎?」→ 搜尋你可能沒有的日誌真憑證出去了,主機上沒有任何東西記下那個請求。在 Bromure Agentic Coding 工作區裡terraform apply → dlp.sh照樣執行 — 以 ubuntu 身分,在客體裡用完即丟的 VM · 主機代理是唯一出口每個環境變數與設定權杖都是誘餌沒有私鑰位元組 — 由主機透過 vsock 簽章AWS:真 key ID、假 secret,主機重簽對外防火牆預設拒絕 — 回呼被擋誘餌被送出自身範圍 → HTTP 451,VM 暫停,工作區標記為已遭入侵不論哪個行程發出,每個請求都有一列它收到的是誘餌,代理擋下回呼並留下了那一列。
同一個模組、同一支 dlp.sh,在兩台機器上。左邊它找到真的環境變數、真的 SSH 金鑰與真的權杖,而它的回呼只是一個沒有任何人記下來的普通 HTTPS 請求。右邊它找到的是誘餌,沒有私鑰位元組,簽章密鑰什麼也認證不了,回呼撞上一道預設拒絕的對外防火牆,而一份寄往範圍之外主機的誘餌會讓虛擬機停下來。

一道指令回答那份公告

主機代理會為每一個離開虛擬機的請求寫下一筆中繼資料紀錄,不論是哪個行程發出 的:時間、主機、連接埠、方法、路徑、狀態、延遲,以及在任何憑證替換之前量到的 位元組數。把工作區的 Session trace 設成 Activity only,你會拿到這筆 紀錄,而且完全不儲存任何請求內容。接著 bromure-cli trace hostnames my-workspace 就會把這個工作區接觸過的每一個不同 主機連同次數一併印出來。Coder 的第五步,一行搞定,針對那台真正跑過程式碼的 機器。

各個引擎做了什麼判斷

Security Timeline(Window → Security Timeline…)是另一個視圖,看的是 Bromure 的引擎判斷了什麼:每一次對外防火牆的裁決、每一份代理居中處理的憑證、 每一次供應鏈攔阻,全部標上顏色、可以過濾。一條連往三天前才註冊網域的被拒連線, 會在建置還在跑的時候就以一列紅色的 Firewall 出現在那裡,而不是等別人的公告告訴 你該往哪看之後,再去 grep 一個月份的流量日誌。

客體能碰到你 Mac 上的只有一樣東西:共用資料夾,一扇通往你真實檔案系統的即時 讀寫窗口,而入侵後的抹除依設計不會動它。分享專案目錄而不是你的家目錄,那麼一支 獵捕 id_rsa.env 的腳本,走遍的會是一個塞滿簽出與套件快取的 Linux 家 目錄。

當你無法檢查來源,就盯住出口

所有來源驗證最終都化約成同一個動作:把送到的東西,跟你已經信任的某樣東西比對。 一個名字、一個發布者、一份簽章、鎖定檔裡的一個雜湊。只要攻擊者還必須往通道裡放 點什麼給你比對,這個動作就有效。握住通道一段的攻擊者什麼都不放進去,而 Coder 的 來源池就是通道的一段。8 月的 BdThemes 入侵是同一個形狀, 一個被挾持的儲存桶改掉了 35 萬個 WordPress 網站會去抓的 JSON 資訊流,沒有任何發行,也沒有任何檔案被 改動

十四個小時就是一個工作天。Coder 抓到了,隔天早上就送出修補,公布了雜湊與指標, 也告訴讀者該檢查什麼。但這份公告最後仍然要求每一位讀者,自己去看自己的對外流量, 因為另一端的紀錄已經沒了。

輪替公告列出的金鑰、清掉快取的模組、套上修補。然後決定下一次要讓它碰到什麼, 因為下一次會從一條你從來沒有理由懷疑的通道進來。 安裝 Bromure Agentic Coding,給建置一台自己的機器,等公告出現 的時候,答案已經備好了。