← すべての投稿に戻る
公開日 · 著者 Renaud Deraison

盗まれたトークンにサーバーは要らなかった

Wiz Research が追跡した Gheist は、インフォスティーラーのログや過去のサプライチェーン漏洩から GitHub トークンを取り出し、各トークンが届くすべてのリポジトリに、シークレットを吐き出すワークフローを仕込む自動化キャンペーンだ。850 を超えるアカウントから 13,500 を超えるブランチが作られた。シークレットは GitHub 自身の API から出ていくので、ブロックすべき攻撃者のサーバーは存在しない。Bromure Agentic Coding では、スティーラーが開発者マシンで見つける GitHub トークンは、ほかのどこから使っても GitHub に拒否される代用値であり、マシン内からの書き込みはすべて Mac 上のダイアログを待つ。

Gheist にはコマンドサーバーがない。ペイロードを実行するのも、シークレットをログに書くのも、そのログをトークンの持ち主に渡すのも、すべて GitHub だ。

10 月 25 日、Wiz Research は 9 月 15 日から追跡してきたキャンペーン Gheist の分析を公開した。自動化ツールが盗まれた GitHub トークンを受け取り、そのトークンが届くリポジトリをすべて列挙し、リポジトリのシークレットを出力するワークフローを仕込む。Wiz は 850 を超える侵害アカウントから仕込まれた 13,500 を超えるブランチを回収しており、10 月 24 日の時点でもキャンペーンは続き、変化し続けていた。昨日は The Hacker News も報じている。

このツールに必要な入力は一つだけで、Wiz はその出どころも示している。トークンは、インフォスティーラーのログや、3 月の Trivy スキャナー侵害のような過去のサプライチェーン侵害で露出したトークンと一致した。Wiz がすべての被害者に向けた助言には、トークン所有者の端末にインフォスティーラーがいないか調べよ、という一文が入っている。

Gheist の仕組み

ツールはリポジトリごとに、ci-sync- にランダムな 6 文字を付けた名前のブランチを作り、ファイルを一つだけ、.github/workflows/sync-check.yml を ci: sync config というメッセージでコミットする。コミットには署名がなく、トークンの所有者の名前で記録される。API リクエストのユーザーエージェントは gheist で、これがキャンペーン名の由来になった。

push がワークフローを起動する。ジョブは環境変数と secrets コンテキスト全体を、どちらも base32 でエンコードして実行ログに出力する。Wiz はこれが通る理由をこう説明する。「GitHub がログ内のシークレット値をマスクするのは、それがそのままの形で現れたときだけだ」。base32 の出力は保存された値と一致しないので、何もマスクされない。リポジトリのデプロイ環境に対するマトリックスによって、環境スコープのシークレットまで引き出される。それは多くの場合、本番用のものだ。

その後ツールは GitHub API 経由でログをダウンロードし、数秒後に実行記録を削除する。Wiz の言葉では、「シークレットは GitHub 自身の API を通って出ていくので、ブロックすべき攻撃者のドメインも外向きの接続も存在しない」。

ツールは改良を重ねた。ある亜種は、9 月 28 日の最初の実行から数時間のうちに、6 分のタイムアウト、環境マトリックス、リポジトリごとのペイロード変更を加えた。10 月 19 日には、セルフホストランナーを狙うジョブが加わり、31 種類のトークン、クラウドインスタンスの認証情報、Kubernetes のサービスアカウントトークンを探すようになった。

盗まれたトークンからリポジトリのシークレットまで1 · 盗まれたトークンスティーラーのログやサプライチェーン漏洩から850 超のアカウント2 · リポジトリごとにci-sync-xxxxxxsync-check.yml を追加13,500 超のブランチ3 · ログにシークレットtoJSON(secrets) | base32エンコードされた値はマスクされない4 · 取得して削除GitHub API でログを取得、数秒後に実行を削除。攻撃者のホストなし公開リポジトリ · GitHub が保留7 月 28 日以降、判定された実行は承認待ち。Wiz が確認した公開側の Gheist の実行はすべて action_requiredプライベートと GHES · 実行された監査ログに ci-sync-* の実行完了と削除の記録。CloudTrail には、それらのブランチからの AWS ロール引き受け
Wiz が説明する Gheist の連鎖。インフォスティーラーのログや過去のサプライチェーン漏洩から得たトークンで、自動化ツールがトークンの届くすべてのリポジトリに、ワークフローファイル一つだけの ci-sync- ブランチを push する。ワークフローは GitHub のログマスクをすり抜けて全シークレットを base32 で出力し、ツールは GitHub の API でログをダウンロードして実行記録を削除する。どの段階にも攻撃者のサーバーは関わらない。

GitHub の新ルールが止まる場所

GitHub は 7 月 28 日以降、公開リポジトリで悪意があると判定したワークフロー実行を、コラボレーターが承認するまで保留している。Wiz が確認した公開リポジトリ上の Gheist の実行は、すべて action_required で止まっていた。「この制御は機能し、悪意あるペイロードは一度も実行されなかった」。

この保留が効くのは github.com の公開リポジトリだけだ。プライベートリポジトリと GitHub Enterprise Server では、ci-sync-* の実行が完了してから削除されたことを示す監査ログと、組織のワークフローがそれらのブランチから AWS ロールを引き受けた CloudTrail の記録を Wiz は見つけた。

トークンは開発者のマシンから来る

Gheist のブランチにはどれも有効なトークンが要る。Wiz はそれらのトークンを、スティーラーのログと漏洩したビルドツールまでたどった。開発者のノート PC には、gh CLI のログイン、~/.git-credentials の個人アクセストークン、シェルプロファイルの GH_TOKEN がある。インフォスティーラーはそれらを一度にまとめてコピーする。コーディングエージェントの普及で、こうしたトークンはさらにありふれたものになった。ブランチを push し、プルリクエストを開くエージェントには、常に手の届くところにトークンが必要だからだ。

盗みは信頼しているツールから来ることもある。10 月 19 日、OVHcloud の開発者の GitHub アカウントを何か月も握っていた攻撃者が、OVHcloud CLI の 5 つのリリース、v0.11.0 から v0.15.0 までのバイナリを、OVHcloud CEO の Octave Klaba の言葉で言えば「ディスクからシークレットを取り出す」ビルドに差し替えた。BleepingComputer の報道によれば、macOS、Linux、Windows のビルドがすべて影響を受けた。Klaba の助言はこうだ。「影響を受けた場合は、ディスク上のシークレットをすべて変更してください」。

盗まれたトークンが、その日のうちに Gheist のようなキャンペーンに届く必要はない。スティーラーのログは売買され、トークンは誰かが失効させるまで使える。

同じトークンを Bromure マシンで

Bromure Agentic Coding は、Claude Code、Codex、Grok、Kimi、omp を Mac 上の Ubuntu VM で動かす。本物の認証情報はホストに残り、エージェントは代用値で作業する。そうしたマシンの一つで、GitHub トークンが Gheist の連鎖をどうたどるかを追ってみよう。

ディスク上のトークンは代用値

GitHub トークンをマシンの Credentials に追加する。起動時、Bromure は VM に偽物を書き込む。gh が期待する形の ghp_ と 36 文字で、GH_TOKEN、gh の設定、~/.git-credentials に入る。エージェントの git push や gh pr create はこれまでどおり動く。リクエストが VM から github.com へ出ていくとき、Mac 上のプロキシが偽物を本物のトークンに差し替える。差し替えるのはそのホスト宛てだけだ。

では、トロイの木馬化された CLI をその VM で実行してみる。エージェントがプロジェクトのインストール手順に従ったときと同じように。CLI はディスクを読むが、そこにある GitHub の値はすべて代用値だ。開発者から普段奪われるほかの戦利品も同じだ。~/.aws/config は偽のシークレットキーを渡すヘルパーを指しており、プロキシが Mac 上で本物のキーを使って AWS リクエストに署名し直す。VM には読み取れる SSH 秘密鍵がない。SSH_AUTH_SOCK の先はホスト上のエージェントで、VM の代わりに署名するが、鍵そのものは決して渡さない。

Gheist は代用値を使えない

Gheist のツールは攻撃者自身のインフラで動く。買ったトークンで api.github.com を呼び、リポジトリが見えることを期待する。Bromure の代用値なら、GitHub は 401 Bad credentials を返す。Bromure は Mac から出ないソルトを使って本物のトークンから代用値を導出しており、それがどの本物のトークンを表すかを知っているのは、その Mac のプロキシだけだ。ツールはリポジトリを一つも列挙できず、ブランチを作ることもない。

Gheist の手口はすべて、トークンが何に届くかを問うことにある。書き込まれた VM の外では、Bromure の代用値はどこにも届かない。

スティーラーへ向かう代用値は警報を鳴らす

スティーラーは見つけたものをどこかへ送らなければならない。Bromure の VM からは、HTTPS 接続はすべて Mac 上のプロキシを通る。プロキシは外向きのリクエストをヘッダーも本文もすべて調べ、発行先ではないホストへ向かう代用値を探す。GitHub 以外へ向かう ghp_ の代用値は、1 バイトも 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

プルリクエストを見ている最中に現れた、頼んだ覚えのない ci-sync- ブランチなら、拒否するのは簡単だ。Don't allow は、ブロックするポリシーと同じエラーをエージェントに返す。Allow once が許すのは、その 1 回の書き込みだけだ。

ノート PC:ディスクに有効なトークンスティーラーがディスクを読むgh ログイン、~/.git-credentials、GH_TOKEN:本物スティーラーのログから gheist ツールへインターネット上のどのマシンからでも動くapi.github.com が受け入れる届くすべてのリポジトリに ci-sync- ブランチ。シークレットは GitHub 自身の API から出ていくBromure:VM には代用値エージェントの VM(Ubuntu)gh 設定、~/.git-credentials、GH_TOKEN = ghp_ 代用値Mac 上のホストプロキシ本物のトークンは github.com 宛てだけ。他所への代用値は 451、VM 停止。書き込みは確認攻撃者のインフラから代用値は GitHub トークンではない。api.github.com が拒否し、何も仕込めない
ノート PC と Bromure マシンでの Gheist。ノート PC では、スティーラーがディスクから有効な GitHub トークンをコピーし、gheist ツールがそれをどこからでも使う。Bromure マシンでは、VM が持つのは ghp_ の代用値だ。攻撃者のインフラから使えば GitHub が拒否し、スティーラーのホストへ向かえばプロキシがブロックして VM を一時停止し、VM 内からの書き込みはすべて Mac 上のダイアログを待つ。

ローテーションは一か所で済む

Wiz が挙げる最初の対応手順は、影響を受けたリポジトリが届きえたシークレットをすべてローテーションし、コミットを push したトークンを失効させることだ。「実行記録やブランチを削除しても、露出はなかったことにならない」。

Bromure の Mac では、このローテーションのうち開発者側の作業は入力欄一つで済む。本物の GitHub トークンは、ホスト上の暗号化された一つのエントリにある。新しいトークンをマシンの Credentials ペインに貼り付ければ、次の起動で対応する代用値が書き込まれる。シェルプロファイルや .env ファイル、エージェントの設定にあるコピーを探し回る必要はない。1Password の参照(Other API keys の下の op://vault/item/field)として保存したキーなら、Bromure が 2 分ごとに解決し直すので、1Password でのローテーションは再起動なしで実行中のマシンに届く。

保管庫そのものは、macOS の Data Protection Keychain にある鍵で守られている。この鍵は Bromure の署名 ID に紐づき、同期されることはない。Bromure のフォルダを Mac からコピーしたスティーラーが得るのは暗号文だけだ。

調査はエージェントに、読み取り専用で

Wiz の調査手順は、多数のリポジトリにまたがる地道な作業で、だからこそエージェント向きだ。^ci-sync-[0-9a-z]{6}$ に一致するブランチをすべて列挙し、ci: sync config というメッセージで sync-check.yml だけを追加しているコミットを見つけ、完了から 1 分以内に削除された実行と、ユーザーエージェント gheist を監査ログから抜き出す。

この仕事は、GitHub の書き込みポリシーを Read-only にした Bromure マシンに任せよう。エージェントはトークンが許すすべてのリポジトリと監査ログを読めるが、push や REST の書き込みは GitHub に届く前に Mac 上で拒否される。エージェントが中身を確かめるために ci-sync- ブランチをチェックアウトしても、Wiz が PolinRider のローダー付きで再 push されたのを確認したブランチであっても、そこで動くものはすべて代用値だらけの VM の中で動く。認証情報の差し替えはすべて Security Timeline に Credential brokering の行として残るので、調査がどのホストに触れたかを後から確認できる。

スティーラーが見つけるのは代用値

GH_TOKEN、gh の設定、~/.git-credentials にあるのは ghp_ の偽物だ。本物のトークンは Mac 上で暗号化されたまま残る。

Gheist には 401

マシンの外では、代用値は GitHub トークンではない。ツールはリポジトリを列挙できず、何も仕込めない。

漏洩は VM を止める

GitHub 以外へ向かう代用値は 451 でブロックされ、VM はあなたが扱いを決めるまで一時停止する。

書き込みは先に確認

新しいマシンは、GitHub への push と REST の書き込みのたびに、正確なリクエストを画面に出して確認する。調査は読み取り専用で走らせられる。

トークンをノート PC から外す

GitHub の承認保留は公開リポジトリでは機能したし、Wiz の堅牢化リストも正しい。本番環境での必須レビュアー、保護されたブランチに限定した OIDC の信頼、個人アクセストークンの代わりに GitHub Apps や短命のきめ細かいトークンを使うこと。どれも、誰かの手に渡った盗難トークンにできることを狭める。

Bromure Agentic Coding はその一歩手前で働く。エージェントは一日中ブランチを push し、プルリクエストを開くが、使っているトークンがスティーラーに読めるディスクに置かれることはない。Gheist がワークフローを仕込めるのはまだ有効なトークンだけで、Bromure マシン上のトークンが機能するのは、そのマシン自身のプロキシを通したときだけだ。

コーディングエージェントのファイルを狙って作られたスティーラーに対して同じ代用値がどう持ちこたえたかは「スティーラーは代用値を持ち去った」を、盗まれた GitHub トークン一つが汚染されたリリースに行き着いた例は「エージェントのトークンはオーナーのものだった」を読んでほしい。