wfuzz
A web fuzzer built around one idea: put the keyword FUZZ anywhere in a request (URL, header, POST body, cookie) and wfuzz substitutes each wordlist entry there. Older than ffuf, still all over HTB writeups. Check your installed build with wfuzz --version and inspect wfuzz --help before reusing options.
The model
wfuzz -w wordlist.txt [filters] <url-with-FUZZ>
Everything is “fire N requests, hide the boring ones.” You almost always start by hiding the baseline response so only interesting results remain.
| Flag | Meaning |
|---|---|
-w file | wordlist (repeat for multiple FUZZ, FUZ2Z, …) |
-z payload | payload generator (range, file, list — e.g. -z range,1-1000) |
-u url | target URL (or give it as the last arg) |
-H "Hdr: val" | add/override a header (put FUZZ here for vhosts) |
-d "body" | POST body (put FUZZ here for form/param fuzzing) |
-b "cookie" | cookie |
-X METHOD | HTTP method |
-t N | concurrent connections (default 10) |
-p ip:port:type | proxy (HTTP/SOCKS4/SOCKS5) |
-c | colorized output |
Hide / show filters (the important part)
| Hide | Show | Filters on |
|---|---|---|
--hc CODE | --sc CODE | HTTP status code |
--hl N | --sl N | number of lines |
--hw N | --sw N | number of words |
--hh N | --sh N | number of response size as reported by wfuzz |
--hs regex | --ss regex | text in the response body |
Run once with no filter, see what the uninteresting responses measure, then --hh/--hw/--hl that value away.
Recipes
# directory / file brute
wfuzz -c -z file,/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
--hc 404 http://10.10.10.5/FUZZ
# vhost / subdomain brute via Host header — hide the default-response size
wfuzz -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-H "Host: FUZZ.trick.htb" --hh 5480 http://10.10.10.5
# then narrow on a found prefix (HTB Trick):
wfuzz -c -w subdomains.txt -H "Host: preprod-FUZZ.trick.htb" --hh 5480 http://10.10.10.5
# GET parameter name discovery (HTB StreamIO)
wfuzz -c -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-H "Cookie: PHPSESSID=..." --hh 1678 "https://streamio.htb/admin/?FUZZ="
# POST body — login/param fuzz
wfuzz -c -w pass.txt -d "username=admin&password=FUZZ" --hs "Invalid" http://10.10.10.5/login
# two payloads at once (user x pass)
wfuzz -c -w users.txt -w pass.txt -d "user=FUZZ&pass=FUZ2Z" --hc 401 http://10.10.10.5/login
# numeric range (IDOR / id sweep)
wfuzz -c -z range,1-500 --hh 0 "http://10.10.10.5/item?id=FUZZ"
[!tip] vhost fuzzing needs the IP in the URL For
-H "Host: FUZZ.site.htb", point the URL at the IP (or the bare domain), notFUZZ.site.htb— you’re varying the Host header against one server, not resolving each name. Add every discovered vhost to/etc/hostsafterward.
[!tip] Calibrate, don’t guess A wildcard vhost or catch-all route returns a constant size for every miss. One probe (
wfuzz ... FUZZwith a clearly-bogus value) tells you that size;--hh <that>removes the noise. If sizes vary slightly, filter on--hw(words) or--hl(lines) instead of--hh.
[!warning] Watch out
- Zero displayed hits can mean every response was filtered; inspect an unfiltered control before concluding there are no candidates.
--hc 404alone is weak against apps that return 200 + a “not found” page; filter on size/words instead.- Use ffuf when its calibration/output model better suits the run; the equivalent filters are mapped below.
ffuf equivalents
| wfuzz | ffuf |
|---|---|
-w list FUZZ in URL | -w list -u http://host/FUZZ |
-H "Host: FUZZ.x" | -w list -u http://IP -H "Host: FUZZ.x" |
--hh 5480 | -fs 5480 (filter size) / -mc all -ac (auto-calibrate) |
--hc 404 | -fc 404 |
--hw 12 | -fw 12 |
# the StreamIO/Trick vhost brute, in ffuf:
ffuf -w subdomains.txt -u http://10.10.10.5 -H "Host: FUZZ.trick.htb" -mc all -ac
Build a baseline and retain the result
# Three misses reveal whether the wildcard response is stable
wfuzz -c -z list,missing_alpha-missing_beta-missing_gamma -H 'Host: FUZZ.domain.htb' http://10.10.10.5/
# Replace 5480 with the size measured above, not a value from another box
wfuzz -c -t 5 -s 0.2 -w subdomains.txt -H 'Host: FUZZ.domain.htb' --hh 5480 -f evidence-vhosts.json,json http://10.10.10.5/
# Verify a candidate against the same IP, outside the fuzzer
curl -i --resolve portal.domain.htb:80:10.10.10.5 http://portal.domain.htb/
Success: the candidate has repeatable behaviour distinct from multiple misses. A single size difference can be a timestamp, reflection or rate-limit page. Save the request shape and filter as well as the hits, so the result is reproducible.
For HTTPS, the Host header and TLS SNI are separate. A changed Host header cannot repair the wrong TLS virtual host; use a hostname URL with a deliberate connection mapping when testing that case. Avoid following redirects at first if their destination could leave the intended server.
Multiple payloads change the request count
# Two files produce combinations: endpoint × parameter name
wfuzz -c -t 3 -w endpoints.txt -w parameter-names.txt \
'http://app.example.test/FUZZ?FUZ2Z=lab'
With the default product iterator, 100 endpoints and 50 parameters create 5,000 requests. Inspect the planned payload sizes before starting. Paired credentials need a deliberately selected iterator and matching lists; otherwise a two-list login example can become a full combination attack.
Keep authentication stable during discovery
wfuzz -c -t 3 -s 0.2 -w parameter-names.txt \
-b 'session=<CURRENT_LAB_SESSION>' \
--hs 'Unknown parameter' \
'http://app.example.test/search?FUZZ=lab'
Measure the authenticated baseline first. If the session expires, the login redirect or unauthenticated page can look like a new hit for every payload. CSRF tokens can expire or be single-use; wfuzz does not automatically reconstruct arbitrary browser state for each request.
POST and JSON requests
wfuzz -c -t 3 -w field-names.txt \
-H 'Content-Type: application/json' \
-d '{"FUZZ":"lab-proof"}' http://app.example.test/api/search
Payload text is substituted into the request; it is not automatically JSON-escaped for arbitrary values. Use a wordlist appropriate to the selected field and check one generated request through your inspection proxy. A malformed JSON response measures parsing failure rather than hidden parameter behaviour.
Compare a hit with multiple controls
curl --resolve portal.example.test:80:192.0.2.20 \
--dump-header portal-headers.txt --output portal-body.txt \
http://portal.example.test/
curl -H 'Host: missing-alpha.example.test' \
--dump-header miss-headers.txt --output miss-body.txt http://192.0.2.20/
wc -c portal-body.txt miss-body.txt
Compare title, redirect destination, cookies and stable body content as well as length. A reflected Host value can change length without selecting a different site. Repeat the request after reducing concurrency if rate-limit pages appeared.
Failure diagnosis
| Observation | Next check |
|---|---|
| Every request is a hit | Wrong baseline, reflected input, expired auth or catch-all routing |
| Every response hidden | Rerun a known-good control without filters |
429 or increasing timeouts | Lower concurrency/delay and observe server behaviour |
| TLS site differs from HTTP Host | Use the intended hostname/SNI and connection mapping |
| Unexpected off-site redirect | Inspect Location before choosing to follow it |
Save payload lists, exact request shape, filters and result file. Name discovery is complete when a candidate produces repeatable behaviour; identifying a vulnerability in that endpoint is the next investigation.
See Also
- ffuf · feroxbuster · gobuster — other dir/vhost brute tools.
- Web Enumeration & Exploitation — where fuzzing sits in the web flow.