Skip to main content

VHosts & Subdomains

Enumerate virtual hosts and subdomains to find admin panels, staging environments, and forgotten services that share the same infrastructure.

CAUTION

Authorized use only. See ../../SECURITY.md.


Table of Contents​


1. Overview​

Three different things that are often confused:

ConceptWhat it meansHow to find
SubdomainDifferent DNS record (admin.example.com)DNS enumeration
VHostSame IP, routed by Host: header (admin on same IP)HTTP fuzzing with Host: header
Virtual directoryPath on the same vhost (/admin/)Directory brute-force

Why enumerate:

  • Development / staging environments are often less protected.
  • Admin panels hide on separate subdomains / vhosts.
  • Forgotten services (test., old., backup.) expose old code and creds.
  • The same IP can serve 5-20 different sites via SNI or Host header.

Rule: a subdomain enum without vhost fuzzing misses half the targets. Do both.


2. Scope & OPSEC​

2.1 Passive vs active​

MethodTraffic to targetTag
Certificate Transparency, DNS, WHOIS, ShodanNone (third parties)[PROD]
DNS brute-force (gobuster, ffuf dns)DNS queries only[PROD]
VHost fuzzing (HTTP requests)Direct HTTP[PROD-RISK]

2.2 Rate limiting​

ToolLabProduction
gobuster dnsdefault--delay 50ms -t 20
ffuf (DNS)default-rate 20 -t 10
ffuf (vhost)default-rate 10 -t 5
gobuster vhostdefault--delay 100ms -t 5
amass enum (active)default-timeout 5 -max-dns-queries 20
dnsxdefault-rate-limit 20

2.3 Common pitfalls in production​

  • Do NOT brute-force at full speed in production - DNS is usually rate-limited by the provider and abuse may be reported.
  • Do NOT fuzz vhosts with aggressive wordlists without approval.
  • Do NOT assume subdomains are in scope. Check the scope document.

3. Passive Enumeration​

No direct interaction with the target's HTTP services.

3.1 DNS and WHOIS​

dig +short target.com
dig +short -t ANY target.com
dig +short -t NS target.com
whois target.com | grep -iE 'name server|registrar|organization'

3.2 Certificate Transparency (crt.sh)​

Every TLS certificate issued for a domain is public. Subdomains leak via SANs.

# CLI
curl -s "https://crt.sh/?q=%25.target.com&output=json" | jq -r ".[].name_value" | sort -u

# Web UI
# https://crt.sh/?q=target.com

Expected output:

admin.target.com
api.target.com
staging.target.com
*.target.com
www.target.com

3.3 Google dorking​

site:*.target.com -www
site:*.target.com inurl:admin
site:*.target.com -inurl:www

3.4 Shodan / Censys​

# Shodan
ssl:target.com
hostname:target.com

# Censys
services.tls.certificates.leaf_data.names: target.com

3.5 TheHarvester​

theHarvester -d target.com -b all -f report.html

Sources: search engines, PGP keys, LinkedIn, GitHub.

3.6 amass (passive)​

amass enum -passive -d target.com -o subs.txt

3.7 subfinder / assetfinder​

subfinder -d target.com -silent -o subs.txt
assetfinder --subs-only target.com

3.8 Merge + deduplicate​

cat subs*.txt | sort -u > all-subs.txt
wc -l all-subs.txt

4. Active Subdomain Enumeration​

DNS brute-force against common names. DNS only, no HTTP.

4.1 gobuster dns​

gobuster dns -d target.com -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -t 20
gobuster dns -d target.com -w wordlist.txt --show-ips

Expected output:

Found: admin.target.com
Found: api.target.com
Found: staging.target.com

4.2 ffuf (DNS)​

ffuf -u http://FUZZ.target.com -w wordlist.txt -fs 0 -rate 20

4.3 dnsx​

cat wordlist.txt | dnsx -d target.com -silent -a -resp

4.4 amass (active)​

amass enum -active -d target.com -brute -w wordlist.txt -o subs.txt

4.5 Resolve found names​

while read sub; do
ip=$(dig +short "$sub" | head -1)
[ -n "$ip" ] && echo "$sub -> $ip"
done < subs.txt

5. VHost Fuzzing​

Same IP, different Host: header. Often the only way to find internal apps.

5.1 The concept​

A server at 10.10.10.10 may respond to:

  • Host: target.htb -> main site
  • Host: admin.target.htb -> admin panel
  • Host: staging.target.htb -> staging

None of these names need to be in public DNS.

5.2 Manual curl test​

curl -sI http://10.10.10.10 -H "Host: admin.target.htb"
curl -sI http://10.10.10.10 -H "Host: staging.target.htb"

# Compare:
curl -sI http://10.10.10.10 -H "Host: does-not-exist" | head -1

If the response is identical for every Host header, vhost fuzzing will not work (or needs a different filter).

5.3 gobuster vhost​

gobuster vhost -u http://target.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain

# Production: add rate limit
gobuster vhost -u http://target.htb -w wordlist.txt --append-domain --delay 100ms -t 5

Expected output:

Found: admin.target.htb Status: 200 [Size: 1234]
Found: staging.target.htb Status: 302 [Size: 0]

5.4 ffuf (vhost)​

ffuf -u http://10.10.10.10 -H "Host: FUZZ.target.htb" -w wordlist.txt -fs 0

# Filter by response size (default page size)
ffuf -u http://10.10.10.10 -H "Host: FUZZ.target.htb" -w wordlist.txt -fs 1234

# Match only 200/302
ffuf -u http://10.10.10.10 -H "Host: FUZZ.target.htb" -w wordlist.txt -mc 200,302

5.5 HTTPS vhosts (SNI-based)​

Some vhosts only respond on 443. Always test both.

curl -sI https://10.10.10.10 -H "Host: admin.target.htb" --insecure
ffuf -u https://10.10.10.10 -H "Host: FUZZ.target.htb" -w wordlist.txt -fs 0

5.6 Filtering noise​

Most vhost wordlists will return the default page for every miss. Filter by:

  • -fs <size> - filter by response size.
  • -fl <lines> - filter by line count.
  • -fw <words> - filter by word count.
  • -fc <codes> - filter out specific status codes.

First, determine the default response:

curl -sI http://10.10.10.10 -H "Host: random-string-12345"
# note status, size, and body

Then filter against it.

5.7 Alternative routing headers​

Some setups route by other headers, not just Host.

curl -sI http://target -H "X-Forwarded-Host: admin.target.htb"
curl -sI http://target -H "X-Forwarded-For: 127.0.0.1"
curl -sI http://target -H "X-Real-IP: 127.0.0.1"
curl -sI http://target -H "X-Original-URL: /admin"
curl -sI http://target -H "X-Rewrite-URL: /admin"

Useful when the main site is behind a reverse proxy that does host-based routing.


6. /etc/hosts Setup​

Add discovered vhosts to /etc/hosts so tools and browsers reach them.

Linux / macOS: /etc/hosts Windows: C:\Windows\System32\drivers\etc\hosts

# target.com vhosts
10.10.10.10 target.htb
10.10.10.10 admin.target.htb
10.10.10.10 staging.target.htb
10.10.10.10 api.target.htb

Linux command:

sudo tee -a /etc/hosts <<EOF
10.10.10.10 target.htb
10.10.10.10 admin.target.htb
EOF

Verify:

getent hosts admin.target.htb
curl -sI http://admin.target.htb

Windows (PowerShell, admin):

Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "10.10.10.10 target.htb"

6.1 Wildcard DNS handling​

If anything.target.com resolves, the target has wildcard DNS.

dig +short random-string-12345.target.com
# if it returns an IP, wildcard is active

Fix: filter out results that resolve to the wildcard IP.

WILDCARD_IP=$(dig +short random-string-12345.target.com | head -1)
while read sub; do
ip=$(dig +short "$sub" | head -1)
[ "$ip" != "$WILDCARD_IP" ] && [ -n "$ip" ] && echo "$sub -> $ip"
done < subs.txt

6.2 Alternative ports​

Do not forget vhosts may exist on:

  • 80, 443 (default)
  • 8080, 8443 (alternate HTTP/HTTPS)
  • 8000, 3000, 5000 (dev servers)
  • 9000 (PHP-FPM, admin panels)

Run vhost fuzzing against each open port.


7. Wordlists​

SecLists is the standard.

FileSizeUse case
/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt5kFast pass
/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt110kDeep pass
/usr/share/seclists/Discovery/DNS/dns-Jhaddix.txt2MExhaustive (slow)
/usr/share/seclists/Discovery/DNS/namelist.txt10kGeneral
/usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt220kVhost names from dir list

Custom wordlists: build from the target

  • Company name variations: acme, acme-dev, acme-staging, acme-old.
  • Project names, employee names (from LinkedIn).
  • Tech names: jenkins, gitlab, grafana, kibana, nagios.
cat /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \\
custom-words.txt | sort -u > combined.txt

8. What to Do With Findings​

Each discovered subdomain / vhost becomes a new target.

  1. Add to /etc/hosts (see §6).
  2. Fingerprint with curl -sI, whatweb (see Fingerprinting.md).
  3. Scan for directories with ffuf, feroxbuster, gobuster dir.
  4. Check for WAF (see WAF-Detection.md).
  5. Look for default creds / misconfigs on admin panels.
  6. Compare with the main site: sometimes a subdomain runs an older version.

9. Common Mistakes​

  • Not filtering wildcard DNS - thousands of false positives.
  • Only running subdomain enum without vhost fuzzing (misses internal apps).
  • Not checking HTTPS (443) separately from HTTP (80) - different vhosts.
  • Ignoring non-standard ports (8080, 8443).
  • Fuzzing with the wrong wordlist - use --append-domain in gobuster or set the Host header properly in ffuf.
  • Forgetting to add found vhosts to /etc/hosts - tools won't reach them.
  • Not checking response size differences - identical responses mean no vhost.

10. OSCP Notes​

IMPORTANT

OSCP report: screenshot vhost enumeration only if a discovered vhost is part of the exploitation path (e.g. admin panel leads to RCE).

Adding entries to /etc/hosts is exam-standard and does not require a screenshot unless it directly leads to a foothold.

See ../../methodology/OSCP-Reporting.md.


11. Cheatsheet​

Passive​

curl -s "https://crt.sh/?q=%25.target.com&output=json" | jq -r ".[].name_value" | sort -u
theHarvester -d target.com -b all
amass enum -passive -d target.com
subfinder -d target.com -silent

Active subdomain​

gobuster dns -d target.com -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -t 20
ffuf -u http://FUZZ.target.com -w wordlist.txt -fs 0
dnsx -d target.com -w wordlist.txt -a -resp

VHost fuzzing​

# Determine default response
curl -sI http://10.10.10.10 -H "Host: random-string"

# Fuzz
gobuster vhost -u http://target.htb -w wordlist.txt --append-domain
ffuf -u http://10.10.10.10 -H "Host: FUZZ.target.htb" -w wordlist.txt -fs <default-size>

# HTTPS
ffuf -u https://10.10.10.10 -H "Host: FUZZ.target.htb" -w wordlist.txt -fs 0

/etc/hosts​

echo "10.10.10.10 admin.target.htb" | sudo tee -a /etc/hosts

12. References​

See also: Fingerprinting.md, WAF-Detection.md, Ports-Services.md


Part of MethodMan. English only by design. Last updated: 2026-10-10.