被竊的權杖不需要伺服器
Wiz Research 追蹤了 Gheist:一場自動化攻擊活動,從資訊竊取程式的日誌和過去的供應鏈外洩中取得 GitHub 權杖,再在每個權杖能觸及的每個儲存庫裡植入一個傾印機密的工作流程,至今已有來自 850 多個帳號的 13,500 多個分支。機密經由 GitHub 自己的 API 流出,所以沒有任何攻擊者伺服器可以封鎖。在 Bromure Agentic Coding 中,竊取程式在開發者機器上找到的 GitHub 權杖是一個替身,從其他任何地方使用都會被 GitHub 拒絕,而機器內發出的每一次寫入都要等你在 Mac 上的對話框點頭。
Gheist 沒有指揮伺服器。執行酬載的是 GitHub,把機密寫進日誌的是 GitHub,把日誌交給任何持有權杖的人的,也是 GitHub。
10 月 25 日,Wiz Research 發表了對 Gheist 的分析,這是它從 9 月 15 日起一直追蹤的攻擊活動。一個自動化工具拿到被竊的 GitHub 權杖後,列出該權杖能觸及的每個儲存庫,並植入一個會印出儲存庫機密的工作流程。Wiz 已從 850 多個遭入侵的帳號中找回 13,500 多個被植入的分支;截至 10 月 24 日,這場攻擊仍在進行,也仍在變化。The Hacker News 昨天報導了此事。
這個工具只需要一種輸入,而 Wiz 說明了它的來源:這些權杖與資訊竊取程式的日誌,以及過去供應鏈入侵中外洩的權杖相符,例如 3 月 Trivy 掃描器遭入侵的事件。Wiz 給每位受害者的建議中有這麼一行:檢查權杖擁有者的裝置上是否有資訊竊取程式。
Gheist 如何運作
對每個儲存庫,工具會建立一個名為 ci-sync- 加上六個隨機字元的分支,並以 ci: sync config 為訊息提交唯一一個檔案 .github/workflows/sync-check.yml。這次提交沒有簽章,作者記為權杖的擁有者。它的 API 請求帶著 gheist 這個 user agent,攻擊活動也因此得名。
推送會啟動工作流程。工作會把環境變數和整個 secrets 內容印進執行日誌,兩者都以 base32 編碼。Wiz 解釋了為什麼這招有效:「GitHub 只有在機密值原封不動出現時,才會在日誌中遮蔽它們。」base32 的輸出與儲存的值不符,所以什麼都沒被遮蔽。一個涵蓋儲存庫各部署環境的矩陣,會把環境層級的機密也一併拉出來,而那往往就是正式環境的機密。
接著,工具透過 GitHub API 下載日誌,並在幾秒後刪除那次執行。用 Wiz 的話說:「因為機密是經由 GitHub 自己的 API 流出,所以沒有攻擊者網域或對外連線可以封鎖。」
這個工具一直在改進。有個變種在 9 月 28 日首次執行後的幾小時內,就加上了六分鐘的逾時、環境矩陣,以及針對每個儲存庫的酬載調整。10 月 19 日,它多了一個瞄準自架 runner 的工作,搜尋 31 種權杖、雲端執行個體的憑證,以及 Kubernetes 服務帳號的權杖。
GitHub 新規則的盡頭
自 7 月 28 日起,GitHub 會把公開儲存庫中被判定為惡意的工作流程執行擋下,直到協作者核准為止。Wiz 檢查過的每一次公開 Gheist 執行,都卡在 action_required。「這項控制有效,惡意酬載從未執行。」
這項攔截只涵蓋 github.com 上的公開儲存庫。在私有儲存庫和 GitHub Enterprise Server 上,Wiz 找到的稽核日誌顯示 ci-sync-* 執行完成後隨即被刪除,CloudTrail 紀錄也顯示組織的工作流程從這些分支取得了 AWS 角色。
權杖來自開發者的機器
每個 Gheist 分支都需要一個有效的權杖,而 Wiz 把這些權杖追溯到竊取程式日誌和外洩的建置工具。在開發者的筆電上,gh CLI 的登入、~/.git-credentials 裡的個人存取權杖、shell 設定檔裡的 GH_TOKEN:資訊竊取程式一次就能全部複製走。程式設計代理讓這類權杖更加普遍,因為一個會推送分支、開 pull request 的代理,隨時都需要一個權杖在手邊。
竊取也可能來自你信任的工具。10 月 19 日,一名掌控某位 OVHcloud 開發者 GitHub 帳號數月之久的攻擊者,把 OVHcloud CLI 五個版本(v0.11.0 到 v0.15.0)的執行檔換成了另一批建置,用 OVHcloud 執行長 Octave Klaba 的話說,這些建置會「從磁碟取出機密」。BleepingComputer 報導,macOS、Linux 和 Windows 的建置全都受到影響。Klaba 的建議是:「如果你受到影響,請更換磁碟上所有的機密。」
被竊的權杖不必在同一天就落到 Gheist 這類攻擊活動手中。竊取程式的日誌會被轉賣,而權杖在有人撤銷之前一直都能用。
同一個權杖放在 Bromure 機器上
Bromure Agentic Coding 在你的 Mac 上以 Ubuntu VM 執行 Claude Code、Codex、Grok、Kimi 和 omp。真正的憑證留在主機上,代理使用的是替身。在其中一台機器上,沿著 Gheist 的攻擊鏈追蹤一個 GitHub 權杖。
磁碟上的權杖是替身
在機器的 Credentials 中加入你的 GitHub 權杖。開機時,Bromure 會在 VM 裡寫入一個假的:ghp_ 加上 36 個字元,正是 gh 預期的格式,放在 GH_TOKEN、gh 的設定和 ~/.git-credentials 裡。代理的 git push 和 gh pr create 照常運作。當請求離開 VM 前往 github.com 時,Mac 上的代理伺服器會把假的換成真正的權杖,而且只針對那個主機。
現在,在那台 VM 裡執行被植入木馬的 CLI,就像代理照著某個專案的安裝說明會做的那樣。它讀取磁碟,而上面每一個 GitHub 值都是替身。開發者身上其他常見的戰利品也一樣。~/.aws/config 指向一個會交出假私密金鑰的輔助程式,代理伺服器在 Mac 上用真正的金鑰重新簽署 AWS 請求。VM 裡沒有任何 SSH 私鑰可讀:SSH_AUTH_SOCK 通往主機上的一個 agent,它替 VM 簽章,但從不交出金鑰。
Gheist 用不了替身
Gheist 工具在攻擊者自己的基礎設施上執行。它拿買來的權杖呼叫 api.github.com,期待看到一串儲存庫。換成 Bromure 的替身,GitHub 回應 401 Bad credentials。Bromure 以一個只留在你 Mac 上的 salt,從真正的權杖衍生出替身,只有那台 Mac 的代理伺服器知道它代表哪個真正的權杖。工具連一個儲存庫都列不出來,自然也不會建立任何分支。
Gheist 的整套手法,就是問一個權杖能觸及什麼。離開被寫入的那台 VM,Bromure 的替身什麼都觸及不了。
送往竊取程式的替身會觸發警報
竊取程式還得把找到的東西送到某個地方,而從 Bromure VM 發出的每條 HTTPS 連線都會經過 Mac 上的代理伺服器。代理伺服器會掃描每個對外請求的標頭和內文,找出被送往非原本簽發主機的替身。一個要送往 GitHub 以外任何地方的 ghp_ 替身,在任何一個位元組離開 Mac 之前,就會被 HTTP 451 擋下。VM 隨即暫停,並跳出 This environment may have been compromised 的警示,列出憑證名稱、它所屬的主機,以及它被送往的主機。
你可以關閉這台機器,或保存它的磁碟和家目錄以供調查。被標記為遭入侵的機器在磁碟和家目錄清除之前不會再開機,所以竊取程式活不過重新啟動。
在機器的 Guardrails 面板把 Unmatched traffic 設為 Deny,再允許專案需要的主機,竊取程式的上傳主機就會直接被拒絕,不管傳的是不是加密封存檔。被拒絕的連線會成為 Security Timeline 中的一列。
機器內的寫入會等你
假設惡意程式放棄竊取,改在替身能用的地方使用它:從 VM 內部,經過代理伺服器。它就得做 Gheist 做的事:建立分支,提交工作流程。
新的 Bromure 機器會把 GitHub 的寫入政策設為 Prompt before write。讀取照常通過。git push 或對 github.com 的 REST 寫入,會停在你 Mac 上的一個對話框,原封不動地顯示這項操作:
Allow write on "GitHub" from workspace "web-app"?
POST /repos/<owner>/<repo>/git/refs
你正在看 pull request 時冒出一個你沒要求的 ci-sync- 分支,要拒絕很容易。Don't allow 回給代理的錯誤和封鎖政策相同,而 Allow once 除了那一次寫入,什麼都不授權。
輪替只在一個地方進行
Wiz 的第一個應變步驟,是輪替受影響儲存庫能觸及的每個機密,並撤銷推送那些提交的權杖。「刪除執行紀錄或分支,並不能抵銷已經發生的外洩。」
在裝有 Bromure 的 Mac 上,這次輪替中屬於開發者的部分只是一個欄位。真正的 GitHub 權杖存放在主機上單一一筆加密項目裡。把新的權杖貼進機器的 Credentials 面板,下次啟動就會寫入對應的替身;不必在 shell 設定檔、.env 檔或代理設定裡四處搜尋副本。對於以 1Password 參照方式儲存的金鑰(Other API keys 下的 op://vault/item/field),Bromure 每兩分鐘重新解析一次,所以在 1Password 裡的輪替不必重新開機就會送達執行中的機器。
保險庫本身由 macOS Data Protection Keychain 中的一把金鑰保護,這把金鑰綁定 Bromure 的簽章身分,且從不同步。把 Bromure 資料夾從 Mac 複製走的竊取程式,拿到的只是密文。
讓代理去搜查,唯讀
Wiz 的搜查步驟是橫跨大量儲存庫的繁瑣工作,正好適合交給代理:列出所有符合 ^ci-sync-[0-9a-z]{6}$ 的分支,找出只新增 sync-check.yml、訊息為 ci: sync config 的提交,再從稽核日誌中撈出完成後不到一分鐘就被刪除的執行,以及 user agent 為 gheist 的請求。
把這份工作交給一台 GitHub 寫入政策設為 Read-only 的 Bromure 機器。代理可以讀取權杖允許的每個儲存庫和稽核日誌,而任何推送或 REST 寫入都會在抵達 GitHub 之前,就在 Mac 上被拒絕。如果代理為了查看而簽出一個 ci-sync- 分支,包括 Wiz 看到被重新推送、夾帶 PolinRider 載入器的那些分支,執行起來的任何東西都是在一台滿是替身的 VM 裡執行。每一次憑證替換都會在 Security Timeline 中成為一列 Credential brokering,事後你就能看出這次搜查碰過哪些主機。
竊取程式找到的是替身
GH_TOKEN、gh 的設定和 ~/.git-credentials 裡放的是假的 ghp_。真正的權杖加密留在 Mac 上。
Gheist 拿到 401
離開這台機器,替身就不是 GitHub 權杖。工具列不出任何儲存庫,所以什麼也植入不了。
外洩會暫停 VM
要送往 GitHub 以外任何地方的替身會被 451 擋下,VM 會一直暫停,直到你決定如何處置。
寫入先問過你
新機器在每次推送和每次對 GitHub 的 REST 寫入之前都會先詢問,畫面上顯示確切的請求。搜查可以用唯讀方式執行。
把權杖從筆電上拿走
GitHub 的核准攔截在公開儲存庫上發揮了作用,而 Wiz 的強化清單也是對的:正式環境必須有審查者、OIDC 信任限定在受保護的分支、以 GitHub Apps 或短效的細粒度權杖取代個人存取權杖。這些都能縮小被竊權杖落入他人手中後所能做的事。
Bromure Agentic Coding 則在更早一步發揮作用。你的代理整天推送分支、開 pull request,而它們使用的權杖從不放在竊取程式讀得到的磁碟上。Gheist 只能用仍然有效的權杖植入工作流程,而 Bromure 機器上的權杖,只有經過那台機器自己的代理伺服器才有效。
想知道同樣的替身如何抵擋一個專為程式設計代理檔案打造的竊取程式,請讀〈竊取程式打包走的是替身〉。想看一個被竊的 GitHub 權杖如何導致一次被汙染的發布,請讀〈代理的權杖是組織擁有者的〉。