# Staging Subdomain Hunt

> Hunt staging via crt.sh when production is WAF-hardened.

- Skill: `uphiago/staging-subdomain-hunt` (Agent Skill)
- Install (CLI): `npx skillmds@latest add uphiago/staging-subdomain-hunt`
- Raw SKILL.md: https://api.skillmd.com/api/skills/uphiago/staging-subdomain-hunt/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: uphiago (https://skillmd.com/u/uphiago)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/uphiago/staging-subdomain-hunt

---


# Staging & Subdomain Hunt Skill

Discover staging, development, and internal subdomains through certificate transparency (crt.sh), DNS brute force, and web probing. Exploit the staging security gap — staging environments consistently have weaker security than production (no WAF, debug enabled, install pages accessible). Proven on 7 US targets where staging subdomains exposed phpinfo, WordPress install pages, and internal APIs not visible on production.

## When to Use

- Running `deep-invade` Phase 5 on a high-value target.
- Production target is well-secured (WAF, no leaks) — pivot to staging.
- Target has a large attack surface (e-commerce, SaaS, franchise model).
- You need additional entry points when the main site is hardened.
- After `subdomain-enumeration` produces a list of subdomains.

## Prerequisites

- `terminal` with curl, httpx, jq.
- Target domain (e.g., `example.com`).
- For DNS brute force: wordlist at `./tools/subdomains.txt`.

## How to Run

```bash
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
```

## Quick Reference

| Indicator | What It Means | Action |
|-----------|---------------|--------|
| `/wp-admin/install.php` returns 200 | Fresh WordPress — no site configured | Install takeover |
| `/wp-admin/upgrade.php` returns 200 | WP needs DB upgrade | DB info disclosure |
| `info.php` / `phpinfo.php` on staging | Debug enabled | PHPInfo analysis (see phpinfo-to-rce) |
| Staging has no Cloudflare/WAF | Direct origin access | Run full deep-invade on origin IP |
| CORS on staging but not production | Staging has weaker CORS policy | CORS attack from staging context |
| `.env` on staging | Dev credentials exposed | Credential theft, pivot to production |

## Procedure

### Step 1 — Certificate Transparency Enumeration

```bash
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"
```

### Step 2 — Live Host Discovery

```bash
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
```

### Step 3 — WordPress Install Page Check (Staging Takeover)

Staging sites frequently have WordPress installed but not configured:

```bash
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
```

### Step 4 — Production vs Staging Security Gap Analysis

```bash
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
```

### Step 5 — Franchise/Multi-Location Subdomain Enumeration

For franchise or multi-location businesses:

```bash
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
```

## Pitfalls

- **crt.sh rate limiting.** crt.sh may return empty JSON if rate-limited. Use 2-3s delays between queries or query the PostgreSQL dump directly at `crt.sh/?d=`.
- **Wildcard certs hide subdomains.** If `*.example.com` is the only cert, individual subdomains won't appear in crt.sh. Use subfinder DNS brute force as fallback.
- **Staging may require VPN.** Some staging environments are IP-restricted.
  Test only from source addresses approved by the engagement.
- **WordPress install.php on production.** Some poorly maintained production sites also have this accessible. It's not always staging-specific. Check for "Welcome to WordPress" title text to confirm it's a fresh install.
- **CORS can differ between production and staging.** Test the same bounded
  endpoint matrix in both environments before claiming a security-control gap.

## Verification

- Every staging subdomain MUST be probed with httpx to confirm it's live.
- WordPress install.php MUST return HTTP 200 with "WordPress" + "installation" in body (not a redirect or SPA).
- Staging weakness MUST be compared against production to confirm a security gap (e.g., production has WAF but staging doesn't).
- Internal subdomain leaks from crt.sh must be verified to be the target's infrastructure (not unrelated domains in the same cert).
- Discovered credentials or configuration values must be handled as sensitive
  evidence. Test credentials only with explicit authorization and approved
  identities; never assume staging credentials may be tried in production.

