ffuf
ffuf (Fuzz Faster U Fool) swaps a wordlist into one marked spot of an HTTP request and shows how each response differs, so the row that stands out from the crowd is your finding. Reach for it to discover directories, hidden parameters, virtual hosts, or login values, anywhere a single field of a request varies and you want to see what changes. Fuzz only targets you own or are authorized to test; it is high volume and very visible in logs.
Mental model: ffuf -u URL/FUZZ -w wordlist [matchers/filters]. Put FUZZ anywhere in the request, then filter until only real hits remain.
New to fuzzing? Core concepts
- FUZZ keyword: the placeholder ffuf substitutes with each wordlist entry, for example
http://TARGET_IP/FUZZ. It can sit in the URL, a header, or the body - Wordlist (
-w): the list of words to try; add:W1to name it for multi-wordlist runs - Matchers (
-mc -ms -mw -ml -mr -mt): keep only responses that match a status, size, word count, line count, regex, or time - Filters (
-fc -fs -fw -fl -fr -ft): the inverse, drop responses that match - Autocalibration (
-ac): ffuf probes bogus paths, learns the “miss” response, and filters it for you - Recursion (
-recursion): queue a new scan inside each discovered directory; the URL must end inFUZZ - Mode (
-mode): with multiple wordlists,clusterbombtries every combo,pitchforkpairs them line by line,sniperwalks one list through several FUZZ spots - The columns: every response is scored by
status,size,words,lines,duration, and the outlier is the signal
When to reach for ffuf (and when not)
Reach for it to fuzz any single field of a request (a path, an extension, a parameter name or value, a Host header, a JSON field, a login field) with surgical status/size/word/line/regex filtering, to calibrate away a catch-all page, or to recurse into directories as you find them.
Reach for something else when you only need to hit a couple of known endpoints (curl), when plain path, vhost, or DNS discovery is enough and you do not need the filtering (gobuster, feroxbuster), or when you must render JavaScript and crawl a client-side app (a browser or a proxy crawler).
Install, update, verify#
sudo apt install -y ffuf # Debian / Ubuntu / Kali
sudo dnf install -y ffuf # Fedora
go install github.com/ffuf/ffuf/v2@latest # newest release -> ~/go/bin/ffuf
sudo apt install --only-upgrade -y ffuf # update (apt)
go install github.com/ffuf/ffuf/v2@latest # update (go)
ffuf -V # version (v1 and v2 differ; know which you have)
command -v ffuf # confirm it is on PATH
ffuf -h | less # full option list
Use apt for speed, Go when you want the newest release. Grab wordlists too: sudo apt install -y seclists puts them under /usr/share/seclists.
Flags you’ll actually use#
Skim these first; the cheat sheet below is these flags combined.
| Flag | Does | Flag | Does |
|---|---|---|---|
-u | Target URL with FUZZ | -mc | Match status codes (200,301,302) |
-w | Wordlist (:W1 names it) | -fc | Filter out status codes (404) |
-H | Add or fuzz a header | -fs | Filter out by response size |
-d | POST body (fuzzable) | -fw | Filter out by word count |
-X | Set the HTTP method | -fl | Filter out by line count |
-e | Extension list (.php,.txt) | -mr / -fr | Match / filter by body regex |
-ac | Autocalibrate the miss response | -recursion | Recurse into found dirs |
-mode | clusterbomb / pitchfork / sniper | -t | Threads (default 40) |
-p | Delay between requests | -rate | Cap requests per second |
-o / -of | Output file / format | -replay-proxy | Send matches to a proxy |
-r | Follow redirects | -request | Replay a raw request file |
Cheat sheet#
Where it helps, a block starts bare and adds one flag at a time, so you can stop at the line that does the job.
# Build up a directory scan: stop at the line you need
ffuf -u URL/FUZZ -w common.txt # 1. raw run, default matchers
ffuf -u URL/FUZZ -w common.txt -ac # 2. + autocalibrate the miss page
ffuf -u URL/FUZZ -w common.txt -ac -c # 3. + color, easier to scan
ffuf -u URL/FUZZ -w common.txt -ac -e .php,.html,.txt # 4. + try each word with extensions
ffuf -u URL/FUZZ -w common.txt -ac -recursion -recursion-depth 2 # 5. + dive into hits
# Match and filter (keep only real hits)
ffuf -u URL/FUZZ -w LIST -mc 200,301,302,403 # keep these statuses only
ffuf -u URL/FUZZ -w LIST -fc 404 # drop 404s
ffuf -u URL/FUZZ -w LIST -fs 1521 # drop the miss by size
ffuf -u URL/FUZZ -w LIST -fw 12 # drop the miss by word count
ffuf -u URL/FUZZ -w LIST -fl 40 # drop the miss by line count
ffuf -u URL/FUZZ -w LIST -fr 'Not Found' # drop by body regex
ffuf -u URL/FUZZ -w LIST -mt '<100' # keep fast responses only (ms)
# Parameter discovery
ffuf -u 'URL/page?FUZZ=1' -w params.txt -fs 0 # hidden GET parameter names
ffuf -u 'URL/item?id=FUZZ' -w values.txt -fc 404 # fuzz a parameter value
ffuf -u URL/api -X POST -w params.txt \
-H 'Content-Type: application/json' -d '{"FUZZ":"x"}' # hidden JSON field
# Vhost discovery (same IP, different Host header)
ffuf -u http://TARGET_IP/ -H 'Host: FUZZ.DOMAIN' -w subs.txt -ac
# POST body / login probing (authorized labs)
ffuf -u URL/login -X POST -w pw.txt \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=FUZZ' -fc 401 -mc 200,302
# Multiple wordlists (modes)
ffuf -u URL/W1/W2 -w dirs.txt:W1 -w files.txt:W2 # clusterbomb (every combo)
ffuf -u 'URL/?u=W1&p=W2' -w users.txt:W1 -w pw.txt:W2 -mode pitchfork # paired, line by line
# Replay a captured request from a file (FUZZ marked inside req.txt)
ffuf -request req.txt -request-proto https -w LIST -ac
# Rate control and stealth
ffuf -u URL/FUZZ -w LIST -t 20 -p 0.1 # fewer threads, small per-request delay
ffuf -u URL/FUZZ -w LIST -rate 50 # cap requests per second
# Save and parse
ffuf -u URL/FUZZ -w LIST -ac -o out.json -of json # machine-readable results
ffuf -u URL/FUZZ -w LIST -ac -s | tee hits.txt # silent, just the hits
Command breakdowns#
Each block below explains one situation, the exact command, and what to expect back.
Discover directories and files#
ffuf -u http://TARGET_IP/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt
The default matcher already keeps common hit codes (200s, 301, 302, 307, 401, 403, 405, 500). Read the status/size/words columns; a row that stands out from the crowd is your finding.
Auto-filter a catch-all “everything is 200” site#
ffuf -u http://TARGET_IP/FUZZ -w common.txt -ac
-ac requests a few known-bogus paths first, learns the miss response, and filters anything that matches it. Reach for it whenever a site answers every path with the same page.
Add file extensions to a path scan#
ffuf -u http://TARGET_IP/FUZZ -w common.txt -e .php,.html,.txt,.bak
-e appends each extension to every word, so admin is tried as admin, admin.php, admin.html, and so on. This is how you catch config.bak or login.php next to a bare directory name.
Filter out the miss response by size#
ffuf -u http://TARGET_IP/FUZZ -w LIST -fs 1521
When -ac misjudges, filter by hand: run once, note the size that repeats on every miss, then -fs it. -fw (words) and -fl (lines) work the same way and often survive dynamic content better than raw size.
Find hidden GET parameters#
ffuf -u 'http://TARGET_IP/page?FUZZ=1' -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -fs 0
FUZZ sits in the parameter name. A response whose size differs from the baseline means the app noticed that parameter. Quote the URL so the shell does not eat the ? and &.
Fuzz a parameter’s value#
ffuf -u 'http://TARGET_IP/item?id=FUZZ' -w ids.txt -fc 404
Now FUZZ is the value, not the name. Useful for IDOR-style ID sweeps or testing which tokens the app accepts. Filter the not-found code so only live values remain.
Discover virtual hosts by Host header#
ffuf -u http://TARGET_IP/ -H 'Host: FUZZ.DOMAIN' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -ac
Same IP, different Host: values. The default site answers with one size; -ac filters it, and whatever remains is a real name-based vhost. Add the winner to /etc/hosts to browse it.
Fuzz a JSON field in a POST body#
ffuf -u http://TARGET_IP/api/login -X POST \
-H 'Content-Type: application/json' \
-d '{"user":"admin","pass":"FUZZ"}' -w pw.txt -fc 401
FUZZ works inside any body. Set the JSON content type or the API ignores the payload. Watch for a status or size change that separates a hit from the 401 wall.
Probe a login form in a lab#
ffuf -u http://TARGET_IP/login -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin&password=FUZZ' -w pw.txt -fc 401 -mc 200,302
Form-encoded instead of JSON. A 302 to a dashboard or a different size usually means success. Authorized labs only, and confirm any hit by hand.
Recurse into discovered directories#
ffuf -u http://TARGET_IP/FUZZ -w common.txt -ac -recursion -recursion-depth 2
When ffuf finds a directory it queues a fresh scan inside it, down to the depth you set. The URL must end in FUZZ. Depth 2 is usually plenty; deeper gets slow and noisy fast.
Combine two wordlists (clusterbomb vs pitchfork)#
ffuf -u http://TARGET_IP/W1/W2 -w dirs.txt:W1 -w files.txt:W2 # every combo
ffuf -u 'http://TARGET_IP/?u=W1&p=W2' -w users.txt:W1 -w pw.txt:W2 -mode pitchfork # line by line
Default clusterbomb tries every W1 by W2 pair; pitchfork walks both lists in lockstep (line 1 with line 1, line 2 with line 2). sniper takes a single wordlist and fuzzes multiple FUZZ positions one at a time.
Replay a captured request from a file#
ffuf -request req.txt -request-proto https -w LIST -ac
Save a real request from Burp (right-click, “Copy to file”), mark the spot with FUZZ, and ffuf reuses its exact headers, cookies, and body. The cleanest way to fuzz a request you already know works.
Match or filter by response body regex#
ffuf -u http://TARGET_IP/FUZZ -w LIST -mr 'admin|dashboard'
ffuf -u http://TARGET_IP/FUZZ -w LIST -fr 'Not Found'
-mr keeps responses whose body matches the pattern, -fr drops them. Use this when status and size are identical across responses but the body text gives the hit away.
Slow down to stay under a rate limit#
ffuf -u http://TARGET_IP/FUZZ -w LIST -t 20 -p 0.1 -rate 50
-t lowers concurrency, -p adds a per-request delay (a range like 0.1-1.0 randomizes it), and -rate caps requests per second. Reach for these on fragile targets or when a WAF starts returning 429.
Save results as JSON and parse with jq#
ffuf -u http://TARGET_IP/FUZZ -w LIST -ac -o hits.json -of json
jq -r '.results[].url' hits.json
-of also writes html, md, csv, and all, but JSON is the one you parse: pull URLs, statuses, or sizes straight into your notes.
Send matched requests to a proxy for inspection#
ffuf -u http://TARGET_IP/FUZZ -w LIST -mc 200 -replay-proxy http://127.0.0.1:8080
Only matched hits get replayed through Burp or mitmproxy, so the proxy history stays clean instead of holding thousands of misses. Use -x instead if you want every request to route through the proxy.
Reading the output#
| Column | What it is | How to use it |
|---|---|---|
Status | HTTP status code | 200, 301, and 403 are usually worth a look; the default matcher already keeps them |
Size | Response body in bytes | The main outlier signal; a size unlike the crowd is the hit |
Words | Whitespace-separated tokens | Survives small dynamic changes better than size |
Lines | Line count of the body | Another stable filter when size drifts request to request |
Duration | Time to the response | A slow outlier can mean a real backend query (a blind-injection hint) |
When every row shares one Size, Words, and Lines, the app has a catch-all: calibrate with -ac or filter, do not trust the flood.
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
| Thousands of identical hits | No calibration or filter | Add -ac, or note the miss size and -fs it |
| No results at all | Matcher or filter too strict | Drop all filters, run once, then refine |
FUZZ never substituted | Keyword missing from the request | Put FUZZ in the URL, a -H, or -d |
| Every path returns 200 | Catch-all page or SPA | -ac, or filter by -fw / -fl instead of size |
-fc 404 hides nothing useful | App returns 200 for misses | Filter by size or words, not status |
| TLS / certificate error | SNI or handshake, not the cert | ffuf already ignores bad certs; set -sni HOST for HTTPS vhosts |
| Getting 429 or blocked | Too fast for the target or WAF | Lower -t, add -p, cap -rate |
| Wordlist not found | Wrong path | ls /usr/share/seclists; install seclists |
| POST body ignored | Missing content type | Add the matching -H 'Content-Type: ...' |
Gotchas#
- The URL must end in
FUZZfor-recursion. ffuf recurses into the position it is discovering, so-recursionwith FUZZ anywhere else silently runs a flat scan. -accalibrates once per scan, not per directory. When each folder has a different miss page, use-ach(per host) or filter each recursion level by words or lines instead of size.- Size filters are brittle. A CSRF token, timestamp, or reflected value shifts the byte count every request;
-fw(words) or-fl(lines) is steadier than-fs. - The default User-Agent is
Fuzz Faster U Fool. WAFs fingerprint it instantly; override with-H 'User-Agent: ...'when testing how an app reacts, never to dodge authorization you do not have. - Quote any URL with
?,&, or spaces. Unquoted, the shell splits it andFUZZlands somewhere you did not intend. - A hit is a lead, not a finding. A different size means “the app noticed”, not “vulnerable”; confirm every hit by hand with
curlor a browser.
Pairs with#
nmap finds the open web port before you fuzz it. Capture a known-good request in Burp Suite, drop it into a file, and let ffuf replay it with -request, or push matches back with -replay-proxy. jq turns the JSON output into clean lists for your notes, and curl confirms each hit as a single clean request. When you only need plain path, vhost, or DNS discovery without ffuf’s filtering, gobuster or feroxbuster (recursion-first) are simpler, and wfuzz is the older keyword-based ancestor you still see in writeups.