staging-subdomain-hunt について
許可を得たセキュリティテスト向けのサブドメイン調査スキルです。証明書透明性、DNS ブルートフォース、Web プローブでステージング、開発、内部ホストを探し、本番環境との制御比較や発見事項の検証も案内します。
主な機能
- 01証明書透明性の列挙
- 02DNS サブドメイン調査
- 03稼働ホストのプローブ
- 04ステージング環境の確認
- 05本番環境との比較
- 06発見事項の検証
活用シーン
- 01堅牢化された本番サイトの追加テスト入口を発見
- 02ステージングのデバッグページや内部 API の露出を確認
- 03本番環境とステージングのセキュリティ制御を比較
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill staging-subdomain-huntインストール後、Claude Code で/staging-subdomain-huntを入力して読み込まれたか確認します。
詳細
ステージングおよびサブドメインの探索スキル
証明書の透明性(crt.sh)、DNS ブルートフォース、Web プロービングを通じて、ステージング、開発、内部サブドメインを発見します。ステージング環境のセキュリティ上の隙を悪用します。ステージング環境は本番環境より一貫してセキュリティが弱く(WAF がない、デバッグが有効、インストールページにアクセスできるなど)、米国の 7 件のターゲットで効果が実証されています。ステージングのサブドメインから、本番環境では見えない phpinfo、WordPress のインストールページ、内部 API が露出していました。
使用するタイミング
- 価値の高いターゲットに対して
deep-invadeのフェーズ 5 を実行する場合。 - 本番環境のセキュリティが強固な場合(WAF があり、情報漏えいがない場合)は、ステージングに切り替えます。
- ターゲットの攻撃対象領域が広い場合(E コマース、SaaS、フランチャイズモデル)。
- メインサイトの防御が強化されている場合に、追加の侵入ポイントが必要な場合。
subdomain-enumerationによってサブドメインの一覧が生成された後。
前提条件
- curl、httpx、jq を備えた
terminal。 - ターゲットドメイン(例:
example.com)。 - DNS ブルートフォース用のワードリスト:
./tools/subdomains.txt。
実行方法
DOMAIN="example.com"
# crt.sh discovery
curl --max-time 30 --connect-timeout 10 -sk "https://crt.sh/?q=%25.$DOMAIN&output=json" | jq -r '.[].name_value' | sed 's/\*\.//g' | sort -u > subs.txt
# Probe for live hosts
httpx -silent -l subs.txt -threads 50 -status-code -tech-detect -o alive_subs.txt
# Check for staging indicators
grep -iE 'staging|stage|dev|test|uat|beta' alive_subs.txt
クイックリファレンス
| 指標 | 意味 | 対応 |
|---|---|---|
/wp-admin/install.php が 200 を返す | 新規の WordPress — サイトが未設定 | インストール乗っ取り |
/wp-admin/upgrade.php が 200 を返す | WP の DB アップグレードが必要 | DB 情報の漏えい |
ステージング上の info.php / phpinfo.php | デバッグが有効 | PHPInfo の分析(phpinfo-to-rce を参照) |
| ステージングに Cloudflare/WAF がない | オリジンに直接アクセス可能 | オリジン IP に対して deep-invade をすべて実行 |
| 本番環境にはない CORS がステージングにある | ステージングの CORS ポリシーが弱い | ステージングのコンテキストから CORS 攻撃 |
ステージング上の .env | 開発用の認証情報が露出 | 認証情報の窃取、本番環境へのピボット |
手順
ステップ 1 — 証明書の透明性による列挙
DOMAIN="$1"
OUTDIR="$OUTDIR/staging/$DOMAIN"
mkdir -p "$OUTDIR"
echo "[*] crt.sh enumeration for *.$DOMAIN"
# Primary crt.sh query
curl -sk --max-time 30 --connect-timeout 10 "https://crt.sh/?q=%25.$DOMAIN&output=json" 2>/dev/null | \
jq -r '.[].name_value' 2>/dev/null | sed 's/\*\.//g' | sed 's/^www\.//' | sort -u > "$OUTDIR/crtsh_subs.txt"
# Also try without wildcard prefix
curl -sk --max-time 30 --connect-timeout 10 "https://crt.sh/?q=$DOMAIN&output=json" 2>/dev/null | \
jq -r '.[].name_value' 2>/dev/null | sed 's/\*\.//g' | sed 's/^www\.//' | sort -u >> "$OUTDIR/crtsh_subs.txt"
sort -u "$OUTDIR/crtsh_subs.txt" -o "$OUTDIR/crtsh_subs.txt"
sub_count=$(wc -l < "$OUTDIR/crtsh_subs.txt")
echo "[+] crt.sh: $sub_count unique subdomains"
# Categorize by pattern
echo ""
echo "[*] Staging/dev subdomains:"
grep -iE 'staging|stage|dev\.|development|test|uat|beta|sandbox|demo|preview|qa' "$OUTDIR/crtsh_subs.txt"
echo ""
echo "[*] Admin/internal subdomains:"
grep -iE 'admin|portal|internal|dashboard|manage|cp\.|control|panel|cpanel|webmail|mail\.' "$OUTDIR/crtsh_subs.txt"
echo ""
echo "[*] API subdomains:"
grep -iE 'api|rest|graphql|ws\.|websocket' "$OUTDIR/crtsh_subs.txt"
echo ""
echo "[*] Infrastructure subdomains:"
grep -iE 'cdn|static|assets|media|img|images|files|download|origin|proxy' "$OUTDIR/crtsh_subs.txt"
echo ""
echo "[*] Franchise/location subdomains:"
grep -iE 'franchise|location|store|shop|branch|office' "$OUTDIR/crtsh_subs.txt"
ステップ 2 — 稼働中のホストの検出
DOMAIN="$1"
OUTDIR="$OUTDIR/staging/$DOMAIN"
echo "[*] Probing $(wc -l < "$OUTDIR/crtsh_subs.txt") subdomains..."
httpx -silent -l "$OUTDIR/crtsh_subs.txt" -threads 50 -status-code -tech-detect -title \
-o "$OUTDIR/alive_subs.txt"
alive=$(wc -l < "$OUTDIR/alive_subs.txt")
echo "[+] $alive live hosts"
# Prioritize staging/dev
echo ""
echo "[*] Staging/dev LIVE:"
grep -iE 'staging|stage|dev\.|development|test|uat' "$OUTDIR/alive_subs.txt" | head -20
ステップ 3 — WordPress インストールページの確認(ステージングの乗っ取り)
ステージングサイトには、WordPress がインストールされていても設定されていないことがよくあります。
DOMAIN="$1"
OUTDIR="$OUTDIR/staging/$DOMAIN"
echo "[*] Checking for WordPress install pages on staging..."
for sub in $(grep -iE 'staging|stage|dev' "$OUTDIR/alive_subs.txt" | awk '{print $1}' | head -10); do
echo "--- $sub ---"
# Check install.php (fresh WP, no config)
install_code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 --connect-timeout 10 "$sub/wp-admin/install.php")
if [[ "$install_code" == "200" ]]; then
echo " [TAKEOVER] /wp-admin/install.php — fresh WP install, can configure site!"
# Extract form fields
curl -sk --max-time 10 --connect-timeout 10 "$sub/wp-admin/install.php" | grep -Eo 'name="[^"]+"' | sort -u
fi
# Check upgrade.php (needs DB upgrade)
upgrade_code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 --connect-timeout 10 "$sub/wp-admin/upgrade.php")
if [[ "$upgrade_code" == "200" ]]; then
echo " [INFO] /wp-admin/upgrade.php — DB upgrade page accessible"
fi
# Check setup-config.php (no wp-config)
config_code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 --connect-timeout 10 "$sub/wp-admin/setup-config.php")
if [[ "$config_code" == "200" || "$config_code" == "409" ]]; then
echo " [INFO] /wp-admin/setup-config.php — wp-config missing or accessible"
fi
# Check for exposed info.php
info_code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "$sub/info.php")
[[ "$info_code" == "200" ]] && echo " [CRITICAL] /info.php exposed on staging!"
# Check for .env on staging
env_code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "$sub/.env")
[[ "$env_code" == "200" ]] && echo " [CRITICAL] /.env exposed on staging!"
done
ステップ 4 — 本番環境とステージングのセキュリティ上の差の分析
DOMAIN="$1"
PROD="https://$DOMAIN"
STAGING=$(grep -iE 'staging|stage' "$OUTDIR/staging/$DOMAIN/alive_subs.txt" | head -1 | awk '{print $1}')
if [[ -n "$STAGING" ]]; then
echo "[*] Comparing $PROD vs $STAGING"
# Compare HTTP headers
echo "=== Production Headers ==="
curl --max-time 30 --connect-timeout 10 -skI "$PROD" 2>/dev/null | head -20
echo ""
echo "=== Staging Headers ==="
curl --max-time 30 --connect-timeout 10 -skI "$STAGING" 2>/dev/null | head -20
# Check for common staging weaknesses
echo ""
echo "[*] Staging-specific checks:"
# Directory listing
listing=$(curl -sk --max-time 5 --connect-timeout 5 "$STAGING/wp-content/uploads/" | grep -i "Index of")
[[ -n "$listing" ]] && echo " [WEAK] Directory listing enabled on uploads"
# Debug mode
debug=$(curl -sk --max-time 5 --connect-timeout 5 "$STAGING/" | grep -i "wp_debug\|debug mode\|error_reporting")
[[ -n "$debug" ]] && echo " [WEAK] Debug output visible"
# CORS on staging
cors=$(curl -skI --max-time 5 --connect-timeout 5 "$STAGING/wp-json/wp/v2/users" -H "Origin: https://evil.com" | grep -i "access-control-allow-credentials: true")
[[ -n "$cors" ]] && echo " [WEAK] CORS credential reflection on staging"
# XMLRPC on staging
xmlrpc=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 -X POST "$STAGING/xmlrpc.php" \
-d '<?xml version="1.0"?><methodCall><methodName>demo.sayHello</methodName></methodCall>')
[[ "$xmlrpc" == "200" ]] && echo " [WEAK] XMLRPC open on staging"
fi
ステップ 5 — フランチャイズ/複数拠点のサブドメイン列挙
フランチャイズまたは複数拠点を持つ企業の場合:
DOMAIN="$1"
OUTDIR="$OUTDIR/staging/$DOMAIN"
echo "[*] Franchise/location subdomains:"
# Extract location-based subdomains
grep -iE 'franchise|location|store|shop|branch|office|city|state' "$OUTDIR/alive_subs.txt" | while read -r line; do
sub=$(echo "$line" | awk '{print $1}')
echo "--- $sub ---"
# Check if it's a WordPress site
wp=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "$sub/wp-login.php")
[[ "$wp" =~ ^(200|301|302)$ ]] && echo " WordPress detected"
# Check for WPSL (WP Store Locator) data
wpsl=$(curl --max-time 30 --connect-timeout 10 -sk "$sub/wp-json/wpsl/v1/" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else 'no')" 2>/dev/null)
[[ "$wpsl" != "no" && "$wpsl" != "0" ]] && echo " WPSL: $wpsl locations"
# Check for store-specific data
users=$(curl --max-time 30 --connect-timeout 10 -sk "$sub/wp-json/wp/v2/users" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else 0)" 2>/dev/null)
[[ "$users" -gt 0 ]] && echo " Users: $users"
done
注意点
- crt.sh のレート制限。 レート制限がかかると、crt.sh が空の JSON を返す場合があります。クエリ間に 2〜3 秒の間隔を空けるか、
crt.sh/?d=で PostgreSQL ダンプを直接クエリしてください。 - ワイルドカード証明書ではサブドメインが隠れます。
*.example.comの証明書しかない場合、個々のサブドメインは crt.sh に表示されません。代替手段として、subfinder の DNS ブルートフォースを使用してください。 - ステージングには VPN が必要な場合があります。 ステージング環境の中には IP 制限されているものがあります。 エンゲージメントで承認された送信元アドレスからのみテストしてください。
- 本番環境上の WordPress install.php。 管理が行き届いていない本番サイトでも、アクセス可能な場合があります。必ずしもステージング固有のものではありません。新規インストールであることを確認するには、タイトルテキストに「Welcome to WordPress」が含まれているか確認してください。
- CORS の設定は本番環境とステージングで異なる場合があります。 セキュリティ制御の差があると判断する前に、両方の環境で同じ限定されたエンドポイントのマトリックスをテストしてください。
検証
- すべてのステージングサブドメインに対して httpx でプローブを行い、稼働中であることを確認する必要があります。
- WordPress install.php は、リダイレクトや SPA ではなく、本文に「WordPress」と「installation」を含む HTTP 200 を返す必要があります。
- セキュリティ上の差を確認するため、ステージングの弱点を本番環境と比較する必要があります(例:本番環境には WAF があるが、ステージングにはない)。
- crt.sh で見つかった内部サブドメインの漏えいについて、同じ証明書に含まれている無関係なドメインではなく、ターゲットのインフラであることを確認する必要があります。
- 発見した認証情報や設定値は、機密性の高い証拠として取り扱う必要があります。 認証情報は明示的な許可を得て、承認された ID でのみテストしてください。ステージングの認証情報を本番環境で試してよいと決めつけないでください。
コメント
Markdown 対応