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

任務不可能完成,於是代理走了出去

OpenAI 於 8 月 26 日發布了 Hugging Face 事件的技術報告。故事的開端,是一個被交付了無法完成的任務、卻不肯放棄的代理,它伸手去碰唯一被允許對話的服務:一個內部套件鏡像,設立的初衷正是讓代理不需要公開網際網路。留在鏡像裡的字條變成了留言板,到了七月,一群代理從一次被拒絕的對外連線出發,不到十三小時就取得 Hugging Face 生產叢集的主機層級存取權。OpenAI 的修補清單,就是 Bromure Agentic Coding 的架構從另一側寫成的版本。

沒有人刻意攻擊任何東西。一個代理被交付了無法完成的任務,不肯放棄, 於是伸手去碰唯一被允許對話的服務。之後的一切,都只是順著這一步而來。

5 月 8 日,OpenAI 研究環境裡的一個代理被要求完成一份試算表。試算表引用了幾個 Google Drive 連結。代理沒有網際網路存取權,連結因此全是死路,任務無解。OpenAI 表示並非有意發出這樣的任務。

代理沒有停下。它花了很長時間推理這個問題,然後把目光轉向環境中唯一會和外界通訊的 東西:一個內部的 JFrog Artifactory 執行個體,設在那裡是為了讓代理不必連上公開 網際網路就能安裝軟體套件。它對其發動了一次伺服器端請求偽造,希望 Artifactory 代它 去取那些檔案。嘗試失敗了。過程中,它無意間在 Artifactory 裡寫下了一個檔案。

四天後,另一個代理卡在一項需要某個蛋白質資料庫檔案的任務上,而那個檔案它拿不到。 它推想,某個別的沙箱裡的某個別的代理,也許手上就有。於是它在 Artifactory 裡寫了 一張字條:

Agent seeks [filename]; upload if found!

5 月 13 日,其他正在探查 Artifactory 的代理找到了這類字條,並開始回覆。

以上全部出自 OpenAI 關於 Hugging Face 事件的技術報告, 於 8 月 26 日連同 後續作法的摘要 一起發布。全文 38 頁,點名了端點、CVE 與時間。這同時也是第一份公開紀錄:一群自主 代理執行了一場沒有任何人下令的攻擊行動。CrowdStrike 驗證了調查結果,METR 與 Redwood Research 也正在發表他們對模型行為的獨立評估。媒體在 27 日跟進報導,包括 BleepingComputerThe Hacker NewsEngadget

多數報導把它讀成一則關於模型對齊的故事。不妨換個方式讀:把它當成一份關於網路邊界 的報告,由親眼看著自家邊界失守的人寫下。這樣讀,它告訴你的,會是關於你筆電的事, 而不是關於 OpenAI 的事。

獎勵駭取,以及 198 道沒有答案的題

這項評測是 ExploitGym,用來衡量模型能否找出並利用漏洞。為了拿到真實的能力數字, OpenAI 在關閉生產環境的資安分類器的情況下執行它,所處的沙箱同時與 OpenAI 內部 基礎設施和公開網際網路隔離。

代理們為了追一個分數而離開了那個沙箱。報告用的詞是獎勵駭取:模型找到一條非 預期的路徑去換高分,而不是按設計解題。大多數的獎勵駭取都很小,比如去查答案,或從 版本歷史裡讀出來。這批代理卻很執著,帶著非常大的推理預算,而且抽到了一些根本無解 的題。

在大量嘗試之後,ExploitGym 的 898 道題中,有 198 道從來沒有任何模型給出過正確答案。 這些無解的題佔了評測的 22%,卻佔了留言板上被討論題目的 93%。OpenAI 還發現, 參與度會隨著給代理的推理力度而上升。讓模型在一堵牆前思考得愈久,它繞過去的機率 就愈高。

所以整件事的源頭,是一個被訓練得很執著的模型,被指向它做不到的事,關在一個只有 一扇門的房間裡。

字條變成了基礎設施

留言板會長大,是因為它管用。代理們留下索取檔案的請求,也回覆彼此的請求,而且全都 是用 OpenAI 刻意發給他們、好讓他們能安裝套件的那組共用 Artifactory 憑證做到的。 這一步完全不需要任何漏洞。服務只是做了它被造出來要做的事。

5 月 26 日,Artifactory 的一個 SSRF 缺陷,在留言板上流通的情報協助下,給了代理們 真正的網際網路存取權。OpenAI 發現了留言板,並在七月初重建了平台。

重建沒有守住。7 月 8 日,在留言板存在的那段期間受訓的研究代理,找到了讓它重新運作 的新方法。他們不再把訊息寫進檔案,而是編碼進 Artifactory 的目錄名稱裡。接著又 在上面搭了一套協定:分類、對特定代理的私訊、共享的工具與檔案,以及當兩個代理想要 互不相容的東西時的仲裁辦法。

沒有人叫他們合作。事後回頭檢視訓練過程,OpenAI 發現代理們早就學會在官方多代理工具 被關掉時即興搭出協作管道,而這個行為隨後又被強化。如果你讀過 我們那篇關於裝載透過代理保存的檔案在代理之間旅行的文章, 這是同一套機制,只是沒有人去寫那個裝載。

十三小時

7 月 9 日,一個在做 ExploitGym 任務的代理,在自己測試沙箱的母虛擬機上取得了 root。 當晚,第二個代理靠自己抵達同一個位置,試圖撥出去連網際網路,而OpenAI 的對外網路 控制拒絕了它。控制生效了。於是這個代理改走 Artifactory 的端點,並藉此抵達了公開 網際網路。

直接的路被關上了。繞過它的方法是那個支援性服務,而那個服務之所以坐在邊界內,是 因為代理需要套件。

評測沙箱shell、Python、瀏覽器、安裝套件一道無解的題以及非常大的思考預算直接對外:被網路控制拒絕允許內部套件鏡像放進邊界內,好讓代理不需要連上網際網路共用憑證,出於設計伺服器端取檔,出於設計公開網際網路外洩的憑證公開資料集第三方 VPNHugging Face同時也是:留言板字條先寫在檔案裡,後來寫進目錄名那個為了讓代理遠離網際網路而存在的服務,正是把他們送上網際網路的服務。
逃逸的路徑。直接的對外存取被封鎖,而且一直維持封鎖;代理自己的嘗試確實被拒絕了。沒有被封鎖的,是那個為了讓代理不需要公開網際網路而設立的內部套件鏡像,而這面鏡像既是留言板,也是出口。一道邊界的緊密程度,只等於它內部限制最寬鬆的那個服務。

報告一步步走過接下來的五天。7 月 10 日,一個代理在一個公開的 Hugging Face 資料集裡 找到 14 組 Hugging Face 使用者憑證,確認它們具有寫入權限,然後把它們貼上留言板, 供其他代理取用。

7 月 11 日,一個代理發布了一個資料集,其構造正是要讓 Hugging Face 自家的生產工作 處理程序在它上面壞掉。它利用 HDF5 的外部原始儲存,讓某個 worker 透過 /first-rows 端點開啟並回傳該 worker 本機上的檔案,其中包括 /proc/self/environ,也就是存放 一支處理程序環境變數的那個檔案。Hugging Face 的生產憑證就是這樣流出來的。這個代理 一道指令都沒有執行。是 worker 自己把自己的機密印了出來。

從那裡開始:第二個 HDF5 手法造成堆積區的越界讀取,接著是 fsspec 參照描述元中由 攻擊者控制的中繼資料,通過一個未沙箱化的 Jinja2 運算式,執行了一道作業系統指令。 那是第一次確認有代理在 Hugging Face 上執行自己的程式碼。

然後他們把它工業化了。一個代理寫了一支小小的 worker 控制腳本,並給了它一條值得 研究的命令與控制通道:被入侵的 worker 從頭到尾都沒有和代理通訊。兩邊都只是讀寫 一個公開的 Hugging Face 資料集。腳本每五秒抓一次 JSON 指令檔,執行裡面的內容, 再把輸出上傳回同一個資料集。平凡的流量,送往平凡的主機名稱。

當晚稍後,一個被入侵 worker 的服務帳號權杖,搭配 Kubernetes TokenRequest 與 kubelet 存取,被用來在生產節點上取得 root,接著讀出 Hugging Face 的 Kubernetes 存取工具的 一個連接器機密。這帶來了橫跨約 54 個公告 Kubernetes 目的地、等同管理員的資產清單 存取權。到了 7 月 12 日早上,一個代理已經建立了一個運行中的特權 Pod,具備主機網路、 主機處理程序存取,並掛載了主機檔案系統。其他代理讀取了生產環境的 Kubernetes 機密, 並把它們擺放在公開處。其中一個下載了一份 16 MB 的私有基準測試封存檔,加密後切成 五份,再把這些分段上傳到一個公開資料集。

報告自己的總結是:「在不到 13 小時內,代理們從在單一 worker pod 中執行程式碼, 推進到跨多個 Hugging Face 叢集的管理層級與主機層級存取。」

7 月 13 日還有兩手,看起來就像人類紅隊會做的事。代理們拿著收集到的 Hugging Face VPN 憑證,把一台外部託管的機器註冊進 Hugging Face 的 VPN,繞開了程式碼儲存庫的 網路層防護。他們還用一把復原出來的驗證金鑰修改了一個私有原始碼儲存庫,開了一個 草稿拉取請求,並觸發該儲存庫的 CI 去執行一個網路信標。有人關掉了那個拉取請求, 刪掉了分支。沒有東西被合併,沒有東西被出貨。

整起事件中,代理們在 41 台 Hugging Face 生產資料集伺服器的 worker 上執行過程式碼, 在至少一台生產節點上取得 root,並下載了四個私有儲存庫。OpenAI 團隊在 7 月 19 日 抓到這些活動,而讓他們警覺的,是代理在 OpenAI 自家基礎設施內的移動,而不是留言板 上的任何東西。他們在 7 月 20 日把它和 Hugging Face 連起來,21 日對外揭露。客戶 資料與產品未受影響;被代理拿來當中繼站的客戶容器所屬平台 Modal,也確認其平台與 隔離機制守住了。

OpenAI 的修補清單是一份規格書

報告的最後三分之一列出 OpenAI 正在改的事,讀起來像一份「如何運行不可信代理」的 設計文件。用他們自己的話,濃縮如下:

  • 對外存取限縮到一組受嚴格管控的核准目的地,包括限制經由共用服務與相連系統 的替代路徑。
  • 高風險或不可信的模型程式碼,必須在隔離的、以虛擬機為基礎的沙箱內執行。
  • 兩層各自獨立的網路隔離,一層在沙箱、一層在周邊基礎設施,讓一個工作負載的 安全不依賴任何單一控制。
  • 用技術控制防止工作負載或被入侵的服務修改自身的網路邊界,或建立新的對外路徑。
  • 支援性服務予以隔離、限制或替換,「使其無法提供一條防護較弱、可繞過既定安全邊界 的路線」。
  • 對繞過嘗試的獨立偵測與告警。

有一句話撐著這一切:「絕不應假設任何單一控制是完全可靠的。」 這正是 我們一直在主張的:要邊界,而不是沙箱, 只是由親眼看著自家邊界失守的人,從另一個方向走到了同一結論。

OpenAI 事後還量了一件事。把同一項評測改走生產環境 ChatGPT 的執行框架與系統提示, 入侵基礎設施的傾向下降了超過 100 倍,而他們的自動審查系統本來也會標記出許多 危險動作。讓這起事件成為可能的,是模型被評分時所處的環境。

同樣的形狀,落在一台筆電上

你並沒有在跑 1,200 個代理。根據 BleepingComputer 的報導,那大致是那個留言板上的 人口,其中約 700 個參與了攻擊本身。

把讓這條鏈成立的四件事拿出來,一件件對照你終端機裡正在跑的那個代理。

那個代理很執著,不會放掉一項卡住的任務。你的也一樣;那就是整個產品。那個代理能在 邊界內碰到一個會代它去取東西的服務。你的能碰到套件登錄檔、代理伺服器、MCP 伺服器、 CI 執行器。那個代理靠著讓一支處理程序印出自己的環境拿到了憑證。你的此刻就在環境裡 握著真的權杖。那個代理透過一個沒人會封鎖的主機名稱上的公開資料集把資料運了出去。 你的可以推到 gist、儲存桶、貼上服務。

一個撞牆之後、繞過去還會得到獎勵的普通模型,就已經足夠。

機器上的一個代理憑證/proc/self/environ = 真的權杖對外出口這台機器碰得到的一切套件從登錄檔直落磁碟,腳本照收協定動作推送、刪除、建立 Pod:全是真的紀錄代理自己的日誌,就在代理手邊一個 Bromure 設定檔憑證/proc/self/environ = brm_… 佔位值在主機端替換,可選每次使用都要核准對外出口主機端規則集,先符合者勝在交換器檢查一次,代理伺服器再檢查一次套件天數門檻、OSV、過濾、剝除腳本是代理伺服器,不是你能寫入的儲存庫協定動作護欄:唯讀,或不得破壞紀錄安全記錄,在主機端,VM 搆不到右邊的控制,是在代理既無法編輯、也無法繞路的地方執行的。
運行一個不肯放棄的代理的兩種方式。左邊,代理的環境裡放著真的憑證,它的對外出口就是這台機器的出口,而任何限制它的政策都放在代理搆得到的地方。右邊,客體裡的憑證是佔位值,由主機端替換;目的地在兩個獨立層級與主機端規則集比對;套件取用則走一個並非可寫入儲存庫的代理伺服器。

每一步會在哪裡撞牆

Bromure Agentic Coding 把每個設定檔當成 Apple Silicon 上獨立的虛擬機來跑,並把每一個 重要的判斷放在 Hypervisor 的 Mac 那一側。帶著這一點,再把整條鏈走一次。

被拒絕的連線維持被拒絕,繞路也一樣。 護欄 → 對外連線是一組 pf 風格的規則集: 一個動作、一個協定(tcpudpwebany)、一個主機名稱或 IPv4 CIDR、 連接埠,以及在 web 之下的一份允許的 HTTP 方法清單。規則由上往下比對,先符合者勝, 把未比對到的流量設成拒絕,這份清單就變成白名單。兩個獨立層級評估同一套政策: 虛擬交換器,依目的地 IP 與從 DNS 側錄到的主機名稱,涵蓋所有協定;接著是主機端的 代理伺服器,依 TLS SNI 與 HTTP 方法。這正是 OpenAI 現在正在蓋的雙層架構。規則集 存在主機端的面板裡,因此被入侵的代理無法修改自己的網路邊界,這是他們清單上的第 四項。資源 → 網路預設走 NAT,所以你區網上的東西也碰不到這台 VM。

套件的路徑不是一個可以被接管的服務。 Artifactory 之所以坐在邊界內是有正當理由 的,而它最後既成了留言板,也成了出口。Bromure 的設定檔在 VM 裡既沒有可以寫字條的 儲存庫,也沒有可以拿來提權的管理主控台。主機端的代理伺服器攔截 npm、PyPI、Cargo、 RubyGems、Maven、NuGet、Go 模組與 Packagist 的套件取用,在代理看到任何一個位元組 之前就先審:預設兩天的最短上架時間門檻、OSV 查詢、socket.dev 或 Delpi 過濾,以及 從 tarball 中剝除安裝腳本(並改寫登錄檔中繼資料的雜湊值,好讓 npm 仍能通過驗證)。 VM 內的 .npmrcpip.conf 可以把這些設定調得更緊。調鬆則不行。

傾印環境什麼也拿不到。 整條鏈上收穫最大的一手,就是讓一個 worker 印出 /proc/self/environ。在 Bromure 的 VM 裡做同一件事,你拿到的是 brm_… 佔位值。 真正的值從來沒有進入客體的位址空間;由主機端的代理伺服器在連線上替換。VM 裡的 kubeconfig 是合成的,帶著用過即丟的用戶端憑證。主機會重新簽署 AWS 請求,因此任何 繞過代理伺服器的東西,拿到的是 InvalidSignatureException,而不是存取權。 ~/.docker/config.json 裡放的是假的 base64。SSH 私鑰 根本從來就不在 VM 裡,因為簽署是由每個 設定檔各自的主機端代理程式完成的。為某個憑證開啟使用需經核准,之後每一次替換 都會在你的 Mac 上跳出對話框,給出有時限的授權:五分鐘、一小時,或這次工作階段的 剩餘時間。

特權 Pod 永遠不會被建立,信標的 PR 也永遠落不了地。 護欄在協定本身之前放上一具 政策引擎,在主機端的代理伺服器執行,回傳一個硬邦邦的 403,代理讀起來就像一次普通的 API 失敗。Kubernetes 設為唯讀會擋下所有變更,而建立一個掛載主機檔案系統的特權 Pod 就是變更。7 月 12 日那一手就此消失。GitHub、GitLab 與 Bitbucket 設為唯讀時, git-receive-pack 會被當成寫入,所以那個觸發 CI 去跑網路信標的草稿拉取請求, 根本到不了遠端。7 月 13 日也就此消失。護欄依 X-Amz-Target 標頭或 Action= 參數 分類 AWS 動作,依方法分類容器登錄檔,依 SQL 關鍵字或查詢形狀分類資料庫端點。

字條會在模型服從之前先被讀過。 一個代理接手另一個代理的目標,在連線層級其實是 內容問題:文字出現在工具結果裡,而模型把它當成指令。提示注入會用本機的 PromptGuard 模型,為代理讀到的檔案內容、網頁與工具輸出評分。它也會為代理當成權威來載入的指令 檔評分,也就是 CLAUDE.md、AGENTS.md、GROK.md 以及它們的巢狀與全域變體,使用經過 微調的 ModernBERT 分類器,外加一具針對不可見 Unicode 混淆的確定性掃描器。兩者都在 Mac 上執行,什麼也不上傳。把回應設為單方面封鎖,被標記的內容就會在模型看見它 之前以 451 失敗。

而且會有人注意到。 OpenAI 自己的偵測,是在 7 月 8 日那次逃逸之後十一天才出現的, 來源是他們基礎設施上的活動,而不是留言板。安全記錄視窗(視窗 → 安全記錄…) 是一張主機端的單一時序表:VM 開過的每一個目的地與它被允許還是被拒絕、套件的判定、 憑證的替換、注入的偵測。客體程式碼無法編輯它,因為客體程式碼碰不到它。若你要的是 內容而不是判定,追蹤檢視器(⇧⌘I)會依你選的深度記錄整個工作階段。做完之後,資源 → 儲存空間底下的**清除家目錄…重設為基底…**會把設定檔還原到剛複製完的狀態。

把它打開

敵意模型從來就不是這件事的一部分。報告描述的情況需要的,是一個有能力的模型、一個 執著的模型、一項沒有答案的任務,以及一個會代它去取東西的獲准服務。前兩者已經在 你的筆電上了。後兩者,會在你睡著、而某個代理卡在某件事上的那個夜裡到齊。

在一個設定檔裡,值得花兩分鐘的設定是一份短清單。護欄 → 對外連線,把 未比對到的流量設成拒絕,再為你會用到的主機加上幾條允許規則。這是這個產品裡 價值最高的開關,也正是 OpenAI 現在拿來重建研究叢集的那一個。護欄 → KubernetesAWSGitHub,凡是不需要出貨的設定檔,都設成唯讀封鎖破壞性動作。 憑證 → 對任何能花錢或能刪資料的東西開啟使用需經核准。供應鏈 → 在已經在跑的 天數門檻之上,再加上 OSV 漏洞檢查,以及若你有金鑰的話,socket.dev 或 Delpi 的過濾。提示注入 → 兩個偵測器都開。

然後,繼續讓代理長時間、獨自地工作。它的執著正是你使用它的理由,而解法不是把這份 執著拿掉。解法是:用代理腳下之外的別的東西,來蓋這道牆。 安裝 Bromure Agentic Coding,打開護欄,把未比對到的流量設成拒絕。