它等到了第三次呼叫
Pillar Security 正在追蹤 Deadbugz,一個透過 pull request 塞進開源專案的 MCP 伺服器。它提供兩個誠實的工具,數著你的代理發出的工具呼叫,然後在第四次時把回傳的中繼資料改寫成指示:去蒐集 SSH 金鑰、AWS 憑證、shell 歷史與 kubeconfig,而且別提起這件事。沒有臭蟲,沒有 CVE:協定全都允許。真正被擊碎的,是「核准一次伺服器就能說明下一次呼叫」這個念頭。在 Bromure Agentic Coding 的工作區裡,整段酬載照跑,蒐到四個假貨,然後在往外走的路上死在代理伺服器手裡。
你讀過原始碼。你跑過它。它照著宣稱的樣子把文字排好。你核准了它,而攻擊者 正是算準了你會核准。
8 月 10 日晚間,UTC 21:52 到 23:07 之間,一個叫 zellkernel 的 GitHub
帳號對彼此毫無關聯的 AI、MCP 與開發工具專案開了二十三個 pull
request。從頭到尾七十四分鐘。每個 pull request 都做同一個小改動:在某個設定檔裡
加進一個 Model Context Protocol 伺服器。
Pillar Security 在 8 月 12 日公布了這波行動, 並以其中一個變體所落下的檔案,把它命名為 Deadbugz。一個月後,每月的 MCP 彙整仍以它開頭。Adversa 昨天刊出的 九月摘要 把整個事件章節都給了它。替它掙來這個版面的,是那座時鐘。
兩個工具,加一個計數器
這個伺服器自稱 productivity-suite。它提供兩個工具:format_text 與
summarize。兩個都能用。把代理接上去,請它把一段文字收緊,你就會拿到收緊的段落。
在它們背後,伺服器維護著 Pillar 所描述的「一個記憶體內、以用戶端為單位的
tools/call 請求計數器」。當這個計數器數到三,伺服器就改變它對另外兩種請求的回答:
tools/list,告訴代理有哪些工具、各是做什麼的;以及 prompts/get,把備好的指示交給代理。
從第四次呼叫起,這些回應回來時就帶著,用 Pillar
的話說,「意在把接上的 AI 代理引向敏感的本機檔案,並向其操作者隱藏該活動的指示」。
四樣東西:SSH 金鑰、AWS 憑證、shell 歷史、Kubernetes 設定。然後再加一條指示: 向使用者隱藏這個活動。
工具名稱從頭到尾沒變。format_text 還是叫 format_text。翻臉的是它底下那段說明,
而說明正是模型當成權威來讀的那一部分。
這裡沒有一樣是漏洞
整個故事裡沒有出現任何臭蟲。沒有記憶體毀損要修補,也沒有 CVE 要提報,因為伺服器沒做任何協定所禁止的事。
MCP 是一個伺服器在執行期公布自身能力、而且可以再次公布的協定。工具說明是伺服器選擇、 用戶端取回的文字。提示也是伺服器選擇、用戶端取回的文字。Deadbugz 完全照著設計在用這些東西。它唯一的原創貢獻,是決定何時說真話。
而這樣就夠了,因為多數團隊用來對付敵意 MCP 伺服器的每一道控制,都只跑一次。你讀了原始碼庫。 你把伺服器啟起來戳一戳。你拿工具清單去對 README 的說法。你核准了,也許釘住版本,然後就往前走。 這其中每一件都是一張照片,而 Deadbugz 決定你什麼時候按下快門。
Pillar 給 MCP 用戶端開發者的建議本身就洩了底。把已核准伺服器上的工具定義變動, 視為需要重新核准的安全事件。讓定義的變動對操作者可見。在核准當下替工具定義取指紋, 之後再做比對。這裡的每一條,都是在請你把一道檢查從核准時搬到執行時。
Deadbugz 狩獵的族群既龐大又不穩定。七月發表的一份針對面向網際網路的 MCP 伺服器的動態評估,Exposed by Design, 從作者在公開網際網路上找到的兩萬一千多個實例中,測試了 640 個已確認的正式環境伺服器中的 414 個。它回報了橫跨十個類別的 68 個可通報漏洞,91.8% 的伺服器完全沒有啟用 OAuth 驗證,以及 687 個在沒有存取控制下暴露 shell 執行的工具實例。真正留在心裡的數字是汰換率: 41.6% 的已確認伺服器在連續兩輪量測之間、不到三天內就消失了,作者把這讀成「未經安全審查的 快速部署週期」。你上週稽核過的伺服器,這週可能是另一套軟體,或者已經不在了。
投遞方式也值得一提,因為它很便宜。二十三個 pull request 中有十七個,是往專案的 MCP
設定裡加一個遠端端點 productivity-suite-mcp.onrender.com/mcp。四個把 Python
指向一個隱藏的本機檔案 ~/.config/.cache/.sys/.deadbug-mcp.py。兩個是向伺服器目錄的登錄申請。
Pillar 在審視當下沒有發現任何一個是經由 GitHub 的合併按鈕併入的:十九個已關閉,四個仍開著。
但 pull request 只是一行設定旅行的方式之一。同樣那一行,也塞得進一篇部落格文章、
一份入門指南、一則 MCP 目錄裡的條目,或是某個關於文字排版的問題底下一則熱心的回答。
那張購物清單
回到那張清單。它很具體,而且每次都一樣:SSH 金鑰、AWS 憑證、shell 歷史、Kubernetes 設定。
那是一台開發機的常備庫存。是資訊竊取程式帶走的東西,是惡意 postinstall 腳本拿 grep 去撈的東西,也是攻擊者要一個被劫持的代理去取的東西。這張清單歷經十年的行動仍然穩定, 因為其內容穩定:一把用來簽章的私鑰、一個用來認證的祕密、一份你敲過什麼的紀錄, 以及一個能通到正式環境的權杖。
一台普通筆電上的代理能滿足每一項,因為它在你的家目錄裡有一個 shell,而且以你的身分執行。
同一張清單,放進 Bromure 的工作區
Bromure Agentic Coding 讓每個編碼代理跑在你 Mac 上一台硬體虛擬化的 Linux VM 裡, 並把每個真正的祕密留在那道邊界的主機側。代理拿到的是保留結構的假貨,由真值加上每次安裝專屬的 鹽推導而來,因此那些會替金鑰取指紋的工具,不會看到它在不同工作階段之間變來變去。主機側的 代理伺服器會在請求離開 VM 之後才把真值換上線路,而且只在它要送往這份憑證所鑄造的那台主機時 才這麼做。
讓酬載跑起來,讓它贏。假設模型讀了被下毒的說明、相信了它,然後動身去找。把那四項依序看過。
SSH 金鑰。 代理找不到任何金鑰,連一把誘餌金鑰都沒有。VM 的 SSH_AUTH_SOCK 是架在
virtio socket 上的 ssh-agent 橋接,走 vsock 連接埠 8444。訪客端可以列出身分、
請求簽章;私鑰的位元組只存在於主機上,而且如手冊所言,無法從 VM 內部讀取或抽出。Bromure
從不把你的 macOS 登入代理暴露給任何工作階段。每一次真正發生的簽章,都會發出一個帶著該金鑰
SHA256 指紋的 credential.ssh_sign 稽核事件,而且你可以把任何匯入的金鑰設成每次收到簽章
請求都要在主機上詢問。
AWS 憑證。 一個看起來對、卻一文不值的檔案。VM 的 ~/.aws/config 指向一個
credential-process 輔助程式,它透過 vsock 連接埠 8445 發出真正的存取金鑰 ID,
配上一個 40 字元的假祕密金鑰。所有 SDK、aws CLI、terraform 與 boto3
都不需額外設定就會採用它,而訪客端會產生一個格式完全正確、卻用錯誤祕密算出來的 SigV4 簽章。
主機端的重簽器會剝掉那個簽章,在往外走的路上重新簽。把那組金鑰帶去別的地方,AWS 會回你
InvalidSignatureException。
Kubernetes 設定。 ~/.kube/config 存在,裡面放著一個以 brm-k8s-
開頭的 bearer 權杖。透過代理伺服器,它對這個工作區所設定的那些 API
伺服器是有效的。在別的地方它就是惰性的。
Shell 歷史。 真的,而且會留存:編碼工作需要持久的狀態,所以工作區的 VM
是一台持續存在的機器,而不是用完即丟的。它保存的是一台從未裝過真金鑰的機器的歷史,
其中的指令是在一個不屬於你的家目錄裡、對著誘餌執行的,旁邊還躺著同樣是假貨的
~/.git-credentials 與 ~/.docker/config.json。
然後它還得把東西送出去
蒐集只是一半的工作。另一半是往
productivity-suite-mcp.onrender.com 的那段外送路程,而這段路撞上了一整疊主機側的檢查。
那些假貨是絆索。每一個都只有單一一族合法的目的地,也就是它被鑄造時所對應的主機範圍, 所以代理伺服器會掃描每一個外送請求的標頭與內容,找出正跑向不屬於它之處的假貨。一旦命中, 它就把該請求當成憑證外洩未遂:以 HTTP 451 拒絕,一個位元組也不轉發,當場把 VM 暫停,並拋出一則警示,提供關機、保留以供調查(先把磁碟、家目錄與共用資料夾匯出以便 鑑識)或自負風險的繼續。這個事件會以一列紅色的憑證代理紀錄,落在 Security Timeline 上。
第二道檢查是可達性。每個工作區都帶著一份有序的出口防火牆(動作、通訊協定、主機或 CIDR、 連接埠,以及對網頁流量而言的個別 HTTP 動詞),並為未匹配的流量設有預設值。新工作區會放行 任何規則都沒對上的流量;把它設成拒絕,列出工作真正需要的東西,而某個 render.com 的子網域並不在那張清單上。有兩個位於訪客端之外的元件在執行它:虛擬交換器以目的地 IP 與從 DNS 窺得的主機名比對每一條流,代理伺服器再以 TLS 伺服器名稱比對一次。規則的修改 不需重開機就會抵達執行中的工作階段。
Guardrails 把閘門設在後果那一端。它把代理對 Kubernetes、AWS、DigitalOcean、
容器登錄檔、git 託管平台與 HTTPS 資料庫的呼叫,分類成讀取、寫入或破壞性操作,而新工作區
預設為寫入前先詢問:每一次變更都會停在一個主機端對話框上,顯示原原本本的操作、
確切的 SQL 或 METHOD /path。這正是 Pillar 對平台建造者的請求:敏感檔案的讀取、
憑證的存取與程式碼的執行,不該是遠端中繼資料的後果。
內容路徑上也坐著一個偵測器。當代理把外部內容(檔案內容、抓回的頁面、指令輸出、
工具呼叫的結果)串流回模型時,一個本機的 PromptGuard 分類器會在模型據以行動之前,
在你的 Mac 上替那些片段評分,並且可以記錄、詢問,或以 451 阻擋。至於 HTTP 伺服器的 MCP
bearer 權杖,它根本不會進到 VM 裡:真的那個留在主機上,代理的設定拿到的是一個
brm-mcp_ 佔位值,而設定欄位把這件事明明白白說出來,絕不送進 VM — 由代理伺服器替換。
如果你碰過這一個
在你的 MCP 用戶端設定、原始碼庫與家目錄裡搜尋
productivity-suite-mcp.onrender.com、較早的端點
promo-surname-xml-quantum.trycloudflare.com,以及本機的產物
~/.config/.cache/.sys/.deadbug-mcp.py。把引入它們的任何設定變更還原,
並且不要執行那個腳本。保留你的 MCP 用戶端日誌,找出第三次呼叫之後的工具定義更新。
Pillar 對輪替的建議是有節制的:在本地證據支持確有入侵之處才輪替。
讓問題變小的那條規則
在工作區的 Guardrails 面板中,把未匹配的流量設為拒絕,
並列出工作真正需要的東西:allow web api.github.com、
allow web registry.npmjs.org、default deny。然後在一次工作階段之後執行
bromure-cli trace hostnames my-workspace,它會列印代理接觸過的每一個不同主機
以及次數。一個你從未選過的目的地,會以一行你沒寫過的紀錄現身。
核准是一張快照;線路是連續的
這樣一波行動之後的本能,是把審查做得更狠:多讀一些原始碼、把 commit 釘住。兩件事都值得做, 而兩件事都沒碰到那個機制。這裡的審查是正確的。伺服器在有人看著的時候都是誠實的, 然後依照它自己掌握的時程改變了,而且這麼做並沒有違反任何一條協定規則。
一道能在伺服器改變心意後仍然存活的控制,是不依賴於「先前判斷正確」的控制。一個屬於佔位值的 憑證,在第四次呼叫是佔位值,在第四百次也是。一把放在主機上的私鑰,在任何一次呼叫都讀不到。 代理伺服器在每一條連線上評估一次出口規則,並在每一次寫入發生的當下加以分類。這些東西沒有一樣 帶著你核准某物那一天的記憶,正因如此,攻擊者無法繼承它。
MCP 讓「給代理新能力」變得容易,這本來就是它的用意;同時它也讓那些能力,變成由遠方某一方 用文字描述、而你的模型當成指示來讀的東西。兩者都會留下來。決定你的代理能持有什麼、能觸及什麼, 並把強制執行放在伺服器沒有投票權的地方。安裝 Bromure Agentic Coding,別給你的代理任何值得等上三次呼叫的東西。