Skip to content
HackIndex logo

HackIndex

Virtual Host Enumeration

5 min read Mar 30, 2026

Virtual host enumeration finds applications routed by hostname on the same IP. When a server returns different content depending on the Host header, there are likely multiple apps running — some not linked from the main site. Run this after tech fingerprinting confirms Host header sensitivity, or after subdomain bruteforce returns names resolving to the same IP.

Confirm Virtual Host Routing First

Before bruteforcing, verify the server responds differently to different Host headers:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP/ | wc -c
612
Explain command
-s Silent mode, suppresses progress and error messages.
-k Allow insecure SSL connections, skipping certificate verification.
http://$TARGET_IP/ Target URL with variable placeholder for the target IP address.
┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP/ -H "Host: $DOMAIN" | wc -c
14832
Explain command
-s Silent mode; suppresses progress and error output.
-k Allow insecure TLS connections, skipping certificate verification.
http://$TARGET_IP/ Target URL using the variable IP address.
-H "Host: $DOMAIN" Sets custom Host header to the specified domain variable.

Different response sizes confirm virtual host routing is active. If sizes match, the server either has no vhost routing or falls back to a default for unknown hosts — bruteforcing will still run but filter results need attention.

ffuf Virtual Host Bruteforce

ffuf injects wordlist entries into the Host header. The key is filtering out the default response size so you only see results that differ from the baseline:

┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://$TARGET_IP/ -H "Host: FUZZ.$DOMAIN" -mc 200,204,301,302,307,401,403
Explain command
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt Specifies the wordlist file used for fuzzing input.
-u http://$TARGET_IP/ Target URL where FUZZ keyword will be substituted during requests.
-H "Host: FUZZ.$DOMAIN" Sets a custom Host header, fuzzing subdomain portion with wordlist entries.
-mc 200,204,301,302,307,401,403 Match only responses with these specific HTTP status codes.

Filter the default response size to remove noise. Measure it first, then add -fs:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP/ -H "Host: invalid.invalid" | wc -c
612
Explain command
-s Silent mode; suppresses progress meter and error messages.
-k Allow insecure TLS connections, skipping certificate verification.
http://$TARGET_IP/ Target URL using the variable placeholder for the target IP address.
-H "Host: invalid.invalid" Sets a custom Host header to an invalid/non-existent hostname.
┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://$TARGET_IP/ -H "Host: FUZZ.$DOMAIN" -mc 200,204,301,302,307,401,403 -fs 612
dev                     [Status: 200, Size: 14832, Words: 1234]
admin                   [Status: 302, Size: 0, Words: 0]
Explain command
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt Wordlist file path used for fuzzing input.
-u http://$TARGET_IP/ Target URL with placeholder for the destination host.
-H "Host: FUZZ.$DOMAIN" Custom Host header; FUZZ keyword is replaced with each wordlist entry.
-mc 200,204,301,302,307,401,403 Match only responses with these HTTP status codes.
-fs 612 Filter out responses with a body size of exactly 612 bytes.

Any result with a size different from the baseline is a valid virtual host. Add the discovered names to /etc/hosts and browse them directly:

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP dev.$DOMAIN" | sudo tee -a /etc/hosts
┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP admin.$DOMAIN" | sudo tee -a /etc/hosts
Explain command
$TARGET_IP Variable placeholder for the target IP address to map.
dev.$DOMAIN Hostname constructed from the DOMAIN variable with 'dev.' subdomain.
admin.$DOMAIN Hostname constructed from the DOMAIN variable with 'admin.' subdomain.
-a Appends output to the file instead of overwriting it.

gobuster vhost Mode

gobuster has a dedicated vhost mode that also measures response differences. Useful as a second pass when ffuf results look incomplete:

┌──(kali㉿kali)-[~]
└─$ gobuster vhost -u http://$TARGET_IP/ -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt --append-domain -t 50
Explain command
vhost Mode to enumerate virtual hostnames via HTTP Host header fuzzing
-u Target base URL to use for vhost enumeration
http://$TARGET_IP/ Placeholder for the target IP address with HTTP scheme
-w Specifies the wordlist file path for brute-forcing vhosts
/usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt Wordlist containing top 20000 common subdomains
--append-domain Appends the base domain to each wordlist entry in Host header
-t Sets the number of concurrent threads for faster enumeration
50 Thread count value — 50 parallel goroutines used

The --append-domain flag appends the base domain to each wordlist entry, producing dev.example.com rather than just dev. Required for most modern setups.

HTTPS Virtual Hosts

For HTTPS targets, add -k to skip certificate validation and specify the full HTTPS URL:

┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u https://$TARGET_IP/ -H "Host: FUZZ.$DOMAIN" -mc 200,204,301,302,307,401,403 -fs 612 -k
Explain command
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt Wordlist file used for fuzzing input.
-u https://$TARGET_IP/ Target URL with $TARGET_IP as the host placeholder.
-H "Host: FUZZ.$DOMAIN" Sets Host header; FUZZ is replaced by wordlist entries, $DOMAIN is the base domain.
-mc 200,204,301,302,307,401,403 Match only responses with these HTTP status codes.
-fs 612 Filter out responses with a body size of 612 bytes.
-k Disable TLS/SSL certificate verification.

Non-Standard Ports

When the target runs on a non-standard port, specify it in both the URL and the Host header:

┌──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://$TARGET_IP:$PORT/ -H "Host: FUZZ.$DOMAIN:$PORT" -mc 200,204,301,302,307,401,403 -fs 612
Explain command
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt Wordlist file used for fuzzing subdomain values.
-u http://$TARGET_IP:$PORT/ Target URL with IP and port placeholders to fuzz against.
-H "Host: FUZZ.$DOMAIN:$PORT" Sets Host header, inserting wordlist entry as subdomain via FUZZ keyword.
-mc 200,204,301,302,307,401,403 Match only responses with these HTTP status codes.
-fs 612 Filter out responses with a body size of exactly 612 bytes.

After discovering virtual hosts, run content discovery separately against each vhost — they often have completely different directory structures and applications.

References