Wfuzz
Wfuzz replaces a FUZZ marker in a request with every word from a payload and sends one request per word, so you can brute a path, a query parameter, a POST field, a header, or a cookie with the same tool. Reach for it when the target varies somewhere and you want to see how each value responds, then filter the boring answers away.
Mental model: wfuzz [options] -z PAYLOAD -u "URL/FUZZ". Put FUZZ where the request should vary; everything else filters the answers.
New to fuzzing? Core concepts
FUZZkeyword: the marker Wfuzz swaps for each payload word. Drop it anywhere the request can change:/FUZZ,?id=FUZZ, a header, or a body field- Payload (
-z): where words come from, one offile,WORDLIST,list,a-b-c(inline), orrange,1-100(numbers) - Multiple markers:
FUZZ,FUZ2Z,FUZ3Zwith one-zper marker fuzz several spots at once (username and password, say) - Filters:
--hc/--hl/--hw/--hhhide by status code, line, word, or character count;--sc/--sl/--sw/--shshow only those - Response fields: each row reports status code (
C), lines (L), words (W), characters (Ch), and the payload word - Encoders: transform payloads inline with a third
-zfield, for example-z file,WORDLIST,urlencode - Concurrency (
-t): connections in flight, default 10; more is faster and louder - False positives: a “found” page can be a custom 404 that returns 200, so filter by size, not just code
When to reach for Wfuzz (and when not)
Reach for it when you need to fuzz an arbitrary part of a request: multiple markers, header or cookie values, POST bodies, or number ranges, with rich filtering and inline encoders. It shines when the job is not plain directory discovery but “vary this one spot and show me what changes.”
Reach for something else when you only need fast path/file discovery (ffuf, feroxbuster), recursive content trees (feroxbuster), simple dir/DNS/vhost modes (gobuster), or GUI-driven request control (Burp Intruder). Wfuzz development has slowed, so most people run ffuf for everyday content discovery and keep Wfuzz for its flexibility.
Install, update, verify#
pipx install wfuzz # preferred: isolated env, easy upgrades
pip install --user wfuzz # per-user install
sudo apt install -y wfuzz # Debian / Ubuntu / Kali (older, quick)
sudo apt install -y libcurl4-openssl-dev libssl-dev # if the pycurl build fails
pipx upgrade wfuzz # update (or: pip install --user -U wfuzz)
wfuzz --version # confirm the version
command -v wfuzz # confirm it is on PATH
wfuzz -e encoders # list built-in encoders (quick self-test)
Wfuzz is a Python package that depends on pycurl; a failed install is almost always missing libcurl headers, not Wfuzz itself. If command -v prints nothing after a pip --user install, add ~/.local/bin to your PATH or open a fresh shell.
Flags you’ll actually use#
Skim these first; the cheat sheet below is these flags combined.
| Flag | Does | Flag | Does |
|---|---|---|---|
-z | Payload source (file/list/range) | --hc | Hide these status codes |
-w | Shortcut for -z file,WORDLIST | --hh | Hide responses of N chars |
-u | Target URL with FUZZ | --hw | Hide responses of N words |
-c | Colorized output | --hl | Hide responses of N lines |
-t | Concurrent connections (default 10) | --sc | Show only these codes (--sl/--sw/--sh too) |
-s | Delay (seconds) between requests | --filter | Keep rows matching an expression |
-X | HTTP method | -d | POST body (put FUZZ in a field) |
-H | Add / override a header | -b | Send a cookie |
-p | Proxy ip:port | -L | Follow redirects |
-R | Recurse into found dirs to depth N | -f | Save output name,format |
Cheat sheet#
Every block starts simple and adds one flag at a time, so you can stop at the line that does the job.
# Build up a scan: stop at the line you need
wfuzz -z file,WORDLIST -u "http://TARGET_IP/FUZZ" # 1. raw, every result
wfuzz -z file,WORDLIST --hc 404 -u "http://TARGET_IP/FUZZ" # 2. + hide 404 noise
wfuzz -c -z file,WORDLIST --hc 404 -u "http://TARGET_IP/FUZZ" # 3. + color
wfuzz -c -t 20 -z file,WORDLIST --hc 404 -u "http://TARGET_IP/FUZZ" # 4. + faster
# Payload sources
wfuzz -z file,WORDLIST -u "http://TARGET_IP/FUZZ" # words from a wordlist
wfuzz -w WORDLIST -u "http://TARGET_IP/FUZZ" # same, shorthand for -z file
wfuzz -z list,admin-login-backup -u ".../FUZZ" # inline words (dash-separated)
wfuzz -z range,1-500 -u "http://TARGET_IP/item?id=FUZZ" # numeric range
# Fuzz different parts of the request
wfuzz -c -z file,WL --hc 404 -u "http://TARGET_IP/FUZZ" # path
wfuzz -c -z range,1-500 --hc 404 -u "http://TARGET_IP/?id=FUZZ" # param VALUE
wfuzz -c -z file,WL --hh 0 -u "http://TARGET_IP/?FUZZ=test" # param NAME
wfuzz -c -z file,WL -X POST -d "user=admin&pass=FUZZ" -u ".../login" # POST field
wfuzz -c -z file,WL -H "User-Agent: FUZZ" --hc 404 -u ".../" # header value
wfuzz -c -z file,WL -H "Host: FUZZ.DOMAIN" --hw 290 -u "http://TARGET_IP/" # vhost
wfuzz -c -z file,WL -b "session=VALUE" --hc 404 -u ".../FUZZ" # with a cookie
# Filter the noise
wfuzz -c -z file,WL --hc 404,403 -u ".../FUZZ" # hide by code (multiple ok)
wfuzz -c -z file,WL --hh 1256 -u ".../FUZZ" # hide by char count (custom 404)
wfuzz -c -z file,WL --hw 12 -u ".../FUZZ" # hide by word count
wfuzz -c -z file,WL --sc 200,301 -u ".../FUZZ" # show ONLY these codes
wfuzz -c -z file,WL --filter "c=200 and w>50" -u ".../FUZZ" # expression filter
# Multiple markers
wfuzz -c -z file,users.txt -z file,pass.txt \
-X POST -d "username=FUZZ&password=FUZ2Z" --hc 200 -u ".../login"
# Encode payloads on the fly
wfuzz -c -z file,WL,urlencode --hc 404 -u ".../search?q=FUZZ" # url-encode each word
wfuzz -c -z file,WL,base64 -u ".../?token=FUZZ" # base64 each word
# Auth, proxy, redirects, recursion, pace
wfuzz -c -z file,WL --basic USER:PASS --hc 404 -u ".../FUZZ" # HTTP basic auth
wfuzz -c -z file,WL -p 127.0.0.1:8080 --hc 404 -u ".../FUZZ" # through Burp
wfuzz -c -z file,WL -L --hc 404 -u ".../FUZZ" # follow redirects
wfuzz -c -z file,WL -R 2 --hc 404 -u ".../FUZZ" # recurse dirs depth 2
wfuzz -c -z file,WL -t 10 -s 0.2 --hc 404 -u ".../FUZZ" # quieter pace
# Save results
wfuzz -c -z file,WL --hc 404 -f output/run.json,json -u ".../FUZZ" # JSON to file
Command breakdowns#
Each block below explains one situation, the exact command, and what to expect back.
Discover hidden directories and files#
wfuzz -c -z file,WORDLIST --hc 404 -u "http://TARGET_IP/FUZZ"
One request per word, --hc 404 drops the “not found” wall so only real pages show. Each surviving line is a lead: note its code and size. This is the everyday content-discovery run.
Fuzz an app that returns 200 for everything#
wfuzz -c -z list,definitely-not-real -u "http://TARGET_IP/FUZZ" # baseline one bogus word
wfuzz -c -z file,WORDLIST --hh 1256 -u "http://TARGET_IP/FUZZ" # then hide that size
When the app serves a custom 404 with a 200 code, --hc is useless. Baseline one impossible word to learn the fake-404 length (say 1256 Ch), then --hh 1256 hides every response that matches the wallpaper.
Fuzz a query parameter value#
wfuzz -c -z range,1-500 --hc 404 -u "http://TARGET_IP/item?id=FUZZ"
range,1-500 walks the numbers 1 through 500 through the id value. A row whose size differs from the rest is worth a manual look, even if its status code matches the noise.
Discover parameter names#
wfuzz -c -z file,WORDLIST --hh 0 -u "http://TARGET_IP/page?FUZZ=test"
Here FUZZ sits in the parameter name, not the value. --hh 0 hides zero-length bodies; a changed response size means the page reacted to that parameter, so it quietly accepts it.
Brute force a login form (fuzz the password)#
wfuzz -c -z file,WORDLIST -X POST \
-d "username=USERNAME&password=FUZZ" \
--hc 200 -u "http://TARGET_IP/login"
-d sends the POST body, FUZZ rides the password field. Hiding the 200 “login failed” page means a different code or size stands out as a candidate. Authorized targets only, and watch for lockouts.
Fuzz two fields at once#
wfuzz -c -z file,users.txt -z file,passwords.txt \
-X POST -d "username=FUZZ&password=FUZ2Z" \
--hc 200 -u "http://TARGET_IP/login"
FUZZ pulls from the first -z, FUZ2Z from the second. By default Wfuzz tries every combination (product), so a small user list times a small password list stays manageable.
Discover virtual hosts#
wfuzz -c -z file,WORDLIST -H "Host: FUZZ.DOMAIN" \
--hw 290 -u "http://TARGET_IP/"
Same IP, varying Host header. Baseline the default response word count first, then --hw 290 hides it; a different word count is a likely name-based vhost the server treats specially.
Fuzz a header value#
wfuzz -c -z file,WORDLIST -H "User-Agent: FUZZ" \
--hc 404 -u "http://TARGET_IP/"
Any header can carry FUZZ. Useful when an app behaves differently per client string, or when you suspect a header (X-Forwarded-For, Referer) changes access.
Fuzz behind an authenticated cookie#
wfuzz -c -z file,WORDLIST -b "session=VALUE" \
--hc 404 -u "http://TARGET_IP/FUZZ"
-b attaches a session cookie to every request, so you enumerate the logged-in area instead of the public one. Grab a valid session value from the browser or a proxy first.
URL-encode payloads on the fly#
wfuzz -c -z file,WORDLIST,urlencode --hc 404 \
-u "http://TARGET_IP/search?q=FUZZ"
The third -z field is an encoder. urlencode escapes spaces and special characters before sending, which matters for query values; base64 and md5 are also built in.
Keep only real hits with an expression filter#
wfuzz -c -z file,WORDLIST --filter "c=200 and w>50" \
-u "http://TARGET_IP/FUZZ"
--filter combines fields with and/or: c (code), l (lines), w (words), h (chars). This keeps 200s with more than 50 words, cutting thin decoy pages in one expression instead of stacking --hc/--hw.
Route requests through Burp or mitmproxy#
wfuzz -c -z file,WORDLIST -p 127.0.0.1:8080 \
--hc 404 -u "http://TARGET_IP/FUZZ"
-p sends every request through your proxy, so each one lands in the history for inspection and replay. Handy for confirming exactly what Wfuzz sent.
Follow redirects to the real response#
wfuzz -c -z file,WORDLIST -L --hc 404 \
-u "http://TARGET_IP/FUZZ"
-L follows each 3xx so you judge the final page, not just the redirect. Without it a directory that 301s to a trailing slash looks like a bare redirect instead of real content.
Recurse into discovered directories#
wfuzz -c -z file,WORDLIST -R 2 --hc 404 \
-u "http://TARGET_IP/FUZZ"
When Wfuzz finds a directory it re-fuzzes inside it, up to depth 2 here. Great for nested trees, but it multiplies the request count fast, so keep the wordlist and depth modest.
Save machine-readable output for notes#
wfuzz -c -z file,WORDLIST --hc 404 \
-f "output/dirs-$(date +%Y%m%d-%H%M).json,json" \
-u "http://TARGET_IP/FUZZ" 2>&1 | tee notes/dirs.txt
-f name,json writes parseable results while tee keeps a colorized copy on screen. Record the exact filter you settled on so a re-run matches.
Reading the output#
Wfuzz prints one row per request. The columns that matter are the status code, and the line / word / char sizes.
ID Response Lines Word Chars Payload
000001: 200 45 L 120 W 1256 Ch "index"
000002: 301 0 L 0 W 0 Ch "admin"
| Column | Means | Read it as |
|---|---|---|
Response | HTTP status | 200 exists, 301/302 redirect (often a real dir), 403 present but forbidden, 401 needs auth, 404 absent |
L / W / Ch | Lines / words / chars | The shape of the body; identical sizes across many hits usually mean a generic page |
Payload | The word tried | The value that produced this row |
A row that differs in size from the crowd is the interesting one, even when its code matches the noise. Empty results usually mean the wrong FUZZ placement or an over-aggressive filter, not that the app is clean.
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
command not found | Not installed / stale PATH | Reinstall; add ~/.local/bin to PATH; open a fresh shell |
pycurl install error | Missing libcurl headers | apt install libcurl4-openssl-dev libssl-dev, reinstall |
Every line is 200 | Custom 404 page | Baseline one bogus word, then --hh/--hw by size, not --hc |
| No results at all | Filter too strict / wrong URL | Loosen filters; confirm FUZZ is in the request |
| Login fuzz never hits | Wrong field name | Capture a real request first, match the body exactly |
Connection refused | App down / wrong port | Confirm the port (scan with Nmap first) |
| SSL error on HTTPS | Cert validation via pycurl | Verify the URL and target cert; check the scheme |
| Flags differ from a guide | Version drift | Check wfuzz --version and wfuzz -h |
Gotchas#
FUZ2Zpulls from the second-z, not the first. Swap the marker order and you silently fuzz the wrong field with the wrong list.--hh 0hides empty bodies, not “hide nothing”. The number is a length, so--hh 0drops zero-length responses; to disable a filter, remove it.- The wordlist must match the marker’s job. A directory list dropped into a parameter-name slot finds nothing; use a parameter wordlist there.
-tis speed and noise at once. The default 10 is usually fine; cranking it can knock over a fragile lab app and trip a WAF on request rate.
Pairs with#
nmap confirms which HTTP/HTTPS ports are open before you fuzz them, and whatweb or nikto fingerprint the stack so you know what to fuzz. seclists supplies the wordlists behind every -z file, and curl captures a real request to copy into -d, then verifies each hit by hand. Reach for ffuf or feroxbuster when the job is plain, fast directory discovery; keep Wfuzz for multi-marker, header, and cookie fuzzing with rich filtering.