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
  • FUZZ keyword: 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 of file,WORDLIST, list,a-b-c (inline), or range,1-100 (numbers)
  • Multiple markers: FUZZ, FUZ2Z, FUZ3Z with one -z per marker fuzz several spots at once (username and password, say)
  • Filters: --hc/--hl/--hw/--hh hide by status code, line, word, or character count; --sc/--sl/--sw/--sh show 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 -z field, 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.

FlagDoesFlagDoes
-zPayload source (file/list/range)--hcHide these status codes
-wShortcut for -z file,WORDLIST--hhHide responses of N chars
-uTarget URL with FUZZ--hwHide responses of N words
-cColorized output--hlHide responses of N lines
-tConcurrent connections (default 10)--scShow only these codes (--sl/--sw/--sh too)
-sDelay (seconds) between requests--filterKeep rows matching an expression
-XHTTP method-dPOST body (put FUZZ in a field)
-HAdd / override a header-bSend a cookie
-pProxy ip:port-LFollow redirects
-RRecurse into found dirs to depth N-fSave 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.

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"
ColumnMeansRead it as
ResponseHTTP status200 exists, 301/302 redirect (often a real dir), 403 present but forbidden, 401 needs auth, 404 absent
L / W / ChLines / words / charsThe shape of the body; identical sizes across many hits usually mean a generic page
PayloadThe word triedThe 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#

ProblemCauseFix
command not foundNot installed / stale PATHReinstall; add ~/.local/bin to PATH; open a fresh shell
pycurl install errorMissing libcurl headersapt install libcurl4-openssl-dev libssl-dev, reinstall
Every line is 200Custom 404 pageBaseline one bogus word, then --hh/--hw by size, not --hc
No results at allFilter too strict / wrong URLLoosen filters; confirm FUZZ is in the request
Login fuzz never hitsWrong field nameCapture a real request first, match the body exactly
Connection refusedApp down / wrong portConfirm the port (scan with Nmap first)
SSL error on HTTPSCert validation via pycurlVerify the URL and target cert; check the scheme
Flags differ from a guideVersion driftCheck wfuzz --version and wfuzz -h

Gotchas#

  • FUZ2Z pulls from the second -z, not the first. Swap the marker order and you silently fuzz the wrong field with the wrong list.
  • --hh 0 hides empty bodies, not “hide nothing”. The number is a length, so --hh 0 drops 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.
  • -t is 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.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop