mcpField
S
subdomain-enumeration

subdomain-enumeration

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

subdomain-enumeration について

subdomain-enumerationは、セキュリティテストや資産把握向けのサブドメイン発見スキルです。証明書透明性ログやパッシブソースに、DNS解決とHTTPプローブを組み合わせ、分類や乗っ取り確認も行います。

主な機能

  1. 01証明書ログとパッシブソースで発見
  2. 02DNSレコードの一括解決
  3. 03稼働中のHTTPホストを確認
  4. 04サブドメインの分類と乗っ取り確認
  5. 05候補生成と新規証明書の監視

活用シーン

  1. 01対象ドメインの偵察を開始
  2. 02組織の資産一覧を作成
  3. 03ステージング環境や管理パネルを発見
  4. 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.shCertificate 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

コメント