A Practical Guide to ffuf
There is a moment in almost every web assessment where the visible application runs out of secrets. You have clicked every link, mapped every form, read the page source, and you are convinced there is more. There usually is. Old admin panels, backup files someone forgot to delete, staging endpoints, .git folders, API routes that never made it into the documentation. None of it is linked from anywhere. You have to go and find it.
That is what ffuf is for. The name stands for “Fuzz Faster U Fool”, which tells you two things: it is fast, and the person who wrote it has a sense of humour. It is a small Go program that does one thing extremely well: it takes a list of words, plugs each one into a spot in an HTTP request, fires them all at the target, and shows you which ones came back interesting.
I use it on nearly every engagement. This guide is how I actually run it, not a copy of the help menu.
Installing it
If you are on Kali or Parrot, ffuf is already there. On anything else with Go installed:
go install github.com/ffuf/ffuf/v2@latest
Or grab a pre-built binary from the project’s releases page and drop it in your path. There is no configuration file to fuss with and nothing to compile from scratch. Confirm it works:
ffuf -h
If you get the help output, you are ready. The only other thing you need is a wordlist, and for that everyone uses SecLists, a giant, well-organised collection of lists for exactly this kind of work. Clone it once and forget about it:
git clone https://github.com/danielmiessler/SecLists.git
On Kali it is often already at /usr/share/seclists.
Your first real scan
Here is the command I start with almost every time:
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-u https://target.com/FUZZ
Two things are doing the work. -w points at the wordlist. -u is the target URL, and the magic word FUZZ marks the spot where each word from the list gets inserted. So ffuf requests https://target.com/admin, https://target.com/backup, https://target.com/config, and so on down the entire list.
That FUZZ keyword is the whole idea behind the tool. It does not care what it is fuzzing. Put FUZZ in the path and you are discovering directories. Put it somewhere else and you are doing something else entirely. We will get to that.
Run it, and you will get a stream of results with status codes and response sizes. And almost immediately, you will hit the problem that trips up everyone new to this tool.
The noise problem, and how to fix it
Run that first scan and you might see hundreds of results, all returning 200 OK. Beginners get excited. Then they realise every single path “exists”, including obviously fake ones like asdfasdf and this-does-not-exist. The server is lying: it returns a friendly page for everything instead of a proper 404.
This is the single most important skill with ffuf: filtering out the responses that mean nothing. Once you can do it, the tool becomes genuinely useful. Until you can, it is just a firehose.
The trick is that fake pages almost always look identical to each other. Same status code, same byte size, same number of words. Real pages are different sizes. So you find the size of the “nothing here” response and tell ffuf to hide it.
Say every bogus path returns a response of exactly 4242 bytes. You filter that size out:
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-u https://target.com/FUZZ \
-fs 4242
-fs means “filter size”. Now the wall of identical junk vanishes and what is left is worth looking at. You have a small family of related flags:
-fsfilter by response size-fcfilter by status code (e.g.-fc 404,403)-fwfilter by word count-flfilter by line count
And their opposites, the match flags, when you want to only see certain responses:
-mcmatch status codes (ffuf defaults to matching 200, 204, 301, 302, 307, 401, 403, 405)-ms,-mw,-ml: match by size, words, lines
My habit is to run once with no filters, watch what repeats, then re-run filtering that value. Thirty seconds of looking saves you drowning in results.
Finding files, not just folders
Directories are half the job. The other half is files, and files have extensions. A wordlist of directory names won’t find backup.zip or config.php.bak on its own. Two ways to handle this.
Use a wordlist that already contains filenames, or append extensions yourself with -e:
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt \
-u https://target.com/FUZZ \
-e .php,.bak,.old,.zip,.txt,.config
Now for every word like admin, ffuf also tries admin.php, admin.bak, admin.old, and so on. Pick extensions that fit the target. If it is a PHP app, test .php, .phtml, .inc. If it is running on IIS, test .aspx, .asmx. Guessing .jsp on a static site is wasted requests.
The genuinely valuable finds here are backup and temporary files. Developers rename login.php to login.php.bak before a risky change and forget to remove it. The server no longer executes it: it hands you the raw source code, credentials and all. I have found live database passwords this way more times than I would like to admit on behalf of the industry.
Recursion: going deeper automatically
When ffuf finds /admin, there is probably a whole application sitting inside it. You could copy the URL and scan again manually, but ffuf can descend into discovered directories on its own:
ffuf -w wordlist.txt -u https://target.com/FUZZ \
-recursion -recursion-depth 2
-recursion tells it that when it finds a directory, it should queue a fresh scan inside it. -recursion-depth 2 stops it going more than two levels deep, which matters: without a limit, a big site can send ffuf off on a scan that runs for hours. I usually keep depth at 1 or 2 and go deeper by hand on the interesting branches.
A word of caution: recursion multiplies your request count fast. On a live target you can go from a few thousand requests to hundreds of thousands without noticing. Which brings us to the thing people forget.
Being considerate (and not getting blocked)
ffuf is fast enough to hurt a fragile server or get your IP blocked in the first ten seconds. By default it runs 40 concurrent threads, which is a lot. On a production target that is authorised but not robust, dial it down and add a delay:
ffuf -w wordlist.txt -u https://target.com/FUZZ \
-t 20 -p 0.1
-t sets the number of threads. -p adds a pause between requests (here 0.1 seconds; you can also give a range like -p 0.1-0.5 to look less robotic). If the target has a web application firewall or rate limiting, slowing down is often the difference between a clean scan and a blocked session. There is no prize for finishing in eleven seconds if you get banned at second twelve.
This restraint is not just politeness. It is part of testing responsibly. On a paid engagement, hammering a production system that a real business depends on can cause an outage, and that is your name on the incident. Match your aggression to what the scope allows.
Beyond directories: what FUZZ can really do
This is where ffuf pulls ahead of older tools like dirb. Because FUZZ is just a placeholder, you can put it anywhere in the request.
Subdomain brute-forcing. Put FUZZ in the Host header and feed it a list of subdomain names:
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u https://target.com \
-H "Host: FUZZ.target.com" \
-fs 0
This asks the server directly which virtual hosts it answers for. Pair it with the passive methods in the subdomain enumeration guide for fuller coverage, or run our Subdomain Finder tool first for a quick passive map before you touch the target at all.
Hidden parameters. APIs and legacy scripts often accept parameters that aren’t in any documentation, and a forgotten debug=true or admin=1 can be a real finding. Fuzz the query string:
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-u "https://target.com/page?FUZZ=test" \
-fs 4242
POST data. Move FUZZ into a request body and set the method to POST to test login fields, search parameters, or JSON values:
ffuf -w wordlist.txt -X POST \
-d "username=admin&password=FUZZ" \
-u https://target.com/login \
-H "Content-Type: application/x-www-form-urlencoded"
Same tool, same mental model, completely different job. Once the FUZZ idea clicks, you stop reaching for a different program every time the target changes shape.
Two options that save you time
A couple of flags turn ffuf from “works” into “works well”, and both are worth building into your habits.
Auto-calibration solves the noise problem automatically. Earlier we filtered junk by hand: run once, spot the repeating size, filter it. The -ac flag does that for you: before the real scan, ffuf sends a few requests for paths it knows cannot exist, learns what a “nothing” response looks like on this server, and filters it out on its own.
ffuf -w wordlist.txt -u https://target.com/FUZZ -ac
It is not perfect (a server that returns genuinely random responses can still slip through), but on most targets it saves you the manual calibration step entirely. I reach for it first and fall back to manual filtering only when a server is being awkward.
Saving your results matters more than people expect. A big scan can take a while, and losing the output because you closed the terminal is maddening. Write it to a file:
ffuf -w wordlist.txt -u https://target.com/FUZZ -o results.json -of json
-o sets the output file and -of sets the format (json is easy to process later, but html, csv and others exist). Saved output means you can come back to a scan, feed the interesting URLs into another tool, and keep a record of exactly what you found and when, which matters on a real engagement where your notes are part of the deliverable.
One more small habit: on a slow or flaky target, keep the request rate modest and save output, so a dropped connection halfway through does not cost you the whole run.
A workflow that holds up
Here is roughly how a content-discovery pass goes for me on an authorised target:
- Quick pass with a medium directory list, no filters, just to see how the server behaves and what a “nothing” response looks like.
- Set filters based on that: hide the junk size or code.
- Add extensions that match the tech stack and run again for files.
- Recurse one level into anything promising like
/admin,/api,/backup. - Pivot: anything interesting gets looked at by hand. ffuf finds the door; you decide whether to open it.
Then I feed the interesting endpoints into the rest of the toolkit: Burp Suite to intercept and manipulate the requests, and manual testing from there.
Common mistakes I still see
- Using one enormous wordlist for everything. A bigger list is not a better list. A focused, medium list you can actually review beats a million-line monster that takes an hour and buries the good stuff.
- Ignoring the response sizes. The size column is where the signal is. Learn to read it.
- Forgetting HTTPS and ports. If the target runs on
https://or a non-standard port, put it in the URL. ffuf does not guess. - Treating a 403 as a dead end. A
403 Forbiddenmeans something is there. The server just won’t let you see it directly. That is often more interesting than a 200. Note it and come back. - Running it on things you don’t have permission to test. This one is not a technique mistake, it is a career-ending one. Content discovery is loud and logged. Stay inside your scope, every time.
Where it fits
ffuf is a discovery tool, not an exploitation tool. Its job is to expand the map: to turn the handful of pages you can see into the full picture of what the server actually exposes. If you prefer a different tool for the same job, Gobuster covers similar ground, and the OWASP Web Security Testing Guide puts content discovery in the context of a full methodology. What you do with those endpoints is the real work, and it is covered across the rest of the blog: intercepting and tampering with requests in Burp, testing them for the OWASP Top 10, checking their security headers.
Master the filtering, respect the target, and ffuf will keep finding things long after the visible application has stopped giving them up. If you want to learn this properly with hands-on labs and a structured path, that is exactly what our training is built for.