hunt-source-leak について
Webアプリで公開状態になったソースコードやビルド成果物を探すセキュリティテスト用スキルです。ソースマップ、API仕様、リポジトリ情報、環境ファイル、JavaScriptバンドルを調査します。
主な機能
- 01ソースマップの発見と分析
- 02SwaggerとOpenAPIの探索
- 03公開された.gitと.envの確認
- 04ビルド成果物とデバッグファイルの確認
- 05生のJavaScriptバンドル分析
活用シーン
- 01ソースマップやJavaScriptバンドルから秘密情報を調査
- 02SwaggerやOpenAPIから非公開APIルートを把握
- 03リポジトリや環境ファイルの公開状況を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill hunt-source-leakインストール後、Claude Code で/hunt-source-leakを入力して読み込まれたか確認します。
同梱ファイルreferences/react-api-extraction.md
詳細
HUNT-SOURCE-LEAK — ソースコードとビルド成果物の漏えい
最重要ターゲット
TypeScript ソースを公開するソースマップがあれば、すべての API ルート、認証ロジック、シークレットを確認できます。Swagger/OpenAPI JSON があれば、API サーフェス全体を把握できます。
特に価値の高い調査結果:
.js.mapソースマップ — TypeScript/ES6 のソースコード全体を復元できます → ハードコードされた API キー、内部エンドポイント、認証ロジックのバイパスを発見できますswagger.json/openapi.json— すべてのエンドポイント、パラメーター、認証方式、内部ルート名を含む完全な REST API 仕様です.env/.env.production— APP_KEY、DB_PASSWORD、API_KEY、SECRET_KEY が平文で含まれています.git/の公開 — ソースコードの全履歴をgit cloneできます → 過去にハードコードされたシークレットをすべて確認できますasset-manifest.json/_next/static/— すべての JS バンドルのパスがわかります → ソースマップを体系的に探せますbuild-info/info.json— git コミットハッシュ、ビルド日時、依存関係のバージョンがわかります → CVE を狙った攻撃に利用できます
落とし穴(プローブの前にお読みください)
1. 偽のソースマップ — .map として HTML を返すケース
一部の SPA(nginx 上の Angular/React)は、.js.map URL を含む、処理されないすべてのルートに対して index.html を返します。URL は .map で終わり、HTTP 200 を返しますが、コンテンツは JSON ではなく HTML です。
処理の前に検出してください:
# First 80 bytes tell the story
head -c 80 /tmp/map_file
# "<!DOCTYPE" or "<html" = faux map (SPA serving HTML)
# "{" or ")))}" = real source map
python3 -c "
with open('/tmp/map_file') as f:
d = f.read()
if d.strip().startswith('{'): print('Real source map')
elif '<!DOCTYPE' in d or '<html' in d: print('FAUX — SPA serving HTML')
else: print(f'Unknown, first 80b: {repr(d[:80])}')
"
フォールバック: 生の JS バンドルをダウンロードし、直接 grep してください(フェーズ 8)。
2. BusyBox / Alpine の grep -P
最小構成のコンテナー(Alpine Linux など)では、grep -P(Perl 互換正規表現)は利用できません。これはフェーズ 2 のステップ 2、フェーズ 7、Swagger の抽出に影響します。代わりに Python3 の re を使ってください:
# Instead of: grep -Eo 'pattern' file
python3 -c "import sys, re; print(*(re.findall(r'pattern', sys.stdin.read())), sep='\n')" < file
3. crt.sh のレート制限
crt.sh API は厳しくレート制限されています。短い間隔でクエリすると、空の JSON やタイムアウトが返ります。クエリの間隔を 10~15 秒空けてください。応答サイズを抑えるには &limit=20 を使ってください:https://crt.sh/?q=%25.TARGET&output=json&limit=20
フェーズ 1 — すぐに確認できる項目(最初に実行)
# These 10 requests take <30 seconds and often yield Critical findings
for PATH in \
"/.env" \
"/.env.production" \
"/.env.local" \
"/.git/HEAD" \
"/swagger.json" \
"/api/swagger.json" \
"/v1/swagger.json" \
"/openapi.json" \
"/api/openapi.json" \
"/api-docs"; do
STATUS=$(curl --max-time 30 --connect-timeout 10 -s -o /tmp/sl_test -w "%{http_code}" "https://$TARGET$PATH")
if [ "$STATUS" = "200" ]; then
echo "[+] HIT: https://$TARGET$PATH"
head -5 /tmp/sl_test
echo "---"
fi
done
フェーズ 2 — ソースマップの発見
React SPA のソースマップを使った API 抽出の全手順については、references/react-api-extraction.md を参照してください。
# Step 1: Get asset manifest to find all JS bundle paths
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/asset-manifest.json" | python3 -m json.tool 2>/dev/null
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/static/js/main.*.js" 2>/dev/null | head -3
# Next.js
BUILD_ID=$(curl --max-time 30 --connect-timeout 10 -s https://$TARGET/ | grep -Eo '"buildId":"\K[^"]+')
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/_next/static/$BUILD_ID/_buildManifest.js" | head -5
# Step 2: For each JS bundle, check for source map reference at end of file
for JS_URL in $(curl --max-time 30 --connect-timeout 10 -s https://$TARGET/ | grep -Eo 'src="[^"]*\.js"' | sed 's/src="//;s/"//'); do
LAST_LINE=$(curl --max-time 30 --connect-timeout 10 -s "https://$TARGET$JS_URL" | tail -1)
echo "$LAST_LINE" | grep -q "sourceMappingURL" && echo "[+] Source map: $JS_URL"
done
# Step 3: Download and reconstruct source from .map files
JS_URL="https://$TARGET/static/js/main.abc123.js"
MAP_URL="${JS_URL}.map"
curl --max-time 30 --connect-timeout 10 -s "$MAP_URL" | python3 -c "
import sys, json, os
data = json.load(sys.stdin)
sources = data.get('sources', [])
contents = data.get('sourcesContent', [])
for i, (src, content) in enumerate(zip(sources, contents)):
if content:
path = '/tmp/sourcemap_extract/' + src.replace('../','').replace('./',''). replace('webpack://','')
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
f.write(content)
print(f'[+] Extracted: {src}')
"
# Step 4: Grep extracted source for secrets
grep -r "API_KEY\|SECRET\|PASSWORD\|TOKEN\|PRIVATE" /tmp/sourcemap_extract/ 2>/dev/null
grep -r "process\.env\." /tmp/sourcemap_extract/ 2>/dev/null | grep -v "NEXT_PUBLIC_" | head -20
grep -r "http://internal\|localhost\|127\.0\.0\.1\|10\.\|172\.\|192\.168" /tmp/sourcemap_extract/ 2>/dev/null | head -20
フェーズ 3 — Swagger / OpenAPI の発見
# Common paths
SWAGGER_PATHS=(
"/swagger.json" "/swagger.yaml" "/swagger/"
"/api/swagger.json" "/api/swagger.yaml"
"/v1/swagger.json" "/v2/swagger.json" "/v3/swagger.json"
"/openapi.json" "/openapi.yaml"
"/api/openapi.json" "/api-docs" "/api-docs.json"
"/api/v1/swagger.json" "/api/v2/swagger.json"
"/rest/swagger.json" "/rest/api-docs"
"/.well-known/openapi.json"
"/graphql/schema.json"
)
for PATH in "${SWAGGER_PATHS[@]}"; do
STATUS=$(curl --max-time 30 --connect-timeout 10 -s -o /tmp/swagger_test -w "%{http_code}" "https://$TARGET$PATH")
if [ "$STATUS" = "200" ]; then
echo "[+] Found: https://$TARGET$PATH"
# Extract all API paths from swagger
python3 -c "
import sys, json
try:
d = json.load(open('/tmp/swagger_test'))
paths = list(d.get('paths', {}).keys())
print(f'Endpoints: {len(paths)}')
print('\n'.join(sorted(paths)))
except: pass
" | head -50
fi
done
フェーズ 4 — .git の公開
# Check if .git directory is accessible
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/.git/HEAD" | grep -q "ref:" && echo "[+] .git exposed!"
# If exposed, reconstruct repo
# Tool: git-dumper
pip3 install git-dumper
git-dumper "https://$TARGET/.git/" /tmp/dumped-repo/
# Grep for secrets in all git history
cd /tmp/dumped-repo && \
git log --all --oneline 2>/dev/null | head -20
git grep -i "password\|secret\|api_key\|token" $(git rev-list --all) 2>/dev/null | head -30
# trufflehog on git history
trufflehog git file:///tmp/dumped-repo/ 2>/dev/null | head -50
フェーズ 5 — 忘れられたファイルとデバッグ用エンドポイント
# Build artifacts and debug files
DEBUG_PATHS=(
"/build-info.json" "/build/build-info.json"
"/info" "/actuator/info" "/api/info"
"/version" "/api/version" "/_version"
"/health" "/status" "/ping"
"/robots.txt" "/security.txt" "/.well-known/security.txt"
"/sitemap.xml" "/manifest.json" "/browserconfig.xml"
"/crossdomain.xml" "/clientaccesspolicy.xml"
"/phpinfo.php" "/info.php" "/test.php"
"/server-status" "/server-info" "/.htaccess"
"/web.config" "/applicationHost.config"
"/WEB-INF/web.xml" "/META-INF/MANIFEST.MF"
"/package.json" "/composer.json" "/Gemfile"
"/Dockerfile" "/docker-compose.yml" "/.dockerenv"
)
for PATH in "${DEBUG_PATHS[@]}"; do
STATUS=$(curl --max-time 30 --connect-timeout 10 -s -o /tmp/debug_test -w "%{http_code}" "https://$TARGET$PATH")
if [ "$STATUS" = "200" ]; then
echo "[+] Found: https://$TARGET$PATH ($STATUS, $(wc -c < /tmp/debug_test) bytes)"
head -3 /tmp/debug_test
echo "---"
fi
done
フェーズ 6 — .DS_Store によるファイル一覧の取得
# .DS_Store files on macOS-deployed web servers reveal directory structure
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/.DS_Store" | xxd | head -10
# Parse .DS_Store to extract filenames
pip3 install ds_store
python3 -c "
from ds_store import DSStore
with DSStore.open('/tmp/ds_store_test', 'r') as d:
for entry in d:
print(entry.filename)
"
# Recursive .DS_Store enumeration
# Tool: https://github.com/lijiejie/ds_store_exp
python3 ds_store_exp.py "https://$TARGET/"
フェーズ 7 — webpack チャンクの分析
⚠ BusyBox/Alpine:以下の grep -Eo は使わず、代わりに Python3 の re を使ってください(「落とし穴」セクションを参照)。
# Download and analyze webpack chunks for hardcoded values
# Find chunk files
curl --max-time 30 --connect-timeout 10 -s https://$TARGET/ | grep -Eo '"[^"]*\.chunk\.js"' | tr -d '"' | while read chunk; do
echo "Analyzing: $chunk"
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET$chunk" | \
grep -oE '"(api_key|apiKey|secret|password|token|key)"\s*:\s*"[^"]+"' | head -5
done
# Also grep for internal hostnames
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/static/js/main.*.js" | \
grep -oE '"(https?://[^"]*internal[^"]*|http://[^"]*localhost[^"]*)"' | sort -u
# Check for Base64-encoded secrets
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/static/js/main.*.js" | \
grep -Eo '"[A-Za-z0-9+/]{30,}={0,2}"' | while read b64; do
DECODED=$(echo "$b64" | tr -d '"' | base64 -d 2>/dev/null)
echo "$DECODED" | grep -iE "key|secret|password|token" && echo " B64: $b64"
done
フェーズ 8 — 生の JS バンドルによるフォールバック(ソースマップが使えない場合)
ソースマップの URL が HTML(偽のソースマップ)または 404 を返す場合でも、分析を中断しないでください。生の JS バンドルをダウンロードし、直接 grep してください。ソースマップからの復元より時間はかかりますが、内部 API URL、ハードコードされた認証情報、プラットフォーム内部の情報を発見できる可能性があります。
8.1 すべての JS バンドルをダウンロード
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET/" -o /tmp/homepage.html
# Extract ALL JS URLs (both absolute and relative)
python3 -c "
import sys, re
html = open('/tmp/homepage.html').read()
seen = set()
for m in re.finditer(r'src=\"([^\"]+\.js[^\"]*)\"', html):
j = m.group(1)
if j not in seen: seen.add(j); print(j)
for m in re.finditer(r'\"([^\"]+\.js[^\"]*)\"', html):
j = m.group(1)
if j not in seen and '.js' in j and 'node_module' not in j:
seen.add(j); print(j)
" | while read js_url; do
full=$(echo "$js_url" | grep -q "^http" && echo "$js_url" || echo "https://$TARGET$js_url")
curl --max-time 30 --connect-timeout 10 -sk "$full" -o "/tmp/js_$(basename $js_url | cut -d? -f1)" 2>/dev/null
sleep 1
done
8.2 シークレットと内部エンドポイントを grep で検索
python3 -c "
import re, os
d = '/tmp'
for f in os.listdir(d):
if not f.startswith('js_'): continue
data = open(os.path.join(d, f)).read()
# Firebase API keys
for m in re.findall(r'AIza[0-9A-Za-z_-]{35}', data):
print(f'[$f] FIREBASE_KEY: {m}')
# AWS access keys
for m in re.findall(r'AKIA[0-9A-Z]{16}', data):
print(f'[$f] AWS_KEY: {m}')
# Stripe keys for m in re.findall(r's[kr]_(live|test)_[A-Za-z0-9]+', data):
print(f'[$f] STRIPE_KEY: {m}')
# Internal URLs with custom ports — HIGH VALUE
for m in re.findall(r'[\"'"'"'](https?://[^\"'"'"'\\\\\)\s,;:]+:\d{3,5}[^\"'"'"'\\\\\)\s,;]*)', data):
print(f'[$f] INTERNAL_URL: {m}')
# Non-public env var references for m in re.findall(r'process\.env\.(?!NEXT_PUBLIC_)([A-Z_]+)', data):
print(f'[$f] ENV_REF: process.env.{m}')
"
8.3 フレームワーク別の確認項目
Angular(実例:[STAGING_ENV]、このセッションから):
environmentまたはenvironments変数を探します.com:PORTのパターンを grep で検索します — 内部 API ゲートウェイが見つかります- すべての JS バンドルのパスについて
manifest.jsonを確認します
React/Next.js:
- HTML 内の
__NEXT_DATA__を検索します(埋め込み状態/props) - ページ単位の JS について
_next/static/chunks/pages/を確認します
Vue.js:
- HTML 内の
__NUXT__状態を検索します - バンドルされた API 呼び出しについて
/js/app.*.jsを確認します
8.4 実例
このセッションでの MedxGo の偵察では、[STAGING_ENV] 上でソースマップが HTML(偽のソースマップ)を返したため、生の JS を分析しました:
# 3.8MB Angular main.js — one grep line found the jackpot:
python3 -c "
import re
data = open('/tmp/js_main.js').read()
for m in re.findall(r'[\"'"'"'](https?://[^\"'"'"'\\\\\)\s,;:]+:\d+)[\"'"'"']', data):
print(m)
"
# → https://mean.[STAGING_ENV]:446
これは文書化されていない内部 MEAN スタック API サーバーでした。grep の結果の一行から、新たな攻撃対象領域全体が見つかりました。
チェーンテーブル
| 情報漏えいの発見内容 | 連鎖先 | 影響 |
|---|---|---|
| API キーを含むソースマップ | キーを直接使用 → API アクセス | 高/重大 |
| 認証ロジックを含むソースマップ | 認証バイパスルートを特定 | 重大 |
| Swagger → 内部エンドポイント | 文書化されていない管理者ルートをテスト | 高 |
| .git の露出 | ソース履歴全体 → 過去のすべてのシークレット | 重大 |
| git ハッシュを含む build-info | 正確なバージョンを標的とした CVE | 高 |
| DB_PASSWORD を含む .env | データベースへの直接アクセス | 重大 |
ツール
# git-dumper (reconstruct exposed .git)
pip3 install git-dumper
git-dumper "https://target.com/.git/" /tmp/repo/
# sourcemap-explorer (visualize what's in bundles)
npm install -g source-map-explorer
source-map-explorer main.js
# unwebpack-sourcemap (extract all source files)
npm install -g unwebpack-sourcemap
# trufflehog (secret scanning)
trufflehog filesystem /tmp/repo/
検証
✅ ソースマップ:再構築された TypeScript ソースに API エンドポイントまたはハードコードされたシークレットが含まれている ✅ Swagger:JSON に UI では表示されない内部エンドポイントが含まれている ✅ .git の露出:git-dumper がリポジトリのクローンに成功し、履歴にシークレットが含まれている ✅ .env の露出:DATABASE_URL、API_KEY、SECRET_KEY が平文で表示されている
重大度:
- 認証情報を含む .env:重大
- 履歴にシークレットが含まれる .git:重大
- シークレットを含むソースマップ:高
- 内部ルートを含む Swagger:中〜高
- robots.txt のみ:情報提供
検証
- スキルの整合性 — スキルファイルの形式が正しいことを確認します: FAIL FAIL すべてのテストで、スキルが適切に構成されていることを確認します。
関連スキル
- hunt-wordpress — wp-config.php、debug.log、wp-content/uploads の露出
- hunt-firebase — JS バンドル内の Firebase API キーの検出
- hunt-supabase — JS および .env 内の Supabase URL/キーの検出
- hunt-laravel — .env、storage/logs/laravel.log、APP_KEY の露出
- hunt-nextjs — _next/static、buildId、ソースマップの分析
- security-arsenal — API キー、JWT、クラウド認証情報の正規表現パターン
- offensive-osint — GitHub のドーキング、露出したリポジトリ、Shodan/Censys
- cors-chain-automation — ソースマップから判明した露出 API エンドポイントでの CORS テスト