subdomain-enumeration について
subdomain-enumerationは、セキュリティテストや資産把握向けのサブドメイン発見スキルです。証明書透明性ログやパッシブソースに、DNS解決とHTTPプローブを組み合わせ、分類や乗っ取り確認も行います。
主な機能
- 01証明書ログとパッシブソースで発見
- 02DNSレコードの一括解決
- 03稼働中のHTTPホストを確認
- 04サブドメインの分類と乗っ取り確認
- 05候補生成と新規証明書の監視
活用シーン
- 01対象ドメインの偵察を開始
- 02組織の資産一覧を作成
- 03ステージング環境や管理パネルを発見
- 04新規サブドメインの証明書を監視
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill subdomain-enumerationインストール後、Claude Code で/subdomain-enumerationを入力して読み込まれたか確認します。
詳細
サブドメイン列挙スキル
Certificate Transparency ログ(crt.sh)、DNS ブルートフォース、パッシブソースを使って、サブドメインを包括的に発見します。あらゆる偵察パイプラインの最初のステップです。存在を知らないものは攻撃できません。サブドメインの列挙では、ステージング環境、内部管理パネル、API ゲートウェイ、忘れられた WordPress インストールが一貫して見つかります。これらは本番サイトよりも防御が手薄な標的です。
使用するタイミング
- 任意のターゲットドメインの偵察を開始するとき。
- 本番サイトのセキュリティが万全な場合に、より侵入しやすい入口を探すとき。
skill_view(name='wp-mass-recon')の後、各 WordPress ターゲットのサブドメインを列挙するとき。- ターゲット組織の完全な資産インベントリを作成するとき。
前提条件
curl、httpx、dig、jq、dnsx、subfinder。- オペレーターが用意した DNS ブルートフォース用ワードリスト。
- crt.sh、crt.name、subfinder のプロバイダーなど、サードパーティのパッシブソースにドメインを開示する承認。
実行方法
DOMAIN="${1:-example.com}"
OUTPUT_ROOT="${OUTPUT_DIR:-./output}"
OUTDIR="$OUTPUT_ROOT/subdomains/$DOMAIN"
if [[ ! "$DOMAIN" =~ ^[A-Za-z0-9.-]+$ ]]; then
echo "Invalid domain: $DOMAIN" >&2
exit 2
fi
mkdir -p "$OUTDIR"
# Passive: crt.sh (retry: it frequently returns 502 while its database refreshes)
for attempt in 1 2 3; do
curl -fsS --max-time 30 --connect-timeout 10 \
"https://crt.sh/?q=%25.$DOMAIN&output=json" -o "$OUTDIR/crtsh_raw.json" && break
sleep 15
done
jq -r '.[].name_value' "$OUTDIR/crtsh_raw.json" 2>/dev/null \
| sed 's/\*\.//g' \
| sort -u > "$OUTDIR/crtsh.txt"
# If crt.sh stays down, crtsh.txt ends up empty: keep going with crt.name/subfinder.
# crt.name (historical CT data; resolve names before probing)
curl -fsS --max-time 30 --connect-timeout 10 "https://crt.name/v1/search?apex=$DOMAIN" \
| sed 's/\r$//' \
| sed 's/^\*\.//' \
| sort -u > "$OUTDIR/crtname.txt"
# Passive: subfinder
subfinder -d "$DOMAIN" -silent -timeout 30 > "$OUTDIR/subfinder.txt"
# Merge and deduplicate
cat "$OUTDIR/crtsh.txt" "$OUTDIR/crtname.txt" "$OUTDIR/subfinder.txt" \
| grep -E '^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' \
| sort -u > "$OUTDIR/all_subs.txt"
# Probe live hosts
httpx -silent -l "$OUTDIR/all_subs.txt" -threads 50 -rate-limit 2 \
-status-code -tech-detect -title -o "$OUTDIR/alive.txt"
クイックリファレンス
| ソース | 方法 | 網羅性 | 速度 |
|---|---|---|---|
| crt.sh | Certificate Transparency | 優秀(ほとんどの証明書) | 高速(1~5 秒) |
| subfinder | パッシブ API(VirusTotal、Shodan、DNSdumpster など) | 非常に良好 | 高速(30~60 秒) |
| dnsx | 一括 DNS A/AAAA/CNAME 解決(dig より 100 倍高速) | 良好(HTTP を使わないホストを発見) | 高速(10~30 秒) |
| httpx プローブ | 稼働中の HTTP/HTTPS チェック | Web 攻撃対象領域の特定に最適 | 高速(30~60 秒) |
| Google 検索演算子 | site:example.com | 補助的 | 手動 |
手順
ステップ 1 — パッシブ列挙(crt.sh + subfinder)
DOMAIN="$1"
OUTPUT_ROOT="${OUTPUT_DIR:-./output}"
OUTDIR="$OUTPUT_ROOT/subdomains/$DOMAIN"
if [[ ! "$DOMAIN" =~ ^[A-Za-z0-9.-]+$ ]]; then
echo "Invalid domain: $DOMAIN" >&2
exit 2
fi
mkdir -p "$OUTDIR"
echo "[*] Passive enumeration for $DOMAIN..."
# crt.sh — certificate transparency logs (retry: it frequently returns 502 while its database refreshes)
echo "[*] crt.sh query..."
for attempt in 1 2 3; do
curl -fsS --max-time 30 --connect-timeout 10 \
"https://crt.sh/?q=%25.$DOMAIN&output=json" -o "$OUTDIR/crtsh_raw.json" 2>/dev/null && break
sleep 15
done
jq -r '.[].name_value' "$OUTDIR/crtsh_raw.json" 2>/dev/null | \
sed 's/\*\.//g' | \
sed 's/^www\.//' | \
sort -u > "$OUTDIR/crtsh.txt"
crt_count=$(wc -l < "$OUTDIR/crtsh.txt")
echo " crt.sh: $crt_count entries"
if [[ ! -s "$OUTDIR/crtsh.txt" ]]; then
echo " [!] crt.sh unavailable or empty after retries; results are incomplete" >&2
fi
# crt.name — historical certificate-transparency data
echo "[*] crt.name query..."
curl -fsS --max-time 30 --connect-timeout 10 "https://crt.name/v1/search?apex=$DOMAIN" 2>/dev/null | \
sed 's/\r$//' | \
sed 's/^\*\.//' | \
sort -u > "$OUTDIR/crtname.txt"
crtname_count=$(wc -l < "$OUTDIR/crtname.txt")
echo " crt.name: $crtname_count entries (historical; resolve before probing)"
# Also query with %25. (wildcard)
for attempt in 1 2 3; do
curl -fsS --max-time 30 --connect-timeout 10 \
"https://crt.sh/?q=%25.%25.$DOMAIN&output=json" -o "$OUTDIR/crtsh_wildcard_raw.json" 2>/dev/null && break
sleep 15
done
jq -r '.[].name_value' "$OUTDIR/crtsh_wildcard_raw.json" 2>/dev/null | \
sed 's/\*\.//g' | \
sort -u > "$OUTDIR/crtsh_wildcard.txt"
# subfinder — passive API aggregation
echo "[*] subfinder..."
subfinder -d "$DOMAIN" -silent -timeout 30 2>/dev/null | sort -u > "$OUTDIR/subfinder.txt"
subf_count=$(wc -l < "$OUTDIR/subfinder.txt")
echo " subfinder: $subf_count entries"
# Merge all passive sources
cat "$OUTDIR"/crtsh.txt "$OUTDIR"/crtsh_wildcard.txt "$OUTDIR"/crtname.txt "$OUTDIR"/subfinder.txt 2>/dev/null | \
sed 's/^www\.//' | \
grep -E '^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' | \
sort -u > "$OUTDIR/all_passive.txt"
total=$(wc -l < "$OUTDIR/all_passive.txt")
echo ""
echo "[+] Total unique subdomains (passive): $total"
ステップ 2 — DNS 解決
DOMAIN="$1"
OUTPUT_ROOT="${OUTPUT_DIR:-./output}"
OUTDIR="$OUTPUT_ROOT/subdomains/$DOMAIN"
echo "[*] Resolving subdomains..."
# Batch resolve with dnsx (100x faster than dig loop)
dnsx -silent -l "$OUTDIR/all_passive.txt" -a -resp-only -o "$OUTDIR/resolved_raw.txt" 2>/dev/null
# Format output: subdomain => IP
while read -r line; do
sub=$(echo "$line" | cut -d' ' -f1)
ip=$(echo "$line" | cut -d' ' -f2)
echo "$sub => $ip"
done < "$OUTDIR/resolved_raw.txt" > "$OUTDIR/resolved.txt"
resolved=$(wc -l < "$OUTDIR/resolved.txt")
echo "[+] $resolved subdomains resolved to IPs"
# Count unique IPs
unique_ips=$(awk '{print $3}' "$OUTDIR/resolved.txt" | sort -u | wc -l)
echo "[+] $unique_ips unique IPs"
# Identify shared hosting (many subdomains → same IP)
echo ""
echo "[*] Shared hosting clusters:"
awk '{print $3}' "$OUTDIR/resolved.txt" | sort | uniq -c | sort -rn | head -10 | while read -r count ip; do
[[ "$count" -gt 1 ]] && echo " $ip: $count subdomains"
done
ステップ 3 — 稼働中のホストの発見
DOMAIN="$1"
OUTPUT_ROOT="${OUTPUT_DIR:-./output}"
OUTDIR="$OUTPUT_ROOT/subdomains/$DOMAIN"
echo "[*] Probing live hosts..."
# httpx with tech detection
httpx -silent -l "$OUTDIR/all_passive.txt" -threads 50 \
-status-code -tech-detect -title -location \
-o "$OUTDIR/alive.txt" 2>/dev/null
alive=$(wc -l < "$OUTDIR/alive.txt")
echo "[+] $alive live hosts"
# Categorize by status code
echo ""
echo "[*] By HTTP status:"
echo " 200: $(grep -c '\[200\]' "$OUTDIR/alive.txt")"
echo " 301/302: $(grep -cE '\[301\]|\[302\]' "$OUTDIR/alive.txt")"
echo " 403: $(grep -c '\[403\]' "$OUTDIR/alive.txt")"
echo " 404: $(grep -c '\[404\]' "$OUTDIR/alive.txt")"
# Categorize by technology
echo ""
echo "[*] By technology:"
grep -Eo '\[[a-z-]+\]' "$OUTDIR/alive.txt" | tr -d '[]' | sort | uniq -c | sort -rn | head -15
# WordPress subdomains
echo ""
echo "[*] WordPress subdomains:"
grep -i 'wordpress' "$OUTDIR/alive.txt" | awk '{print $1}' | head -10
# Non-HTTP services (from DNS resolution)
echo ""
echo "[*] Interesting non-standard ports from DNS (MX, NS, etc.):"
dig +short "$DOMAIN" MX 2>/dev/null | head -5
dig +short "$DOMAIN" NS 2>/dev/null | head -5
dig +short "$DOMAIN" TXT 2>/dev/null | grep -i 'spf\|v=spf' | head -3
ステップ 4 — サブドメインの分類
DOMAIN="$1"
OUTPUT_ROOT="${OUTPUT_DIR:-./output}"
OUTDIR="$OUTPUT_ROOT/subdomains/$DOMAIN"
echo "[*] Categorizing subdomains..."
# Staging/Dev
echo ""
echo "=== STAGING / DEV ==="
grep -iE 'staging|stage|dev\.|development|test|uat|beta|sandbox|preview|qa' "$OUTDIR/all_passive.txt"
# Admin/Internal
echo ""
echo "=== ADMIN / INTERNAL ==="
grep -iE 'admin|portal|internal|dashboard|manage|cp\.|control|panel|cpanel|webmail|mail\.' "$OUTDIR/all_passive.txt"
# API
echo ""
echo "=== API ==="
grep -iE 'api|rest|graphql|ws\.|websocket' "$OUTDIR/all_passive.txt"
# Infrastructure
echo ""
echo "=== CDN / STATIC ==="
grep -iE 'cdn|static|assets|media|img|images|files|download|origin|proxy' "$OUTDIR/all_passive.txt"
# Email
echo ""
echo "=== EMAIL ==="
grep -iE 'mail\.|smtp|imap|pop|email|webmail|autodiscover' "$OUTDIR/all_passive.txt"
# Cloud
echo ""
echo "=== CLOUD ==="
grep -iE 'aws|azure|gcp|cloud|s3|bucket|firebase' "$OUTDIR/all_passive.txt"
# Legacy
echo ""
echo "=== LEGACY / OLD ==="
grep -iE 'old|old\.|v1|v2|legacy|archive|backup|bak' "$OUTDIR/all_passive.txt"
ステップ 5 — サブドメイン乗っ取りの確認
DOMAIN="$1"
OUTPUT_ROOT="${OUTPUT_DIR:-./output}"
OUTDIR="$OUTPUT_ROOT/subdomains/$DOMAIN"
echo "[*] Checking for subdomain takeover opportunities..."
# Check for dangling CNAMEs (subdomains pointing to non-existent services)
# For bulk CNAME check: dnsx -silent -l all_passive.txt -cname -resp-only
while read -r sub; do
cname=$(dig +short "$sub" CNAME 2>/dev/null)
if [[ -n "$cname" ]]; then
# Check if the CNAME target resolves
cname_ip=$(dig +short "$cname" A 2>/dev/null)
if [[ -z "$cname_ip" ]]; then
echo "[TAKEOVER?] $sub => $cname (NOT RESOLVING)"
# Identify provider from CNAME
if echo "$cname" | grep -qi 'amazonaws.com'; then
echo " Provider: AWS (S3/CloudFront) — check if bucket/domain is claimable"
elif echo "$cname" | grep -qi 'azure'; then
echo " Provider: Azure — check if resource is claimable"
elif echo "$cname" | grep -qi 'github.io'; then
echo " Provider: GitHub Pages — check if repo name is available"
elif echo "$cname" | grep -qi 'herokuapp.com'; then
echo " Provider: Heroku — check if app name is available"
elif echo "$cname" | grep -qi 'vercel-dns.com'; then
echo " Provider: Vercel — check if project is claimable"
elif echo "$cname" | grep -qi 'zendesk.com'; then
echo " Provider: Zendesk — check if help desk is claimable"
fi
fi
fi
sleep 0.5
done < "$OUTDIR/all_passive.txt"
注意点
- crt.sh のレート制限。 レート制限がかかると、crt.sh が空の JSON を返すことがあります。遅延を入れるか、PostgreSQL ダンプを直接クエリしてください。
- subfinder には API キーが必要です。 一部のソース(VirusTotal、Shodan)では、
~/.config/subfinder/provider-config.yamlに API キーが必要です。キーがない場合、結果は限られます。 - ワイルドカード DNS。
*.example.comが同じ IP に解決される場合、httpx ではすべてのサブドメインが「稼働中」と表示されます。ランダムな文字列を解決してワイルドカードを確認してください:dig RANDOMSTRING.example.com。 - Cloudflare によるプロキシ。 Cloudflare の背後にあるサブドメインでは、オリジン IP ではなく Cloudflare の IP が表示されます。過去の DNS レコードには、SecurityTrails または DNSDumpster を使用してください。
検証
- 各サブドメインに対して必ず httpx プローブを実行し、HTTP/HTTPS を提供していることを確認してください。
- CDN の IP(Cloudflare、CloudFront、Fastly)をオリジン IP と誤認しないよう、解決された IP を必ず既知の CDN IP と照合してください。
- サブドメイン乗っ取りの候補について、CNAME の参照先が未取得であることを手動で確認してください。
- 稼働中のすべてのサブドメインについて、URL、HTTP ステータス、技術スタック、ページタイトルを記録してください。
フェーズ 7 — パーミュテーションと予測
すでに発見したサブドメインから有効な変化形を生成し、隠れたサービスを探します:
# gotator — generates permutations
gotator -sub all_subs.txt -perm permutations.txt -depth 1 -numbers 3 -md | sort -u > subs_permuted.txt
# Resolve permutations
puredns resolve subs_permuted.txt -r resolvers.txt -o subs_permuted_alive.txt
# Common permutation patterns for the wordlist
# %s-dev, dev-%s, %s-staging, staging-%s, %s-prod, %s-internal
# %s-admin, admin-%s, %s-api, api-%s, %s-test, test-%s
# %s-stg, stg-%s, %s-uat, uat-%s, %s-www, www-%s
フェーズ 8 — TLD の拡張
target.com を所有する企業は、target.io、target.net、target.xyz を見落としていることがよくあります:
# tldbrute — discovers registered TLD variants
tldbrute -d target.com
# Manual IANA TLD list approach
wget -q https://data.iana.org/TLD/tlds-alpha-by-domain.txt
ROOT=$(echo "target.com" | cut -d. -f1)
cat tlds-alpha-by-domain.txt | tr '[:upper:]' '[:lower:]' \
| while read tld; do echo "$ROOT.$tld"; sleep 0.2; done \
| httpx -silent -mc 200 > tlds_alive.txt
# Expand existing subdomains across TLDs
cat all_subs.txt | while read sub; do
cat tlds-alpha-by-domain.txt | tr '[:upper:]' '[:lower:]' \
| sed "s/^/$sub./"
done | dnsx -silent > subs_tld_expanded.txt
フェーズ 9 — 証明書のリアルタイム監視
新しいサブドメインが発行された瞬間に検出します:
# gungnir — real-time certificate transparency monitoring
gungnir -d target.com
# certwatcher — alternative CT log monitor
certwatcher -d target.com --webhook https://hooks.slack.com/xxx
コメント
Markdown 対応