返回所有文章
發佈於 · 作者 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 會針對登錄檔自己的主機名稱,用方法來分類容器登錄檔的流量:GETHEAD 是 pull,PUTPOST 是 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 裡的 .npmrcpip.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.mdAGENTS.md 這類指示檔案的形式抵達代理時,Prompt Injection 面板會用一個本機模型在裝置上為它評分,而且什麼都不會離開 Mac。選 Ask me what to do,請求會暫停並秀出被標記的那一段;選 Block unilaterally,代理會拿到一個硬邦邦的 451。當你想讓這台機器徹底消失,Erase home 會重置 /home/ubuntu,而 Reset to base 會從基礎映像重新複製一份工作區的系統磁碟。

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

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

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