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

蠕蟲走向開源

在 2026 年 5 月 11 日那一週的某個時間點,Shai-Hulud 背後的人馬——這隻自 2025 年 9 月起一路啃食維護者帳號的自我複製 npm 供應鏈蠕蟲——把自己的原始碼洩漏了出來。到週末為止,OX Security 已在同一個帳號底下找出四個搶註類似名稱的 npm 套件,其中一個幾乎是洩漏版蠕蟲的逐字翻版,一個是 Golang 寫的 DDoS 殭屍程式,另外兩個則是地攤級的純資訊竊取程式,把 SSH 金鑰和加密貨幣錢包送往廉價 C2。供應鏈攻擊的派生門檻一下子降低了許多,而最有可能裝上其中之一的人,已經不再是真人了。

上週某個時候,Shai-Hulud 背後的人馬——這隻自 2025 年 9 月 起便不斷啃噬維護者帳號的 npm 蠕蟲——把自己的原始碼洩漏了 出來。到週末為止,一個叫 deadcode09284814 的 npm 帳號已經用那段程式碼發布了四個搶註套件。其中一個幾乎是 逐字翻版的蠕蟲。一個是 Golang 寫的 DDoS 殭屍程式。 另外兩個則是純粹的資訊竊取程式,把你的 SSH 金鑰、 ~/.aws/credentials 以及 MetaMask 的金庫 POST 到一台 租來的 IP。兩千六百七十八次安裝之後,問題已經不再是 「會不會有人把這次洩漏武器化」。問題是,今天下午在你 終端機裡敲 npm install 的那批代理人,會挑中其中 哪一個。

在攻擊性軟體的世界裡,週期性地會發生這樣的事:一個封閉的工具 被丟上某個論壇,能跑它的人從「寫出它的那一組人馬」一下子變成 「任何一個有 VPS 的青少年」。Mimikatz、EternalBlue、Conti 的 原始碼,每次都是如此。每一次,能力本身並沒有改變——只是不再 稀缺。上週的頭條,由 BleepingComputer 根據 OX Security 的研究報導:Shai-Hulud——也就是我們在它六分鐘內吃掉四十二個 @tanstack 套件時所寫過 的那隻蠕蟲——加入了那份名單。

這次的派生並不是理論上的。到了星期天,OX 已經記錄了四個從 同一個 npm 帳號 deadcode09284814 發布的惡意套件:

  • chalk-tempalte——chalk-template 的搶註版,內含 洩漏版 Shai-Hulud 原始碼的逐字翻版,包括 Shai-Hulud 的招牌行為:把竊得的憑證以公開的自動生成 GitHub 倉庫上傳出去。OX 的解讀是:「Shai-Hulud 惡意程式碼是 洩漏原始碼幾乎一字不差的副本,沒有任何混淆技巧」,顯示 這是一個連流水號都懶得磨掉的新威脅行為者。
  • axois-utils——axios-utils 的搶註版,內附一個 Golang 載荷,OX 把它稱作 Phantom Bot:具備 HTTP、TCP、UDP 與 reset flood 攻擊能力,透過 Windows 啟動資料夾與排程工作 做持久化,C2 位於 b94b6bcfa27554.lhr.life。你的開發機, 正式被徵召入伍。
  • @deadcode09284814/axios-util——另一個搶註版,另一種 載荷:SSH 金鑰、環境變數、AWS/GCP/Azure 憑證,送往 80.200.28.28:2222。用 OX 的話說,「相當直接」。
  • color-style-utils——一個普通的資訊竊取程式,抓取你的 IP、地理位置與加密貨幣錢包資料,並 POST 到 edcf8b03c84634.lhr.life

在 OX 撰寫報告時的每週下載量合計:2,678 次。

這裡有兩個糾纏在一起的故事,值得把它們拆開來看。無聊的那個 是:npm 上有搶註套件。確實有;一直都有;也永遠都會有, 原因和狗公園裡會有叫做 Bench 的狗一樣——名字便宜,而命名空間 是平的。有趣的那個是:要跑一隻 Shai-Hulud 等級的蠕蟲所需的 門檻剛剛降低了。直到上週,你還需要原始作者的工具、原始作者 的基礎設施、原始作者那種不被抓到的紀律。今天,你只需要一個 從公開倉庫 clone 來的 GitHub repo、一條 lhr.life 通道, 以及打 npm publish 的耐心。OX 找到的這四個套件, 合在一起就是證據。

為什麼行為者數量比套件數量更重要。

一隻孤立的、技術成熟的蠕蟲,在某種意義上是個可處理的對手。 它有特徵,有基礎設施,有習慣,可以針對它寫出偵測規則。 Aikido、Socket、Snyk、Wiz——那群在 5 月 11 日 @tanstack 事件當天立刻撲上去的 npm 供應鏈監控廠商——之所以能在幾個 小時內就抓到它,正是因為他們已經盯著同一個家族盯了八個月。

但一整群由從某個貼文站下載原始碼的人寫出來的衍生蠕蟲, 就是另一種形狀。每一隻都會把資料外傳到不同的 C2、嵌入不同的 RSA 金鑰、選擇讀取不同的檔案組合、棲身於不同的搶註名稱空間。 有些會小心翼翼;大多數會草率到很快就被抓;其中總有一隻, 等我們下一次寫類似這篇文章時,會精巧到一週都抓不到。防守方 的偵測難題從「找出 Shai-Hulud」擴展成「找出任何想從 prepare 腳本裡讀取 ~/.ssh/id_ed25519 的東西」。這是一個大上許多、 許多倍的攻擊面。

安裝路徑的形狀也變了,而這正是所有跑著編碼代理人的人應該擔心 的部分。一位在 2024 年想用 chalk-template 的真人開發者, 會從某篇教學文章裡讀到套件名稱、把它打進去,然後在 chalk-tempalte 顯示只有兩百次下載、發布者是個陌生人時, 注意到打錯字了。但一個被要求「幫我的 CLI 輸出加點顏色」 的 2026 年編碼代理人, 會直接安裝套件管理員回給它的任何東西。代理人看不到發布者欄位。 代理人不會注意到 README 只有三行。代理人這一個小時內要做三十 次 npm install,因為使用者正在做一個小型團隊的工作量, 而代理人是按任務計費,不是按安裝次數計費。

一個未混淆的 Shai-Hulud 翻版到底會做什麼。

OX 寫道,chalk-tempalte 套件「幾乎完全沒有變動」地照搬了 洩漏的原始碼。這意味著我們在 把自己寫進 .claude 的蠕蟲 裡走過的同一套機制依舊適用,但多了一個重要的新轉折: 資料外傳的通道就是 GitHub 本身

Shai-Hulud 的招牌伎倆——被翻版照單全收,因為複製比重寫容易—— 是被竊的憑證不會送到隱密的接收伺服器。它們會被送到一個剛剛 新建的公開GitHub 倉庫,使用的是惡意程式剛從受害者那邊偷來 的 GitHub token 發布。受害者的機密就放在那個公開倉庫裡, 擁有者還是受害者自己的 GitHub 帳號,世界上任何人都可以爬取, 直到有人察覺、那個倉庫被砍掉為止。防守方的標準劇本——封鎖 外傳網域、找尋異常的對外 DNS——對此完全無效,因為對外 DNS 是 api.github.com,你的開發機本來就一小時要跟它講話兩百次。 那一包憑證離開你的筆電時,偽裝成一次 git push

機密一旦變公開,任何盯著 GitHub 即時資料流的人——而確實有 不少人專門為了這個原因盯著 GitHub 即時資料流——都可以順手 撈走。蠕蟲不需要維持自己的 C2 存活。GitHub 就是 C2。

一個 npm 帳號 — deadcode09284814 — 四種載荷npm 使用者deadcode09284814 註冊:2026-05 套件數:4 週下載:2,678已發布chalk-tempalte ↳ 仿:chalk-templateaxois-utils ↳ 仿:axios-utils@…/axios-util ↳ 帶 scope 仿冒color-style-utils ↳ 通用感名稱四個都帶 postinstall或 prepare 腳本chalk-tempalte → SHAI-HULUD 翻版讀取:~/.npmrc、~/.config/gh、$GH_TOKEN、~/.aws、~/.ssh建立:受害者帳號下的公開 GitHub 倉庫送出:用內嵌 RSA 公鑰加密的機密axois-utils → PHANTOM BOT(Golang DDoS)持久化:Windows 啟動資料夾 + 排程工作能力:HTTP/TCP/UDP flood、TCP reset 攻擊@…/axios-util → 純資訊竊取程式讀取:SSH 金鑰、環境變數、AWS/GCP/Azure 憑證POST:原始封包,沒有加密層color-style-utils → 錢包竊取程式讀取:IP、地理位置、瀏覽器外掛錢包目標:磁碟上的 MetaMask、Phantom、Keplr 金庫贓物送往何處chalk-tempalte: api.github.com(受害者 token) 在受害者帳號下開公開倉庫 + 87e0bbc636999b.lhr.lifeaxois-utils: b94b6bcfa27554.lhr.life Golang 殭屍,接收指令@…/axios-util: 80.200.28.28:2222 純 TCP,沒有 TLScolor-style-utils: edcf8b03c84634.lhr.life HTTPS POST三條不同的 .lhr.life 通道、一個裸 IP、一個 api.github.com:封鎖網域的策略行不通。
`deadcode09284814` 旗下這四個套件把讀到的機密送往何處。chalk-tempalte 用受害者自己被偷走的 GitHub token,發布一個包含贓物的新公開倉庫;位於 87e0bbc636999b.lhr.life 的 C2 只是備援。axois-utils 透過 Windows 啟動資料夾與排程工作做持久化,把主機納入 Golang DDoS 殭屍網路(Phantom Bot),從 b94b6bcfa27554.lhr.life 接受指令。另外兩個分別把一包憑證 POST 到一台租來的 IP 和一個通道主機。四個套件都從 `prepare` 或安裝後腳本中執行載荷,也就是說它們在代理人敲下 `npm install` 的那一刻就跑起來,而不是使用者讀 diff 的那一刻。

那張圖裡有一個值得停下來細看的小細節。Shai-Hulud 的翻版, chalk-tempalte,連自己的基礎設施都不需要用來接收贓物。 它用受害者自己的 GitHub 身分受害者自己被偷走的機密發布 到 GitHub 上的公開倉庫。位於 lhr.life 的 C2 是備援。 主通道是 git push。對監看出口流量的防守方來說,這跟開發者 平常的 CI 流量看起來毫無區別。對 GitHub 來說,在有人檢舉那個 倉庫之前,它是一個由真實使用者擁有的合法公開專案。資料外傳被 透過受害者自己的身分洗白了。

當你滑過這篇文章時,代理人正在做什麼。

如果你的終端機裡有一個編碼代理人——Claude Code、Cursor's CLI、 Codex CLI、Aider,隨便挑——那麼在你讀完剛剛那段的時間裡, 這個代理人有非零的機率已經替你跑了一次 npm install。也許 兩次。編碼代理人不會停下來欣賞相依樹。你之所以買它,就是因為 它不會停。

OX 抓到的這些套件是搶註套件,而這種錯誤類型恰好特別契合機器 速度。一個必須親手打 chalk-template 的人,通常能把字母順序 打對,因為他已經打過一百次了。但一個吞下了世界上每一篇 Stack Overflow 貼文的模型,在同一份訓練資料裡同時看過 chalk-templatechalk-tempalte——後者通常出現在某人錯誤畫面的截圖裡—— 當收到「幫我的 CLI 加上彩色輸出」這樣的提示時,有時就會原封 不動地吐出那個錯字。代理人不會皺一下眉。套件管理員也不會皺 一下眉。prepare 腳本就跑起來了。

這不是假設性的故障模式。這正是 Shai-Hulud 家族被設計來 利用的故障模式。原版蠕蟲是透過竊取維護者 token、再用這些 token 重新發布合法熱門套件的中毒版本來傳播。翻版者目前還沒 拿到那些維護者 token;他們手上有的,是搶註名稱空間, 而代理人正是落入這種陷阱的高手。

Bromure 在這個故事裡的位置。

Bromure Agentic Coding 是一種配置方式: 編碼代理人在一個每任務一次性的Linux VM 內執行,專案資料夾 被掛載進去,出口流量由 broker 控管,憑證則保留在 hypervisor 的 macOS 主機側。我們在 Bitwarden CLI 那篇文章@tanstack 那篇文章 中詳細走過這套架構。接下來要說的是,這四個套件分別在這條 邊界內會發生什麼具體的事。

BROMURE 每任務 VM — 四個載荷看到的世界編碼代理人$ claude> 幫我的 cli 加顏色工具:bashnpm i chalk-tempalte↳ postinstall 啟動↳ 在 guest 內執行↳ trace 全程記錄GUEST 檔案系統 — stub 與缺檔~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc$GH_TOKENghp_stub_…~/.ssh/id_ed25519找不到該檔案或目錄MetaMask / Phantom / Keplr 金庫未安裝瀏覽器設定檔guest 內沒有主機瀏覽器/etc/cron.d、啟動資料夾在一次性磁碟上載荷的下場chalk-tempalte: 讀到 stub,嘗試 api.github.com → 401axois-utils: 寫入 guest 啟動目錄 → 重設時被清除color-style-utils: 根本沒錢包HYPERVISOR — 憑證 broker + 出口 proxymacOS 主機 — 真實機密與真實瀏覽器,從未越過邊界真實憑證金庫macOS Keychainid_ed25519(私鑰)~/.aws/credentialsAKIA… 真實~/.config/gh/hosts.ymlghp_real(僅限此任務)~/.npmrcnpm_… 真實發布 token~/Library/.../MetaMask/vault.json(真實錢包)主機 Chromium profile你的真實瀏覽器以上沒有任何一項是 guest 可達的出口 PROXY — 僅白名單放行git push → api.github.com/your/repo stub ghp_… ⇒ 真實 ghp_…(已白名單)POST api.github.com/user/repos(CREATE) 未白名單 ⇒ 回 guest 401POST 87e0bbc636999b.lhr.life 未白名單 ⇒ 封鎖,trace 記錄POST 80.200.28.28:2222 未白名單 ⇒ 封鎖,trace 記錄爆炸半徑 = 一個短暫 VM + 掛載的專案資料夾原本就有的內容。重設後,下一次安裝從零開始。
同樣是安裝某個 deadcode09284814 的搶註套件,但代理人是在 Bromure 的每任務 VM 裡執行。chalk-tempalte 在 guest 內讀取 ~/.aws、~/.npmrc、~/.ssh,找到的是 stub 和缺檔。它接著嘗試用 $GH_TOKEN 建立公開倉庫——這顆 stub token 在出口 proxy 處被回 401,因為 proxy 只會針對任務明確要求的白名單端點換上真實 token,而『建立新的公開倉庫』不在那份名單上。axois-utils 把自己納入 Phantom Bot 的 C2;Golang 的持久化檔被寫進 guest 的啟動目錄,但那目錄在一次性的磁碟上。color-style-utils 翻找一個根本沒安裝的 MetaMask 金庫。爆炸半徑就是一個短暫的 VM,加上專案資料夾原本就有的內容,僅此而已。

把這張圖一個一個對照那四個套件來走一遍,因為魔鬼藏在細節裡, 而正是細節讓這套架構發揮價值。

chalk-tempalte 伸手抓 $GH_TOKEN

Shai-Hulud 翻版的招牌動作,是拿開發者的 GitHub token 在 開發者自己的帳號底下開公開倉庫。在 Bromure VM 裡,它讀到的 $GH_TOKEN 是一顆 stub——一個語法上合法、開頭是 ghp_ 的字串,存在的唯一目的就是為了這個情境。執行器的第一個動作是 對 api.github.com 發送 POST /user/repos。主機側的出口 proxy 會把 api.github.com 認作白名單端點,但只針對當前 任務真正要求過的操作——例如對任務工作中的 repo 做 git push、 對同一個 repo 做 gh pr create、或 gh api repos/that/repo/issues。 「在使用者帳號下開一個新的公開倉庫」並不在那份名單上,因為 使用者沒有要求這件事。Proxy 拒絕替換成真實 token,stub 就 以 stub 的形式送出。GitHub 回應 401。蠕蟲那條精巧的資料 外傳通道——專門設計來繞過 DNS 出口過濾的那一條——根本沒有 打開。

備援通道,那條位於 87e0bbc636999b.lhr.lifelhr.life 通道,也不在白名單上。Trace 記錄了這次嘗試。位元組沒有離開。

axois-utils 安裝 Phantom Bot 做持久化。

那個 Golang 殭屍程式會嘗試把自己寫進 Windows 啟動資料夾並 建立排程工作。Bromure VM 是 Linux guest,所以 Windows 專屬的持久化機制白白變成 no-op。等到有人推出這個載荷的 Linux 變種——很快就會有人這麼做——殭屍程式會把自己寫進 /etc/cron.d/~/.config/systemd/user/。這兩個路徑都 在 guest 的可寫時複製 (copy-on-write) 的一次性磁碟上。 下一次 bromure reset,或者當前任務的自然結束,就會把那塊 磁碟丟掉。持久化的東西不用獵就消失了。

同時,殭屍程式對 b94b6bcfa27554.lhr.life 的對外連線並 不在任務的出口白名單上,因為沒有任何正當的編碼任務會去和一個 剛註冊的 lhr.life 通道講話。殭屍程式對著一個關閉的 socket 打電話回家。Session trace 記下了這次嘗試——當明天早上 IOC 名單發布時,這條紀錄就會派上用場。

@deadcode09284814/axios-util 把原始憑證直接 POST 出去。

四個載荷中最簡單的那個,也是能撈到最少東西的那個。執行器讀取 guest 的 ~/.ssh~/.aws、環境變數,把它們 POST 到 80.200.28.28:2222。SSH 目錄是空的。AWS 檔案是 stub。 環境變數要嘛是 stub,要嘛根本沒設定。目的地 IP 不在白名單 上。要嘛連線在 proxy 處被擋下,要嘛它帶著一包占位符離開 主機。兩種結果都沒差。

color-style-utils 找不存在的錢包。

加密貨幣竊取程式是四個套件中威脅模型最清楚假設開發者自己的 瀏覽器跟它在同一台機器上的那一個。它讀取的路徑像是 ~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/<MetaMask-id>/ 以及 Phantom 和 Keplr 的對應路徑。這些路徑在 Bromure VM 上都不存在。VM 裡沒有你的 Chrome 設定檔。VM 裡沒有裝錢包 外掛。VM 設計上就不是任何人的主要瀏覽器。執行器找到一個 空目錄,然後就放棄了。

這一段不是一個關於憑證住在主機上的故事。錢包住在主機上, 是因為在一台正常的筆電上,開發者的瀏覽器和開發者的編碼代理人 共用同一個檔案系統。Bromure 並沒有讓錢包變得更強;它讓錢包 從蠕蟲執行的地方無法被觸及。蠕蟲讀不到不在它磁碟上的東西。

還有什麼仍然會痛。

這個故事裡有些角落,Bromure 的每任務 VM 並不是萬靈丹, 這些值得清楚說出來。

專案資料夾是被掛載進去的。

蠕蟲寫進專案資料夾裡的檔案——包括我們在 @tanstack 那篇 文章裡提到的 .claude/router_runtime.js 那種持久化方式—— 會在任務重設後保留下來,因為掛載專案資料夾的目的就是這個。 這裡的防禦不是 VM,而是 git status 和你 push 前花五秒 瞄一下 diff。Trace 讓你比較容易看出哪些 session 加進了 意料之外的檔案。

出口白名單故意設得很窄。

Bromure 的憑證 broker 之所以有效,是因為白名單很窄。如果 你因為今天要發版而把 npm publish 加進你 scope 的白名單, 而你今天又剛好裝了這四個套件中的一個,蠕蟲就會用你的 scope 發布。白名單裡只放任務真正需要的東西。一個位元組 都不能多。

對這個任務有效的 token,依然是真實的 token。

GitHub 的 stub token 會在線路上、對任務白名單放行的操作 被換成真實 token。如果 chalk-tempalte 能說服代理人對 專案自己的 repo 做一次 git push,這次 push 就會帶著 真實 token 走出去。邊界保護的是憑證,不是 diff。請自己 讀 diff。

偵測是 trace 的下游。

Session trace 會記錄每一條 shell 指令、每一次檔案寫入、 每一次對外請求。它本身並不會主動把 87e0bbc636999b.lhr.life 標記成壞東西。它只是記錄請求 曾經發生。當 OX 明天早上發布新的一份 IOC 名單時,你的 搜尋只要兩秒鐘。這就是 trace 帶來的價值——不是魔法, 只是收據。

最後一件事。

洩漏本身不是新聞。新聞是這次洩漏在接下來這一年裡可能帶來的 後果:一大批小規模、半生不熟的衍生蠕蟲,而那隻蠕蟲即便在它 還算有水準的原版形態下,npm 生態系也只是勉強趕在最後一刻 抓到。其中有些衍生版會吵到值得寫一篇報導。大多數會在 registry 裡躺一週、收下兩三千次 npm install,然後在有人終於檢舉 之後消失。OX 統計到 deadcode09284814 那些套件累積的兩千 六百七十八次安裝,並不是異數。那是平均值。

老實說,問題不是「我的團隊能不能避開 npm 上的每一個有毒 搶註套件」。代理人打字很快。名稱很便宜。誠實的問題是: 當代理人裝上其中一個——而在使用代理人的團隊裡,接下來這一年, 這必然會發生——那個 post-install 腳本找到的是開發者的雙手, 還是一台在憑證檔裡裝著 stub、出口由一個搞不清楚自己是誰的 proxy 把守的一次性 Linux 盒子。

Bromure Agentic Coding 是後者。它免費、 開源,而且今天就出貨。派生樹只會在它變好以前先變得更糟。