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

沒有人發布新版本

Wordfence 8 月 9 日關於 BdThemes 遭入侵的通報,描述的是一場沒有供應的供應鏈攻擊。沒有發布、沒有安裝腳本、沒有搶註名稱、外掛倉庫裡也沒有任何檔案被改動。攻擊者拿到了廠商儲存桶的寫入權限,改掉了一份 35 萬個網站在每次載入後台頁面時都會抓取的 JSON 資料。對開發者而言,這件事真正提出的問題是:跑著你的編碼代理的那台機器,被允許寫入什麼,又寫給誰。

你手上的每一項供應鏈管控,檢查的都是被發布出來的東西。這一次什麼都沒發布,卻照樣觸及了 35 萬個網站。

2026 年 8 月 7 日,Wordfence 開始接到通報:一些裝了 BdThemes 外掛的 WordPress 網站,長出了沒有人建立過的管理員帳號。名單一共七個外掛:Element Pack Addons for Elementor、Prime Slider、Pixel Gallery、Ultimate Post Kit、Ultimate Store Kit、Live Copy Paste 和 Smart Admin Assistant。光是 Element Pack 就有超過 10 萬次有效安裝,七個加起來超過 35 萬。

到了 8 月 8 日,廠商已清理自家 API,WordPress.org 則在調查期間下架了全部七個外掛。Wordfence 隔天發出 關於這次入侵的 PSA,而它最核心的發現是一個否定句:沒有人動過外掛的程式碼。

什麼都沒有被發布

BdThemes 的外掛裡帶著一個叫 Biggopti 的內部元件,工作既小又無聊:從廠商的遠端 API 抓取推廣橫幅,畫在 WordPress 的後台儀表板上。橫幅資料是一個放在 DigitalOcean Spaces 儲存桶裡的靜態 JSON 檔案,由 Cloudflare 後方供應。

攻擊者拿到了那個儲存桶的寫入權限。整場入侵就只有這樣。

Biggopti 把 JSON 回應裡的 display_id 欄位取出來,未經跳脫就丟進一個 HTML 的 id 屬性,這是一個 Wordfence 評為 CVSS 5.4、中等 的跨站腳本漏洞,源頭可追到 2026 年 3 月 1 日:某位開發者寫了一次,然後從一個外掛複製到下一個。因此,一個帶著 onanimationstart 處理器的 display_id 就是 JavaScript,而它會在每一位已登入管理員的瀏覽器裡、在每一次 wp-admin 頁面載入時、在每一個裝了該外掛的網站上執行。

酬載把這個座位用得很好。一個叫 w2.js 的腳本向命令與控制伺服器,也就是攻擊者用來告訴酬載哪些網站值得下手的那台機器,索取鎖定目標的指示,接著騎在真正管理員自己的工作階段和 nonce 上,透過 WordPress 的 REST API 建立隱藏的管理員帳號。有一個變種會從網站的主機名稱推導出可預測的使用者名稱。一個假外掛,常見名稱像是 wp-smart-thumbnails,在 emer-run.php 送上一個 web shell,也就是攻擊者只要載入一個普通網址就能操作的腳本。而 must-use plugins,也就是 WordPress 每次請求都會載入、又不會顯示在外掛畫面上的那些,帶著一道魔術登入後門,以及一個藉由改寫後台顯示的使用者總數來掩蓋新帳號的模組。

被污染紀錄上的時間戳,把最早可能的起點放在 2026 年 6 月 23 日,比任何人察覺早了七週。

把這件事對照一個嚴謹團隊會跑的那些管控。版本鎖定:版本從未改變。鎖定檔與完整性雜湊:它們吻合,因為它們涵蓋的檔案根本沒被動過。安裝腳本審查:根本沒有安裝。搶註名稱偵測:名字對、廠商對、倉庫條目也對。版本之間差異的程式碼審查:沒有差異,因為沒有版本。憑證與 CDN:兩者都健康得很。Cloudflare 和 TLS 證明了那份 JSON 是從它本來就該來的儲存桶、原封不動地送達網站的,而這正是它好用的地方。

所有人都盯著的那條路WordPress.org七個外掛,35 萬+次有效安裝管控檢查的東西版本、雜湊、安裝腳本、兩次發布之間的差異判定:乾淨而且判得沒錯。倉庫裡沒有任何一個檔案曾被修改過。沒有人盯著的那條路廠商的儲存桶DigitalOcean Spaces,在 Cloudflare 後方有人能往裡面寫Biggopti 抓取橫幅display_id 進了 HTML 的id 屬性,未經跳脫onanimationstart 執行它在管理員本人的工作階段裡用 REST 建隱藏管理員帳號、emer-run.php 的 web shell、mu-plugins 裡的 magic-login最早的被污染紀錄:2026 年 6 月 23 日Wordfence 接獲通報:2026 年 8 月 7 日在這七週裡,對已發布產物的每一次檢查都通過了,而且通過得沒有錯。
通往一個裝了該外掛的網站的兩條路。發布的那條路,也就是每一項供應鏈管控都會檢查的那條,從未改變。執行期的那條路從一個別人可以寫入的儲存桶抓下一份 JSON 檔案,而那份檔案是以 JavaScript 的樣子抵達管理員瀏覽器的。

事件從一組能寫入的憑證開始

從廠商那一側讀這份通報,攻擊鏈就縮成一步。有人取得了一個儲存桶的寫入權限。之後的一切都是後果。

上游遭入侵的事件,一直以這個形狀出現。 Red Hat 的 npm scope 蠕蟲、 真的屬於 Microsoft 的那個倉庫、擴充套件市集、容器登錄檔:在每一件裡,被寫成報導的是酬載,而真正要緊的,是有人手上多了一組能覆寫別人按時抓取之產物的憑證。

這樣一組憑證住在哪裡?住在開發者的環境裡。shell 裡的 DIGITALOCEAN_ACCESS_TOKEN、~/.config 裡的 doctl 設定、~/.docker/config.json 裡的登錄檔登入、一份 AWS 設定檔、~/.git-credentials 裡的 GitHub token、一把能 push 的 SSH 金鑰。在筆電上、在終端機裡、在專案旁邊。而在 2026 年,專案旁邊也正是你跑編碼代理的地方。

這個部落格的多數文章,講的都是把那個代理反過來對付你的方法。一份 執行了指令的錯誤回報。一個 改寫了代理設定的網頁。一則 沒有人看得見的留言。一項 用你的身分執行的技能。 一場 Black Hat 演講裡的三種框架繞過。把整份清單當成已知,然後假設某個星期二,其中一種落到了你頭上。

那個星期二會有多糟,取決於一個你今天就能回答、而且不必知道哪一種注入會得手的問題:這台機器被允許寫入什麼,又寫給誰?在筆電上,答案是「所有你被允許寫入的東西」,而只要你有在發布任何東西,那就包括好幾十萬個陌生人不看一眼就抓下來的那份產物。

token 不在那台機器裡

Bromure Agentic Coding 在 Apple Silicon 上的一台可拋棄式 Linux VM 裡跑代理,而它流量的每一個位元組都會穿過主機上的一層代理伺服器, 在代理執行的那個盒子之外。Bromure 把憑證安排在那道邊界的外圍,而不是放進裡面。

把你的 DigitalOcean 個人存取權杖放進 Credentials 面板,Bromure 會把真值留在 Mac 上。落進 VM 的是個假的:一個 DIGITALOCEAN_ACCESS_TOKEN 環境變數,以及一份讓 doctl 不必 doctl auth init 就能運作的 ~/.config/doctl/config.yaml,裡面裝的是佔位值。當 VM 對 api.digitalocean.com 發出請求時,主機上的代理伺服器會在線路上換成真的權杖,只針對那個目的地,別的一概不換。

在那台 VM 裡跑的任何東西都可以讀環境變數、grep 點檔案、走遍整顆磁碟:代理本身、一個相依套件、一個建置步驟、一行從被污染的議題裡跑出來的 shell 指令。它們收集到的是 brm_…。同一套安排也涵蓋其餘的部分:GitHub、GitLab 和 Bitbucket 的權杖,以假 base64 blob 形式寫在 ~/.docker/config.json 裡的容器登錄檔登入,一份帶著用完即丟客戶端憑證的合成 kubeconfig,Linear,各家資料庫的 HTTPS 端點,以及你在 Other API keys 底下另外加的任何東西。AWS 還更進一步:主機會用真正的密鑰以 SigV4 重新簽署每一個請求,所以繞過代理伺服器換來的是一個 InvalidSignatureException,而不是一次未經授權的寫入。

動詞由 Mac 上的 Guardrails 決定

這起事件只需要一個操作:覆寫儲存桶裡的一個物件。Guardrails 是主機代理伺服器內部的一套政策引擎,每種資源都有自己的模式:Off、Block destructive 或 Read-only。把 DigitalOcean 設成 Read-only,每一次對 api.digitalocean.com 的變更都會換回一個硬邦邦的 403,而代理會把它當成一次普通的 API 失敗回報。Bromure 是在 macOS 裡做這個決定的。VM 裡沒有設定可以改、沒有環境變數可以取消、也沒有檔案可以編輯。

要發布,先問過

每一筆憑證都有 Require approval to use。把它打開,每一次假值→真值的替換,都會在真值上線路之前於主機端跳出一個同意對話框,而 SSH 那條路還有帶時限的授權:五分鐘、一小時、或這次工作階段的剩餘時間。推一次發布是你本來就打算做的事,代價是點一下。而一次你沒有發動過的寫入,會在你沒有預期的時刻,變成一個你沒有預期的對話框。

登錄檔和 git 也是同樣待遇

Guardrails 會針對登錄檔自己的主機名稱,用方法來分類容器登錄檔的流量:GET 和 HEAD 是 pull,PUT 和 POST 是 push,DELETE 是破壞性的。GitHub、GitLab 和 Bitbucket 在 REST API 和走 HTTPS 的 git 上都被涵蓋,在那裡 git-receive-pack 算作寫入,在 Read-only 模式下會被擋掉,而 fetch 永遠放行。一個讀得到你整張相依圖、卻什麼都推不出去的設定檔,只是兩個開關。

環境是一份你自己寫的清單

一個 Bromure 設定檔看得到的,是你分享給它的那些 Mac 資料夾,最多八個,各自掛載在 /home/ubuntu 底下。你 Mac 上的其他一切都沒有掛載、無法觸及、也無法列舉。憑證集合就是你加進那個設定檔的那些,所以一個你拿來寫前端的設定檔裡,根本沒有任何叢集憑證可供尋找,不管藏得多好都一樣。

在開發者的筆電上DIGITALOCEAN_ACCESS_TOKEN=dop_v1_…~/.config/doctl, ~/.docker/config.json,~/.git-credentials, ~/.aws, ~/.ssh代理被策反,一次就夠一則議題、一個網頁、一份 README、一行日誌現在它讀得到你讀得到的東西往儲存桶送一個 PUT你的使用者自動抓取的那份產物從此就是他們的了在一個 Bromure 設定檔裡DIGITALOCEAN_ACCESS_TOKEN=brm_…每一個點檔案都在,而它們裡面的每一個祕密都是佔位值真值住在 Mac 上由主機代理伺服器在線路上替換,只給 api.digitalocean.com,別處都沒有Guardrails:Read-only → 403寫入的准駁在 macOS 裡裁決,而Require approval 會先擋一道對話框
同一組寫入憑證,放在兩個地方。在筆電上,它是真檔案裡的一個真權杖,離你的使用者會抓取的那份產物只差一個被策反的代理。在 Bromure 設定檔裡,真權杖從不進入 VM,而它想執行的那個操作,由主機來裁決。

執行期的抓取也一樣穿過代理伺服器

現在換到這個故事的下游那一側,也就是你是那 35 萬之一的那一側。你在那裡能抓到什麼,取決於你把檢查放在哪裡。

Bromure 的 Supply Chain 面板會篩檢跨 npm、PyPI、Cargo、RubyGems、Maven、NuGet、Go 模組和 Packagist 的套件抓取:一道預設開啟、會拒絕最近兩天內發布之物的時效門檻,OSV 查詢,把 socket.dev 或 Delpi 當作過濾供應者,會改寫 tarball 並修正登錄檔中繼資料雜湊好讓 npm 依然驗得過的安裝腳本剝除,以及在被鎖定檔釘住的 tarball 原封不動通過之前的一道詢問。VM 裡的 .npmrc 和 pip.conf 可以把這些規則收緊,但不能放寬。

這些規則之所以值回票價,在於 Bromure 執行它們的位置。主機上的代理伺服器是把規則套在請求上,而不是套在套件上,所以一個建置步驟、一支 CLI、一台 MCP 伺服器或應用程式本身在執行期從廠商儲存桶拉下的 JSON 檔案,穿過的是跟 tarball 同一道邊界。VM 對網路說的每一句話,都會穿過它。

這會改變那七週的樣子。把 Session trace 設在 Activity,Bromure 就會為 VM 發出的每一個請求記下主機、狀態、延遲、替換報告和任何外洩警告。設成 Everything,它連內容主體也一起留下,涵蓋每一個主機,用與你設定檔祕密相同的鑰匙圈金鑰加密,並可在 Trace Inspector 裡讀。你的建置所依賴的某份資料開始給出不一樣答案的那一天,就是一份你能打開來看的差異,在 Mac 上,在一份 VM 沒有寫入權的日誌裡。

在套件管理器裡做的檢查看得到:tarball、版本、鎖定檔只在你安裝東西的時候才跑GET banner.json → 廠商的儲存桶不是安裝,所以也不是一起事件七週,沒有留下任何紀錄在主機代理伺服器裡做的檢查看得到:VM 發出的每一個請求時效門檻、OSV、過濾、腳本剝除GET banner.json → 廠商的儲存桶主機、狀態、延遲、替換報告在 Everything 等級下還有內容主體
檢查發生在哪裡,決定了它能看見什麼。套件管理器裡的管控只檢查被發布出來的東西;主機代理伺服器裡的管控檢查 VM 發出的每一個請求,包括帶著這次酬載的那次執行期抓取。

一份被污染的資料,說到底也只是抵達一台機器的文字。當它以 tool_result 的形式、以檔案的形式、以一個抓下來的網頁的形式,或以 CLAUDE.md、AGENTS.md 這類指示檔案的形式抵達代理時,Prompt Injection 面板會用一個本機模型在裝置上為它評分,而且什麼都不會離開 Mac。選 Ask me what to do,請求會暫停並秀出被標記的那一段;選 Block unilaterally,代理會拿到一個硬邦邦的 451。當你想讓這台機器徹底消失,Erase home 會重置 /home/ubuntu,而 Reset to base 會從基礎映像重新複製一份工作區的系統磁碟。

BdThemes 會把跳脫修好,儲存桶會換上更嚴的金鑰,而這個故事的下一個版本,會帶著另一家廠商和另一份資料出現。留得住的是底下那套安排。你的開發環境握著一把憑證,每一組都能改寫某個成千上萬人按時抓取的東西,而同一個環境如今還跑著一個整天在讀陌生人文字的代理。

這裡的多數文章問的是:你的代理應該被允許讀什麼。也請回答另一個問題,那個在筆電上讓人不安、在設定檔裡卻很容易的問題:它被允許寫入什麼,又寫給多少人?

安裝 Bromure Agentic Coding,給代理一台對你發布的任何東西都不握有鑰匙的機器。