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 :W1 to 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 in FUZZ
  • Mode (-mode): with multiple wordlists, clusterbomb tries every combo, pitchfork pairs them line by line, sniper walks 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.

FlagDoesFlagDoes
-uTarget URL with FUZZ-mcMatch status codes (200,301,302)
-wWordlist (:W1 names it)-fcFilter out status codes (404)
-HAdd or fuzz a header-fsFilter out by response size
-dPOST body (fuzzable)-fwFilter out by word count
-XSet the HTTP method-flFilter out by line count
-eExtension list (.php,.txt)-mr / -frMatch / filter by body regex
-acAutocalibrate the miss response-recursionRecurse into found dirs
-modeclusterbomb / pitchfork / sniper-tThreads (default 40)
-pDelay between requests-rateCap requests per second
-o / -ofOutput file / format-replay-proxySend matches to a proxy
-rFollow redirects-requestReplay 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#

ColumnWhat it isHow to use it
StatusHTTP status code200, 301, and 403 are usually worth a look; the default matcher already keeps them
SizeResponse body in bytesThe main outlier signal; a size unlike the crowd is the hit
WordsWhitespace-separated tokensSurvives small dynamic changes better than size
LinesLine count of the bodyAnother stable filter when size drifts request to request
DurationTime to the responseA 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#

ProblemCauseFix
Thousands of identical hitsNo calibration or filterAdd -ac, or note the miss size and -fs it
No results at allMatcher or filter too strictDrop all filters, run once, then refine
FUZZ never substitutedKeyword missing from the requestPut FUZZ in the URL, a -H, or -d
Every path returns 200Catch-all page or SPA-ac, or filter by -fw / -fl instead of size
-fc 404 hides nothing usefulApp returns 200 for missesFilter by size or words, not status
TLS / certificate errorSNI or handshake, not the certffuf already ignores bad certs; set -sni HOST for HTTPS vhosts
Getting 429 or blockedToo fast for the target or WAFLower -t, add -p, cap -rate
Wordlist not foundWrong pathls /usr/share/seclists; install seclists
POST body ignoredMissing content typeAdd the matching -H 'Content-Type: ...'

Gotchas#

  • The URL must end in FUZZ for -recursion. ffuf recurses into the position it is discovering, so -recursion with FUZZ anywhere else silently runs a flat scan.
  • -ac calibrates 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 and FUZZ lands 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 curl or 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.

Find us elsewhere

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