1万3,500本を超える不審なブランチが、850を超えるアカウントのリポジトリで見つかりました。どれも、盗まれたGitHubのトークンで作られたものです。クラウドセキュリティ企業Wizの調査チーム(Wiz Research)は10月25日(米国時間)、この攻撃を「Gheist」と名付けて報告しました。攻撃のツールがGitHubのAPIに送るUser-Agentが「gheist」で、名前はそこから取っています。
Gheistは、トークンで書き込めるリポジトリすべてにワークフローを1つ足し、そのリポジトリのシークレットを実行ログに書き出させます。GitHubが7月に始めた保護機能で、公開リポジトリでは止まりました。一方、非公開リポジトリとGitHub Enterprise Server(GHES)ではワークフローが実行され、その後のクラウドへの侵入にもつながっていました。Wizによると、10月24日の時点でも攻撃は続き、手口も変わり続けています。
1つのファイルで、シークレットを実行ログに書き出させる
ツールは、リポジトリごとに「ci-sync-」と6文字のランダムな文字を続けたブランチを作り、.github/workflows/sync-check.ymlを1つだけコミットします。コミットの文言は「ci: sync config」で、署名はなく、作成者はトークンの持ち主になっています。このpushをきっかけに、ワークフローが動きます。
ワークフローは、ランナーの環境変数と、リポジトリのシークレットをまとめたもの(toJSON(secrets))を、base32という形式に変えて実行ログに出力します。Wizは、この手口が通る理由を3つ挙げています。
- GitHubがログの中のシークレットを伏せ字にするのは、値がそのまま出たときだけです。base32に変えた値は一致しないため、そのまま出ます。
- ワークフローに「permissions: {}」と書くと、ジョブが使うGITHUB_TOKENの権限は絞られますが、シークレットは読めます。攻撃者はGITHUB_TOKENを必要としていません。
- リポジトリにデプロイ用の環境(environment)があると、ツールは環境ごとにジョブを並べます。本番の認証情報を置くことが多い、環境のシークレットまで出力させます。
ツールはGitHubのAPIで実行ログを取得し、数秒後に実行の記録を削除します。シークレットはGitHub自身のAPIを通って持ち出されるため、ブロックできる攻撃者のドメインも、外向きの通信もありません。
企業の組織では、ツールが数分のうちに複数の利用者や自動化用アカウントのトークンを切り替え、非公開リポジトリを複製してからワークフローを置いていました。数時間後には、同じ会社のクラウドの認証情報で、AWS Secrets Managerの中身が列挙されていたといいます。
Gheistの攻撃の流れと、公開・非公開で分かれた結果、探す目印作成: 編集部 / 出典: Wiz Research、GitHub Changelog
9月から10月にかけて、手口が変わった
Wizは、並行して動く2つの版を見つけています。見分け方はブランチ名の末尾で、Tool Aは16進数、Tool Bは英小文字と数字(36進数)です。Tool Bは、9月28日に初めて動いてから数時間のうちに、6分で打ち切る設定と、環境ごとにジョブを並べる処理を足しました。
10月19日には、Tool Bがもう1つのジョブ「sync-host」を加えました。実行先はセルフホストランナー(自社で用意したActionsの実行用サーバー)で、ランナーを登録しているリポジトリだけをねらったとみられます。このジョブは、.gdmp_pat・.gdmp_patg・.gdmp_sweepの3つのファイルを書き、次のものを集めようとします。
- サーバーの情報と、crontabの内容
- 31種類の形式のトークン、秘密鍵、.envファイル、Terraformのstateファイル、シェルの履歴。ホームディレクトリなどを順にたどって探し、AIサービスではAnthropic、Hugging Face、Groq、NVIDIAなどのAPIキーも対象です
- すべてのプロセスの環境変数
- Dockerのソケットを使って、ホストのファイルシステムをマウントしたコンテナを作り、ホストでも同じ検索
- AWSのインスタンスロールの認証情報と、GCP・Azureのメタデータ
- GitHub ActionsとGitLabのランナーの認証情報のファイル、Kubernetesのサービスアカウントのトークン
もっとも、このジョブには不具合があります。Wizによると、ランナーの鍵のファイルを取り違えている、URLに含まれる認証情報を拾う正規表現が壊れている、GCPとAzureのトークンを取れていない、の3点です。
公開リポジトリでは止まり、非公開リポジトリでは動いた
GitHubは7月28日から、公開リポジトリで悪意がありそうなワークフローの実行を保留し、書き込み権限のある共同作業者の承認を待つようにしました。承認は、ログインしたブラウザーからだけ受け付けます。Wizが確かめた公開リポジトリのGheistの実行は、セルフホストランナーのジョブも含めて、すべて承認待ち(action_required)のままでした。保護機能が働き、悪意のある処理は1度も動いていません。
ただし、GitHubの発表のとおり、この保留はgithub.comの公開リポジトリだけが対象で、GHESにはありません。Wizは非公開リポジトリとGHESで、ci-sync-のブランチの実行が完了し、User-Agent「gheist」の操作で削除された記録を、GitHubの監査ログで確認しています。AWSの操作記録(CloudTrail)にも、そのブランチで会社自身のワークフローが動き、AWSのロールを引き受けた記録がありました。承認なしにActionsが動いた証拠です。
同じアカウントを、ほかの攻撃者も使っている
Wizは、Gheistに使われたアカウントが、ほかの攻撃でも使われているとして、次の重なりを挙げています。
- 10月6日、391のアカウントが約1,461のリポジトリを、fl-studio-crackのような海賊版ソフトを装う名前に変えました。マルウェアを配るためのおとりです。このうち166のアカウントはGheistの記録にないため、盗まれたアカウントはGheistで見えるより多いとWizはみています。
- 10月18日、25のアカウントが9分のうちに「Windows RDP Runner」というリポジトリを作り、GitHub Actionsを無料の計算資源として使おうとしました。暗号資産の採掘が目的とみられます。
- 10月19〜20日、3つの被害者のアカウントで、Gheistのコミットにローダーを足したものが送り直されました。vite.config.jsの大量の空白の後ろにコードを隠す、北朝鮮との関連が指摘されるマルウェア「PolinRider」の手口です。
- Gheistの初期の57アカウントは、5月の「Megalodon」の被害も受けていました。
トークンの出どころとしてWizがたどれたのは、インフォスティーラー(情報を盗むマルウェア)の記録と、3月のTrivyの侵害など過去のサプライチェーン攻撃で漏れたトークンです。10月に取り上げたGhostActionとは使うファイルも送り先も違い、Wizは別の攻撃としています。GhostActionは、シークレットを外部のサーバー(193.32.204[.]199)に送っていました。
影響を受けるのは、トークンが盗まれた会社
- 社員や自動化用アカウントのトークン(個人用アクセストークンなど)が盗まれていれば、そのトークンで書き込めるリポジトリすべてが対象です。社員の私物のPCからトークンが漏れても、会社のリポジトリがねらわれます。
- 非公開リポジトリとGHESは、GitHubの承認の保留がないため、ワークフローがそのまま動きます。リポジトリ・組織・環境のシークレットが漏れる恐れがあります。
- GitHub ActionsからOIDCでクラウドに入る設定で、信頼ポリシーをブランチや環境で絞っていない会社は、攻撃者のブランチで動いたワークフローにもクラウドのロールを渡してしまいます。Wizが見たCloudTrailの記録が、その例です。
- セルフホストランナーを使い、Dockerのソケットやクラウドのメタデータに届く作りにしている会社は、ランナーのサーバーとそのインスタンスロールまで影響を受けます。
組織でGitHub Actionsを無効にしている会社では、ワークフローは動きません。編集部の見立てでは、それでもトークンが盗まれていればリポジトリの中身は読まれるため、トークンの点検は別に必要です。
ブランチ・監査ログ・クラウドの記録で探す手順
- 各リポジトリで、「ci-sync-」に英小文字か数字の6文字が続くブランチを探します。変更が.github/workflows/sync-check.ymlの追加だけで、コミットの文言が「ci: sync config」なら、Gheistの目印と一致します。リポジトリが多いときは、GitHub CLIで組織のリポジトリを順に調べられます(下の例)。
- GitHubの監査ログで、ci-sync-*のブランチでの実行のうち、完了から1分以内に削除されたものと、User-Agent「gheist」の操作を探します。
- GitHub ActionsからOIDCでクラウドに入っているなら、クラウドの監査ログで、ロールの引き受けやトークンの交換のうち、subjectにci-sync-のブランチが入っているものを探します。AWSならCloudTrailのAssumeRoleWithWebIdentityの記録です。GitHubのログを集めていなくても、攻撃者のブランチでActionsが動いたかが分かります。
- セルフホストランナーでは、.gdmp_pat・.gdmp_patg・.gdmp_sweepのファイルと、ジョブのログの「gdmp-pivot」を探します。
- 承認待ちになっているsync-checkの実行は承認せず、ci-syncのブランチのコードも実行しません。
Wizによると、10月6日にリポジトリの名前が変えられた際、610本のci-syncのブランチが一緒に消えました。ブランチが見つからなくても、監査ログとクラウドの記録は確かめておきます。
組織のリポジトリを順に調べる例(GitHub CLIでログインし、組織のリポジトリを読める権限が必要です):
gh repo list 組織名 --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' |
while read -r repo; do
gh api "repos/$repo/branches" --paginate --jq '.[].name' |
grep -E '^ci-sync-[0-9a-z]{6}$' | sed "s|^|$repo: |"
done
見つかったときに入れ替える範囲
Wizの勧めは、次のとおりです。
- そのリポジトリで使えたシークレットを、リポジトリ・組織・環境の単位ですべて入れ替えます。実行やブランチを消しても、漏れたことは取り消せません。
- コミットを送ったトークンを失効させ、持ち主のPCにインフォスティーラーが入っていないかを調べます。
- ci-syncのブランチがセルフホストランナーで動いていたら、そのサーバーとインスタンスロールは乗っ取られた前提で扱います。
再発を防ぐ設定として、Wizは次の4つを挙げています。
- 本番のシークレットを持つ環境で、レビューの承認を必須にする
- OIDCの信頼ポリシーを、保護したブランチか環境に限る
- 個人用アクセストークンの代わりに、GitHub Appか、有効期限の短いfine-grained personal access tokenを使う
- セルフホストランナーを使い捨てにし、Dockerのソケットやメタデータに自由に届かないようにする
今回の侵入口は、インフォスティーラーや過去のサプライチェーン攻撃で漏れたトークンです。トークンの有効期限と権限の棚卸しを、シークレットの入れ替えと同じ日に進めておくと、次に漏れたときの被害を小さくできると、編集部はみています。