Strix 的自主 agent 從一個沒人特別記得的子網域開始,翻出一個不用登入就能列清單、拉 image 的 Harbor registry;拉下一層 layer、跑一次 TruffleHog、讀一次 image history, 25 分鐘後,手上握著一個對 Baseten 三個 repo 有 admin 權限的 GitHub token。
二十五分鐘拿到 admin
這篇記錄的是 Strix——一個自主滲透測試 agent——對 Baseten 的一次授權測試。Baseten 是市值 130 億美元的推論平台,作者形容選它當測試對象是「顯而易見的選擇」。 整條攻擊鏈沒有用到任何 exploit,沒有猜密碼,全部建立在唯讀 API 請求跟一個公開可讀的 container registry 上——這也是這篇文章值得細看的地方:每一步都合法,每一步都只是「讀」, 加起來卻是三個內部 repo 的 admin 權限。這種 agent 的定位跟照 CVE 清單比對版本號的傳統掃描工具不太一樣, 它從偵察開始,一步一步做判斷、自己決定下一步要往哪裡查,中間不需要人類介入。
這個案例值得拆開來看的原因有兩個。第一是找到的每一個環節單獨看都不新鮮——子網域列舉、 匿名 registry、Docker build history 洩漏憑證——每一項都是資安圈老生常談的檢查項目, 多數團隊也都聽過。第二是把這些老生常談串起來、而且全程自動化,正好示範了防守方該擔心的 不是哪一個環節,而是有沒有人(或有沒有 agent)願意花時間把它們一路串到底。
點每個階段看 agent 實際做了什麼、拿到了什麼 · 5 個階段
偵察 · 從子網域到目標
Strix 用黑箱方式列舉 Baseten 的主機、翻憑證透明紀錄,把整個對外表面攤開——這一步不需要原始碼,也不需要任何帳號。
篩出的目標之一是 gcp-us-east4-zlw.registry.baseten.co,一個掛在 GCP us-east4 區域的 Harbor registry,網域裡那串隨機字元看起來像是自動化部署腳本建出來的。
Harbor · 匿名也能讀
這個 Harbor 專案沒有把匿名存取關掉。不用任何 token,Strix 就能列出 repository、拿到匿名 pull token、把完整的 image manifest 與 blob 下載下來。
「沒認證」本身還不算漏洞——只是讓接下來每一步都不需要密碼。
baseten-app · 拉下來看看
agent 挑了 baseten/baseten-app 這個 image,拉下所有 layer 之後兵分兩路:對 layer 內容跑 TruffleHog,同時直接檢視 image config。
真正的發現在 config 裡:image history 陣列的 created_by 欄位,躺著一段完整的 RUN 指令,裡面有一個貨真價實的 GitHub personal access token。
驗證 · 先確認它還活著
找到字串不等於找到可用的憑證。Strix 先用一個唯讀的 GET /user 打 GitHub API——200,帳號名稱 basetenbot。
回應標頭裡的 X-OAuth-Scopes 寫著 repo,帳號隸屬 basetenlabs 組織:授權範圍是整個 repo 層級。
測繪 · 這個 token 能碰到什麼
確認 scope 之後,agent 再用一連串唯讀請求,逐一查每個 repo 的實際權限。
結果是三個 repo 有 admin 加 push:basetenlabs/b***(product 本體)、basetenlabs/f***(GitOps)、basetenlabs/h***(CLI 分發),外加其他私有 repo 的 read/write。
互動圖表
從子網域列舉到匿名 Harbor,拉 image、掃 history 挖出 token,25 分鐘內測繪出三個 repo 的 admin 權限。
偵察:從子網域到一個不設防的 Harbor
Strix 的偵察邏輯很直接:先把目標的對外表面整個攤開,而不是只看官方文件裡列出的那幾個服務。 做法是列舉主機、翻憑證透明紀錄,把每一個找得到的子網域都記下來——背後的假設是, 真正嚴重的問題常常不在主清單上,而在某個被忘記的子網域,這也是為什麼黑箱測試要跟拿到原始碼的測試一起做。
這次找到的其中一個目標是 gcp-us-east4-zlw.registry.baseten.co——一個掛在
Harbor 上的 container registry,網域裡帶著 GCP us-east4 區域跟一串隨機字元,
看起來就是那種自動化部署腳本會建出來、平常沒人會特地去記的子網域。
Harbor 官方把自己定位成 CNCF 的 Graduated 專案,功能包括掃描 image 找漏洞、幫 image 簽章、 以及身分整合搭配角色型存取控制——要不要允許匿名拉取,正是這層存取控制裡的一個開關。 合理的推測是,自架 registry 時這個開關多半是關著的;一旦打開,通常是為了讓內部 CI 少一道認證手續,圖個方便。
這也呼應了 Strix 一開始就講的那句話:黑箱測試要跟拿到原始碼的測試一起做。單看原始碼, 很難注意到「這個 Harbor 專案設成公開」這種基礎設施層級的設定問題——它不在任何一份 程式碼 diff 裡;反過來,只做黑箱測試又抓不到 image 內部的 history 洩漏。兩種角度疊起來, 才拼出這條完整的攻擊鏈。
這個 Harbor 專案本身沒有把匿名存取關掉。不需要任何 token,Strix 就能列出 repository、 拿到匿名 pull token、把完整的 image manifest 跟 blob 下載下來。這一步還不算漏洞—— 只是讓接下來每一步都不需要密碼。
這類設定其實不算罕見。Harbor、其他 container registry,甚至雲端廠商代管的服務, 常常因為內部方便而把某個 project 設成公開可讀——圖的通常是讓內部 CI 少一道認證, 覺得反正不會有人特地去找。問題是子網域列舉跟憑證透明紀錄都是公開資訊,不需要對方主動 洩漏任何東西——光是把服務掛上網,就已經足夠被找到。
按按鈕切換掃描路徑:從掃過整排子網域收斂成只指向一個目標
拉下 image,在 history 裡挖出一個活著的 token
能匿名讀,agent 自然會挑一個 image 看看裡面有什麼。它選了 baseten/baseten-app,
拉下所有 layer 之後兵分兩路:對 layer 內容跑 TruffleHog 掃秘密字串,同時直接檢視 image
的 config。真正的發現落在後面這條路上——image config 裡的 history 陣列,
每一筆對應一個 build 步驟,key 叫 created_by,裡面躺著一段完整的 RUN 指令,
而這段指令裡有一個貨真價實的 GitHub personal access token。
TruffleHog 是一套秘密掃描工具,官方說法是它能在 Git、聊天紀錄、wiki、log、 API 測試平台、物件儲存、檔案系統等各種地方找秘密,辨識超過 800 種憑證格式, 而且對每一種能辨識出來的秘密,都能直接拿去對應的服務登入,確認它是不是還活著。 這次真正的發現落在另一條路徑上:不是這些內容本身,是 image 自己的 build 紀錄。
這個 token 怎麼跑到那裡的,原因很平常:某次 build 需要抓 private 依賴,工程師把 token
當成 build argument 傳進去,指令執行時 ${GITHUB_TOKEN} 展開成明文,
直接寫進那一句 RUN。問題出在 Docker 記錄 build 過程的方式——RUN 指令執行完,
只有它對檔案系統造成的差異會變成一層新的 layer;指令字串本身不是檔案系統的一部分,
但 Docker 仍然會把每一步完整的指令,連同展開後的引數,記進 image 的 metadata 跟 history 裡。
這次記下來的就是那個 token 的實際值。Docker 自己的文件其實明白警告過這件事,
只是警告不會自動變成 CI pipeline 裡的行為。
換句話說,防守方常常以為「最終 image 裡沒有那個檔案就安全」,但 history 從來就不是 檔案系統的一部分——它是隨 image 附帶、任何拉得到這個 image 的人都讀得到的建置紀錄。
Docker 官方文件對這件事有清楚的警告:「build argument 跟環境變數都不適合拿來傳秘密, 因為它們會留在最終 image 裡。」這句話講的其實比 Strix 這次踩到的還要早一步—— 就算工程師夠小心,讓 RUN 指令執行完之後不在檔案系統裡留下任何痕跡,秘密還是可能透過 history 這個大多數人不會想到要去檢查的管道留下來。
拖動分隔線比較同一個 RUN 指令的兩種紀錄方式
左右拖曳分隔線比較同一個 RUN 指令的兩種紀錄方式
同一個 RUN 指令:檔案系統看不到 token,image history 卻把完整指令(含明文 token)記進 metadata。
二十五分鐘後:驗證 token、測繪出三個 repo 的 admin
找到字串不等於找到可用的憑證,Strix 接下來做的是驗證。它用這個 token 打了一個唯讀的
GET /user 請求——回應 200,帳號名稱是 basetenbot。
回應標頭裡的 X-OAuth-Scopes 寫著 repo,帳號隸屬
basetenlabs 組織:這個 token 的授權範圍是整個 repo 層級,不是某種受限的細粒度權限。
GitHub 官方文件講得很白:token 對資源的存取能力,跟它擁有者本身的能力一樣,另外還會被
授予的 scope 進一步限縮——token 沒辦法讓人拿到超出擁有者帳號本身能力的存取權。
這也是為什麼一旦 basetenbot 這個帳號本身對某些 repo 有 admin,經由它簽出來
的 token 就一樣有 admin。從命名看,basetenbot 像是一個服務用的機器帳號,
通常掛在 CI 流程底下自動跑,這類帳號的權限範圍常常比一般開發者帳號設得更寬,
因為它要能代表整條 pipeline 去做事,而不是代表某一個人。
驗證這一步用的是 GitHub 標準 API 的唯讀行為:一個 GET /user對 GitHub REST API 打這支端點只需要一個有效 token,不需要任何寫入權限;回傳 200 就代表 token 還活著,順便帶出帳號名稱。 請求,配上回應標頭裡的 X-OAuth-ScopesGitHub 在每個 API 回應的標頭裡都會列出這個 token 實際擁有的 OAuth scope,不用另外呼叫端點就能讀到——這裡讀到的是 repo,代表整個 repo 層級的讀寫權限。 ,兩個都是唯讀查詢,不會觸發任何寫入動作。
確認 scope 之後,agent 再用一連串唯讀請求,逐一查每個 repo 的實際權限,測繪出這個 token
到底碰得到什麼——這一步全程沒有寫入任何東西,純粹是讀取權限資訊。結果是三個 repo 拿到
admin 加 push:basetenlabs/b***,是 Baseten 的 product 本體;
basetenlabs/f***,是他們的 GitOps repo;basetenlabs/h***,
是 CLI 分發用的 repo。作者的說法是 f***「可以說比 product repo 更可怕」——
GitOps repo 的內容通常直接對應正式環境該跑哪個版本,寫入權限大致等於部署權限,
這大概就是作者這樣講的理由。同一個 f*** 底下還有一個 customers/
目錄,一層一層按客戶名稱命名的子目錄排在裡面。除了這三個 admin 等級的 repo,
同一個 token 對其他私有 repo 也有 read/write。
admin 加 push 這個組合值得多說一句:一般來說,這代表除了改程式碼,還能調 branch protection 規則、直接推送到原本受保護的分支、甚至增減其他協作者的權限。三個 repo 裡有一個又剛好是控制正式環境部署的 GitOps repo,合理的推測是,這也是作者選擇把每個 repo 的用途跟權限層級分開寫清楚、而不是籠統帶過「有存取權」的原因。
basetenlabs/h*** 這個 CLI 分發 repo 也值得單獨拉出來看。文章形容它是
「Baseten 的 CLI 怎麼裝到開發者機器上」的機制——如果有寫入權限的人能改這個 repo 的內容,
合理的推測是,受影響的就不只是 Baseten 自己,而是所有安裝這支 CLI 的開發者,
這已經是供應鏈層級的風險,跟前兩個「打進 Baseten 自己系統」性質不太一樣。
customers/ 這個目錄結構也不只是多一個資料夾——子目錄按客戶命名,
代表這個 repo 承載的不只是 Baseten 自己的基礎設施設定,還有跟個別客戶綁定的資訊。
文章沒有進一步說明每個子目錄底下放的是什麼,但對一個多租戶的推論平台來說,這種目錄結構
一旦外洩,合理的推測是牽連通常不會只停在「這家公司的系統被看光」,而是可能波及用這個
平台的其他公司。
從偵察開始算,整個過程 agent 自主完成,沒有人在旁邊手動操作,耗時大約 25 分鐘—— 找到這種等級的存取權限,作者自己的形容是留在一個公開可下載的 image 裡,是「瘋狂的過度授權」。
從偵察到測繪權限,這整條攻擊鏈裡沒有一步用到寫入操作——沒有推送 commit、沒有改任何設定, 也沒有觸發那種通常會被稽核紀錄下來的動作型事件。這對防守方是個不舒服的事實: 傳統上很多監控靠的是「有沒有異常寫入」當訊號,但光是讀,就足夠把整個攻擊面跟權限範圍摸清楚。
| repo(原文已遮蔽) | 用途 | token 權限 |
|---|---|---|
basetenlabs/b*** | product 本體 | admin + push |
basetenlabs/f*** | GitOps(含 customers/ 目錄) | admin + push |
basetenlabs/h*** | CLI 分發 | admin + push |
| 其他私有 repo | —— | read / write |
通報、輪替、和三年空窗
image config 裡的 history 是有時間戳記的,留下 token 的那個 build 步驟執行於 2023 年 3 月 3 日。也就是說,這個活著的憑證,在一個公開可讀的 registry 裡, 靜靜躺了超過三年,直到 2026 年 7 月才被翻出來。
文章本身沒有解釋這三年空窗,合理的推測是,這個 Harbor 專案一直是公開狀態, 只是沒有人——不管是 Baseten 自己還是外部的攻擊者——系統性地掃過這個子網域; 主機列舉加憑證透明紀錄的組合,剛好是那種平常不會有人特地去跑、但一旦跑了就一次全部攤開的偵察方式。
三年多的空窗期本身就是這篇文章的另一半重點:漏洞存在的時間,跟漏洞被找到的時間, 是兩條完全不相關的時間軸。一個 2023 年就留下的問題,安不安全從來不取決於它躺了多久沒被 發現,而是取決於有沒有人(或有沒有 agent)決定去找。
這是 image history 這種洩漏管道特別麻煩的地方——它不會隨著時間自然過期,也不會因為 程式碼早就改版好幾次而跟著失效,一個 2023 年留下的 token,只要沒被明確撤銷, 到了 2026 年一樣是活的憑證。唯一能讓它失效的動作,是有人主動去輪替。
通報走的是標準的協調揭露節奏。7 月 13 日晚間 11 點 10 分,Strix 通報了三件事:
還活著的 basetenbot token、公開的 Harbor 專案,以及測繪出來的 repo 權限。
隔天早上,Baseten 把 Harbor 專案設回私有,但 token 本身還沒被撤銷——Strix 特地把這點標出來。
同一天下午 4 點 34 分,Baseten 資安團隊的 Anton 把這個問題定調為 critical,確認 Harbor
專案已經設為私有、token 也已經輪替。半小時後,Strix 確認 token 真的失效,順便把同一次掃描裡
另外兩個嚴重度較低的發現一起送過去。7 月 17 日,Baseten 把剩下的兩個發現也收尾。
一直到 9 月,Strix 才通知 Baseten 準備公開這個案例,並附上草稿。
從通報到修復,中間隔了不到一天;從修復到公開,中間隔了將近兩個月。這個節奏符合一般 協調揭露的慣例——先讓廠商有足夠時間確認、修復,再把細節公開,讓其他工程師照著同一份 案例去檢查自己的系統。整篇文章讀起來像是事後的技術報告,而不是即時的爆料, 也是因為時間軸本身就是照這個順序走的。
拖曳圖示看每個時間點的狀態 · 8 個事件,從 2023 年建置到 2026 年公開
文章給的建議很具體,三條都指向同一個方向——把發現漏洞的流程反過來,變成自己團隊平常就會做的檢查。
Docker 官方文件寫得很直接:「build argument 跟環境變數都不適合拿來傳秘密,因為它們會留在最終 image 裡。」建議是改用 secret mount——把秘密掛進建置容器,只在對應那一個 RUN 指令執行的當下才存在, 指令結束後就不會留下任何痕跡,更不會被記進 history。前提是消費那個秘密的指令本身也不能手動把它 寫進某個檔案、再留在 image 裡。
第二條建議是重新檢查 build token 實際擁有的權限。這次找到的 token scope 是整個 repo 層級, 但這次 build 只是要抓一個 private 依賴——抓依賴只需要對那個依賴的 read 權限,不需要對三個不同 repo 的 admin。範圍設得太寬,代表就算 token 沒有外洩,單一個被入侵的 build 環境也能波及遠超出 它原本任務範圍的東西。
第三條建議是工程師自己動手查——用 docker history --no-trunc 讀出完整的
build history,或者直接檢視 image config blob 裡的 history[].created_by
欄位。這個檢查成本很低,卻不是預設流程的一部分:多數 CI pipeline 產出 image 之後就直接推走,
沒有人回頭看過 history 裡到底記了什麼。
作者在文章最後把這件事講得很白:AI 攻擊者可以走一模一樣的路徑。這句話值得放在整篇的脈絡裡看—— 這次從頭到尾沒有用到任何未公開的漏洞,每一步單獨拿出來都是資安教材等級的基本功(列舉子網域、 檢查匿名存取、掃 build history、驗證 token scope),組合起來卻是一次完整的權限升級。 能把這套基本功用 25 分鐘跑完、而且全自動,才是這篇文章真正的重點所在。
文章最後一條建議是把自主資安測試工具拿來對自己的基礎設施跑——邏輯很直接: 這類 agent 能做的事,能力接近的攻擊者遲早也做得到;與其等外部研究者(或惡意的一方) 先找到,不如自己先跑一遍,把找到的洞在 25 分鐘之外、而不是被通報那一刻,就先補起來。
現在能做的事:找一個正式環境在用的 image,跑一次
docker history --no-trunc——如果看到熟悉的 token 前綴,記住這次的數字:
一個自主 agent,從一個被忘記的子網域開始,25 分鐘就能走到 admin。