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-invadePhase 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-enumerationproduces a list of subdomains.
Prerequisites
terminalwith curl, httpx, jq.- Target domain (e.g.,
example.com). - For DNS brute force: wordlist at
./tools/subdomains.txt.
How to Run
bashDOMAIN="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
bashDOMAIN="$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
bashDOMAIN="$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:
bashDOMAIN="$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
bashDOMAIN="$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:
bashDOMAIN="$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.comis 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.

