October 19, 2026
Finding Virtual Hosts with ffuf: A Practical Guide
What is FFUF?

By MainEkHacker
5 min read
Ffuf (Fuzz Faster U Fool) can discover virtual hosts (vhosts) hiding behind a single IP address by fuzzing the Host header instead of the URL path useful for finding staging, admin, or internal subdomains that share infrastructure with a public site.
Now What vhost fuzzing is; Many servers host several websites on one IP address. Which site you get back depends on the Host header your browser sends in the request this is how shared hosting and content delivery networks serve different domains from the same box.
A vhost that isn't linked anywhere public (an admin panel, a staging build, an internal API named internal.target.com or staging.target.com) still resolves if you ask for it by name, even though no link on the public site points to it. Vhost fuzzing means sending the same request to the same IP over and over, swapping in a different Host value each time, and watching for a response that differs from the default "nothing here" page. A different response a different page title, a different size, a login form instead of a 404 means you found something real.
Prerequisites • Install ffuf. go install github.com/ffuf/ffuf/v2@latest, or grab a prebuilt binary from its GitHub releases page, or install it via your distro's package manager (e.g. apt install ffuf on Kali). • Get a wordlist. SecLists is the standard choice. A good vhost list is SecLists/Discovery/DNS/subdomains-top1million-5000.txt (or the 20000/110000 variants for deeper coverage). Clone it with git clone https://github.com/danielmiessler/SecLists.git. • Know the target's IP. Vhost fuzzing works by hitting one IP address directly and varying the Host header, so resolve the target's IP first (dig +short target.com or host target.com). • Authorization. Only run this against a target you own or have explicit written permission to test . The basic command ffuf -u https://TARGET_IP -H "Host: FUZZ.target.com" -w subdomains-top1million-5000.txt FlagMeaning : -u https://TARGET_IP The request goes straight to the server's IP (or its base URL), not to a name the Host header is what decides which site answers. -H "Host: FUZZ.target.com" The custom header being fuzzed. FUZZ is ffuf's placeholder — it gets replaced with each wordlist entry, so admin, staging, dev, etc. become admin.target.com, staging.target.com, and so on. -w subdomains-top1million-5000.txt The wordlist supplying the guesses.
If you prefer hitting the domain name directly rather than a bare IP (useful when the target sits behind Cloudflare or another CDN and you don't have the origin IP), swap -u for the domain itself and ffuf will still vary only the Host header, not the URL. Filtering out false positives Many servers respond identically no matter what Host you send a catch all "default site" page. Without filtering, ffuf reports that page as a hit for every single word in your list, burying the real finds. First, send one request with an obviously-fake Host value and note its response size and word count that's your baseline for "nothing here": ffuf -u https://TARGET_IP -H "Host: thisdoesnotexist123.target.com" -w <(echo test) Then filter that size out of the real run with -fs (filter size) or -fw (filter words): ffuf -u https://TARGET_IP -H "Host: FUZZ.target.com" -w subdomains-top1million-5000.txt -fs 4242 Now only responses that differ from the baseline size show up those are your real candidates. Confirming a real vhost For each candidate ffuf surfaces, check manually before treating it as a finding: 1. Request it directly with curl, forcing the Host header the same way: curl -s -H "Host: admin.target.com" https://TARGET_IP -o out.html 2. Open out.html and look for something that genuinely differs from the default site — a login form, a different title, an API response, internal branding. 3. Watch for soft matches some apps return a 200 with a generic "not found" page for every vhost, which looks real until you compare page content, not just status code. 4. If it's a redirect, follow it (curl -sL) to see where it actually lands some vhosts just bounce back to the main site. Useful extra flags What it does -t 40 Threads raise for speed, lower if the target rate-limits or you want to stay quiet. -p 0.1 Delay (seconds) between requests pairs well with low threads for a stealthier scan. -mc 200,301,302 Match only these status codes instead of filtering by size. -fc 400,404 Filter out specific status codes. -of json -o results.json Save results in JSON so you can parse or diff them later. -recursion For path fuzzing (not vhosts) recurse into discovered directories automatically. -sa Stop on all error cases (useful if the target starts blocking you and you don't want to burn the whole wordlist). Skip retyping the command… If you don't want to re-type the flags every time, a small wrapper script can build the command for you: ./fuffwrap.sh vhost -u https://target.com -w subdomains-top1million-5000.txt
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
OUT_DIR="./fuff_results"
DEFAULT_DIR_WORDLIST="/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt"
DEFAULT_VHOST_WORDLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt"
THREADS=40
usage() {
cat <<EOF
$SCRIPT_NAME — simple wrapper around ffuf
MODES
vhost Fuzz the Host header to discover virtual hosts on a target
dir Fuzz paths/directories on a target
REQUIRED
-u URL Target base URL, e.g. https://target.com
OPTIONAL
-w WORDLIST Wordlist to use (defaults depend on mode, see below)
-t THREADS Number of threads (default: $THREADS)
-x EXT Comma separated extensions for dir mode, e.g. php,html,txt
-o NAME Base name for output files (default: derived from URL+timestamp)
-h Show this help
Anything after "--" is passed straight through to ffuf untouched,
so you can still use one-off flags without editing the script.
DEFAULT WORDLISTS (edit the variables at the top of this script to change)
dir mode: $DEFAULT_DIR_WORDLIST
vhost mode: $DEFAULT_VHOST_WORDLIST
EXAMPLES
$SCRIPT_NAME dir -u https://target.com -x php,txt,bak
$SCRIPT_NAME vhost -u https://target.com -w my_subdomains.txt
$SCRIPT_NAME dir -u https://target.com -- -mc 200,301,302 -recursion
EOF
}
require_ffuf() {
if ! command -v ffuf >/dev/null 2>&1; then
echo "Error: ffuf is not installed or not on PATH. Install it first (e.g. 'go install github.com/ffuf/ffuf/v2@latest' or your package manager)." >&2
exit 1
fi
}
MODE="${1:-}"
if [[ "$MODE" != "vhost" && "$MODE" != "dir" ]]; then
usage
exit 1
fi
shift
URL=""
WORDLIST=""
EXT=""
OUTNAME=""
EXTRA_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-u) URL="$2"; shift 2 ;;
-w) WORDLIST="$2"; shift 2 ;;
-t) THREADS="$2"; shift 2 ;;
-x) EXT="$2"; shift 2 ;;
-o) OUTNAME="$2"; shift 2 ;;
-h) usage; exit 0 ;;
--) shift; EXTRA_ARGS=("$@"); break ;;
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "$URL" ]]; then
echo "Error: -u <target URL> is required." >&2
usage
exit 1
fi
require_ffuf
mkdir -p "$OUT_DIR"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
HOST_SLUG="$(echo "$URL" | sed -E 's~https?://~~; s~[^a-zA-Z0-9.]+~_~g')"
[[ -z "$OUTNAME" ]] && OUTNAME="${HOST_SLUG}_${MODE}_${TIMESTAMP}"
JSON_OUT="${OUT_DIR}/${OUTNAME}.json"
if [[ "$MODE" == "vhost" ]]; then
[[ -z "$WORDLIST" ]] && WORDLIST="$DEFAULT_VHOST_WORDLIST"
if [[ ! -f "$WORDLIST" ]]; then
echo "Error: wordlist not found: $WORDLIST (pass one with -w)" >&2
exit 1
fi
# Strip scheme to get bare host for building the Host header value
BASE_HOST="$(echo "$URL" | sed -E 's~https?://~~; s~/.*$~~')"
echo "[*] Running vhost discovery against $URL"
echo "[*] Wordlist: $WORDLIST"
echo "[*] Output: $JSON_OUT"
echo
ffuf -u "$URL" \
-H "Host: FUZZ.${BASE_HOST}" \
-w "$WORDLIST" \
-t "$THREADS" \
-of json -o "$JSON_OUT" \
"${EXTRA_ARGS[@]}"
elif [[ "$MODE" == "dir" ]]; then
[[ -z "$WORDLIST" ]] && WORDLIST="$DEFAULT_DIR_WORDLIST"
if [[ ! -f "$WORDLIST" ]]; then
echo "Error: wordlist not found: $WORDLIST (pass one with -w)" >&2
exit 1
fi
TARGET_URL="${URL%/}/FUZZ"
CMD=(ffuf -u "$TARGET_URL" -w "$WORDLIST" -t "$THREADS" -of json -o "$JSON_OUT")
[[ -n "$EXT" ]] && CMD+=(-e ".${EXT//,/,.}")
echo "[*] Running directory/path discovery against $URL"
echo "[*] Wordlist: $WORDLIST"
echo "[*] Output: $JSON_OUT"
echo
"${CMD[@]}" "${EXTRA_ARGS[@]}"
fi
echo
echo "[*] Done. Results saved to: $JSON_OUT"
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
OUT_DIR="./fuff_results"
DEFAULT_DIR_WORDLIST="/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt"
DEFAULT_VHOST_WORDLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt"
THREADS=40
usage() {
cat <<EOF
$SCRIPT_NAME — simple wrapper around ffuf
MODES
vhost Fuzz the Host header to discover virtual hosts on a target
dir Fuzz paths/directories on a target
REQUIRED
-u URL Target base URL, e.g. https://target.com
OPTIONAL
-w WORDLIST Wordlist to use (defaults depend on mode, see below)
-t THREADS Number of threads (default: $THREADS)
-x EXT Comma separated extensions for dir mode, e.g. php,html,txt
-o NAME Base name for output files (default: derived from URL+timestamp)
-h Show this help
Anything after "--" is passed straight through to ffuf untouched,
so you can still use one-off flags without editing the script.
DEFAULT WORDLISTS (edit the variables at the top of this script to change)
dir mode: $DEFAULT_DIR_WORDLIST
vhost mode: $DEFAULT_VHOST_WORDLIST
EXAMPLES
$SCRIPT_NAME dir -u https://target.com -x php,txt,bak
$SCRIPT_NAME vhost -u https://target.com -w my_subdomains.txt
$SCRIPT_NAME dir -u https://target.com -- -mc 200,301,302 -recursion
EOF
}
require_ffuf() {
if ! command -v ffuf >/dev/null 2>&1; then
echo "Error: ffuf is not installed or not on PATH. Install it first (e.g. 'go install github.com/ffuf/ffuf/v2@latest' or your package manager)." >&2
exit 1
fi
}
MODE="${1:-}"
if [[ "$MODE" != "vhost" && "$MODE" != "dir" ]]; then
usage
exit 1
fi
shift
URL=""
WORDLIST=""
EXT=""
OUTNAME=""
EXTRA_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-u) URL="$2"; shift 2 ;;
-w) WORDLIST="$2"; shift 2 ;;
-t) THREADS="$2"; shift 2 ;;
-x) EXT="$2"; shift 2 ;;
-o) OUTNAME="$2"; shift 2 ;;
-h) usage; exit 0 ;;
--) shift; EXTRA_ARGS=("$@"); break ;;
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "$URL" ]]; then
echo "Error: -u <target URL> is required." >&2
usage
exit 1
fi
require_ffuf
mkdir -p "$OUT_DIR"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
HOST_SLUG="$(echo "$URL" | sed -E 's~https?://~~; s~[^a-zA-Z0-9.]+~_~g')"
[[ -z "$OUTNAME" ]] && OUTNAME="${HOST_SLUG}_${MODE}_${TIMESTAMP}"
JSON_OUT="${OUT_DIR}/${OUTNAME}.json"
if [[ "$MODE" == "vhost" ]]; then
[[ -z "$WORDLIST" ]] && WORDLIST="$DEFAULT_VHOST_WORDLIST"
if [[ ! -f "$WORDLIST" ]]; then
echo "Error: wordlist not found: $WORDLIST (pass one with -w)" >&2
exit 1
fi
# Strip scheme to get bare host for building the Host header value
BASE_HOST="$(echo "$URL" | sed -E 's~https?://~~; s~/.*$~~')"
echo "[*] Running vhost discovery against $URL"
echo "[*] Wordlist: $WORDLIST"
echo "[*] Output: $JSON_OUT"
echo
ffuf -u "$URL" \
-H "Host: FUZZ.${BASE_HOST}" \
-w "$WORDLIST" \
-t "$THREADS" \
-of json -o "$JSON_OUT" \
"${EXTRA_ARGS[@]}"
elif [[ "$MODE" == "dir" ]]; then
[[ -z "$WORDLIST" ]] && WORDLIST="$DEFAULT_DIR_WORDLIST"
if [[ ! -f "$WORDLIST" ]]; then
echo "Error: wordlist not found: $WORDLIST (pass one with -w)" >&2
exit 1
fi
TARGET_URL="${URL%/}/FUZZ"
CMD=(ffuf -u "$TARGET_URL" -w "$WORDLIST" -t "$THREADS" -of json -o "$JSON_OUT")
[[ -n "$EXT" ]] && CMD+=(-e ".${EXT//,/,.}")
echo "[*] Running directory/path discovery against $URL"
echo "[*] Wordlist: $WORDLIST"
echo "[*] Output: $JSON_OUT"
echo
"${CMD[@]}" "${EXTRA_ARGS[@]}"
fi
echo
echo "[*] Done. Results saved to: $JSON_OUT"
What one line expands into the full ffuf -u … -H "Host: FUZZ.…" -w … -of json -o … command behind the scenes, with sensible defaults for threads and output location, and still lets you pass any extra ffuf flag after for oneoff runs.
Stay in scope: Vhost fuzzing can surface internal systems that were never meant to be public treat anything you find as sensitive. Run this only against targets you own or are explicitly authorized to test, stay inside the scope a bug bounty program or client engagement defines, and keep logs of what you ran and when. Scanning infrastructure without authorization can be illegal even when the technique itself is harmless.
Thankyou For Reading:)