Web Fuzzing Round Up

Web fuzzing. The brute force way of web reconnaissance.

So here's what fuzzing actually is: you take a URL and just start throwing words at it to see what sticks. The server talks back with status codes: 404 for "nope doesn't exist," 403 for "exists but you're not allowed" (which honestly tells you more than you'd think), and 200 for "here you go mate, help yourself."

And those numbers? They're basically the server snitching on itself. That admin panel someone thought was secure because the URL is /definitely_not_the_admin_panel_trust_me? The backup folder with last month's database dump just sitting there? API endpoints that were "just for internal testing"? Fuzzing finds all of it.

I learned this through HTB and honestly, it's ruined me as a developer. Now every time I build something I'm like "okay but what if someone fuzzes this?" (Spoiler: they will. They have wordlists for days.)

Directory and File Fuzzing

This is the foundation: checking what folders and files exist on a server. You're not sitting there typing /admin, /backup, /test, /totally_not_secret_stuff one by one like some kind of masochist. That's what wordlists are for.

SecLists is your friend here. It's basically every common directory name, file name, and path that has ever existed, compiled into text files. Then you point a tool at a server and let it go crazy.

I use ffuf for this because it's fast and saying "ffuf" out loud makes me sound way more technical than I actually am.

Here's what a basic directory fuzz looks like:

ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt \
     -u http://IP:PORT/FUZZ

What's happening here (because remember, computers start counting at zero and naming things is the second hardest problem in CS):

  • -w is your wordlist: the big list of words that'll get thrown at the URL
  • -u is the target you're testing
  • FUZZ is where the magic happens: it gets replaced with each word from the list

Run that and you'll see something like:

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

 :: Method           : GET
 :: URL              : http://IP:PORT/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/...
 :: Threads          : 40
 :: Matcher          : Response status: 200-399

admin                   [Status: 301, Size: 0, Words: 1, Lines: 1]
backup                  [Status: 200, Size: 1337, Words: 42, Lines: 8]
old_site                [Status: 403, Size: 278, Words: 20, Lines: 10]
test_db_FINAL_v3        [Status: 200, Size: 4567, Words: 234, Lines: 89]

Each line is the server basically telling on itself. 301? Redirect, probably a directory. 200? Exists and you can see it. 403? Exists but access denied, which is almost more interesting because why protect something if it's supposedly not there? (Yeah, exactly.)

From the developer side: This has made me paranoid in ways I didn't know were possible. That /api/debug endpoint I left in "just for testing"? Found it. That .env.backup file I thought was clever? Found it. The entire /test directory from when I was figuring out how authentication works? You guessed it: found it.

I once fuzzed a project I built in second year. Found a file called passwords_DO_NOT_COMMIT.txt. In production. It was committed. I committed it. Six months prior. (The fuzzer didn't judge me but I judged myself pretty hard.)

Want to hunt for specific file types? Because developers love leaving .bak, .old, and .zip files everywhere like breadcrumbs:

ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt \
     -u http://IP:PORT/FUZZ \
     -e .php,.html,.txt,.bak,.zip,.old,.swp

That -e flag just appends those extensions to each word. So now you're checking config.php, config.bak, config.old, config_BACKUP_USE_THIS.zip: all the greatest hits of "things I created at 3am and forgot existed."

Parameter and Value Fuzzing

APIs are just functions that live on the internet, right? And functions take parameters. Parameter fuzzing is figuring out what inputs an API actually accepts, including the ones nobody documented (because who has time to write docs at 2am when the deploy is in 6 hours).

Say you find an endpoint: http://IP:PORT/api/user?id=1

Cool. But what else might it accept? role? isAdmin? pleaseLetMeIn? adminMode? (Yes I've seen adminMode as a parameter. Yes it worked. No I can't make this up.)

ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
     -u http://IP:PORT/api/user?FUZZ=test \
     -fs 0

That -fs 0 filters responses with 0 size (usually error pages), so you only see the hits that matter.

But finding parameters is only half the battle. What about the values?

ffuf -w /usr/share/seclists/Usernames/top-usernames-shortlist.txt \
     -u http://IP:PORT/api/user?username=FUZZ \
     -mc 200

Now you're testing actual usernames. -mc 200 means "only show me successful responses." Suddenly you've enumerated every valid user on the system without anyone handing you a list. (This is how I learned input validation isn't optional, it's mandatory.)

Developer wake-up call: I built an API once where I validated required fields (because I'm not a complete monster) but forgot about optional ones. Someone could literally inject {"role":"admin","permissions":"all"} into a user creation endpoint and my code just went "oh okay cool, here's admin access" with zero validation.

The API accepted it. Created the user. Made them admin. Because I never checked if those fields should even exist in that context.

Fuzzing is that friend who asks "but why though?" about every single assumption until you realize your entire security model is held together by hope and duct tape.

Virtual Host and Subdomain Fuzzing

Fun fact: servers can host multiple websites on the same IP. They figure out which site you want based on the Host header in your HTTP request. Which means there could be entire subdomains just... sitting there. Hiding. Waiting to be found.

Virtual host fuzzing finds those hidden sites.

ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
     -u http://IP \
     -H "Host: FUZZ.example.com" \
     -fs 1234

What's happening:

  • -H "Host: FUZZ.example.com" sets a custom Host header for each request
  • -fs 1234 filters the default error page size (you figure this out by trying a garbage subdomain first)

This is how you find gems like:

  • dev.example.com (zero authentication, full access to everything)
  • staging.example.com (same database as production because "we'll fix that later")
  • admin.example.com (just... sitting there. Open to the internet.)
  • test123.example.com (someone's entire dev environment from 2 years ago)

I once found a subdomain called boitumelo-testing-DO-NOT-USE. It was very much in use. By production. With real user data. (To be clear, that wasn't mine. But it could have been. I've made similar mistakes.)

All because someone thought "if we don't link to it, nobody will find it."

Narrator: They found it. Using a 30-second ffuf command.

Filtering Fuzzing Output

Real talk: raw fuzzing output is LOUD. You'll get thousands of 404s, error pages, redirects to the same place, empty responses that mean nothing. Filtering is how you find the actual interesting stuff buried in all that noise.

Filter by status code:

ffuf -w wordlist.txt -u http://IP/FUZZ -mc 200,301,302

This says "only show me 200 (success), 301 (permanent redirect), and 302 (temporary redirect)." Everything else? Hidden.

Filter by response size:

ffuf -w wordlist.txt -u http://IP/FUZZ -fs 1234

"Hide anything that's exactly 1234 bytes." Error pages usually have consistent sizes, so once you know that size, you can filter them all out in one go.

Filter by word count:

ffuf -w wordlist.txt -u http://IP/FUZZ -fw 42

Same concept, different metric. "Hide responses with exactly 42 words."

My actual workflow: Run a quick test first with like 10 words. Look at the error responses. Note their size or word count. Then re-run the full wordlist with -fs or -fw to filter those out. What's left is usually the good stuff.

But here's the cheat code:

ffuf -w wordlist.txt -u http://IP/FUZZ -ac

Auto-calibration. ffuf sends a few requests with garbage paths, learns what the "nothing here lol" response looks like, and filters it automatically. This has saved me so many hours of staring at response sizes at midnight doing mental math (I'm terrible at mental math).

Recursive Fuzzing

You found /admin. Congrats! Achievement unlocked.

But what if there's /admin/backup? And what if that has /admin/backup/2024? And what if that has /admin/backup/2024/january/DO_NOT_DELETE?

You could manually fuzz each directory. Spend your entire weekend typing commands. Question every life choice that led you to this moment.

OR. Or. You use recursion and let the computer do what it does best: repetitive tasks without complaining or needing snack breaks.

ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt \
     -u http://IP:PORT/FUZZ \
     -recursion \
     -recursion-depth 2 \
     -e .html,.php \
     -v

Breaking it down:

  • -recursion enables recursive mode (revolutionary naming, I know)
  • -recursion-depth 2 limits how deep it goes (depth 4+ will run until the heat death of the universe, trust me)
  • -e .html,.php checks for files with these extensions at every level
  • -v is verbose so you can actually see what's happening instead of staring at a blank terminal wondering if it froze

How it works: ffuf uses a queue. Finds /admin? Adds to queue. Fuzzes /admin/FUZZ. Finds /admin/backup? Queue. Fuzzes /admin/backup/FUZZ. Rinse and repeat until it hits your depth limit or runs out of directories.

Two strategies exist: Depth-First Search (goes deep before going wide) or Breadth-First Search (completes each level before descending). ffuf uses BFS by default, which is usually what you want because it gives you a better overview faster. (Also because I said so. My blog so I do what I want. I'm playing I'm playing.)

Developer confession time: I recursively fuzzed an old university project once. Found a three-level-deep directory structure I had completely forgotten about. Inside? Database dumps. From two years ago. With plaintext passwords. Admin credentials. And notes that said "TODO: fix this security hole before deploying."

I never fixed it. I just forgot it existed. The fuzzer found it in 30 seconds.

Recursive fuzzing is humbling in ways I wasn't emotionally prepared for.

Validating Findings

Finding something is step one. Figuring out if it actually matters is step two.

Got a 200 response? Don't just do a victory dance: actually visit the URL. Sometimes it's a blank page. Or a redirect loop. Or a "coming soon" message from 2019 that's still coming soon.

Got a 403? Try different HTTP methods:

ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt \
     -u http://IP/admin/FUZZ \
     -X POST,PUT,DELETE

I've seen servers that block GET but happily accept POST. Or allow DELETE to anyone because someone copy-pasted CORS config from Stack Overflow without reading it. (That someone may have been me once. Twice. No further questions.)

Found an interesting file? Download it and look inside:

curl http://IP/backup.zip -o backup.zip
unzip backup.zip
ls -la

Check what's actually in there. Config files with database creds? Source code with hardcoded API keys? A README that literally says "DO NOT DEPLOY THIS TO PRODUCTION" that was definitely deployed to production?

This is where fuzzing stops being discovery and starts being "oh no we need to fix this immediately before someone else finds it."

API Fuzzing

APIs are developer-to-developer communication. Which means they're often less polished than user-facing stuff. Less "does this look pretty" and more "does it work when I test it once at 3am yes okay ship it."

Which makes them excellent fuzzing targets.

API fuzzing combines everything: directory fuzzing, parameter fuzzing, value fuzzing, into one beautiful chaotic mess.

Finding endpoints:

ffuf -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
     -u http://IP/api/FUZZ \
     -mc 200,500

Why include 500 errors? Because 500 responses are CHATTY. They leak stack traces. Database queries. Internal file paths. Framework versions. Server configurations. It's like the server is having a breakdown and oversharing everything.

Fuzzing HTTP methods:

ffuf -w methods.txt -u http://IP/api/users -X FUZZ

Where methods.txt is just:

GET
POST
PUT
DELETE
PATCH
OPTIONS
HEAD

Sometimes GET is locked down tight but PUT is wide open because someone tested the happy path and called it a day. (Not naming names. Okay fine, it was me. In my defense, it was finals week.)

Fuzzing JSON parameters:

ffuf -w params.txt \
     -u http://IP/api/endpoint \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"FUZZ":"test"}' \
     -mc 200

This tests which JSON fields the API accepts. Including the ones that definitely shouldn't work but do anyway because input validation is hard and we're all tired and deadlines exist.

Why This Actually Matters (From Someone Who's Been Humbled)

Here's the thing about fuzzing that hit me like a truck: It's not elite hacker magic. It's just systematically checking every assumption you made while coding.

You assumed nobody would find that backup directory because it has a GUID in the name? Wordlists contain GUIDs.

You assumed your API only receives the five parameters you documented? Fuzzing tests every common parameter in existence.

You assumed that dev. subdomain is safe because you didn't link to it? Fuzzing doesn't need links. Doesn't need hints. Doesn't need anything except time and a wordlist.

Learning fuzzing has genuinely made me a better developer because it forced me to stop thinking like someone building features and start thinking like someone actively trying to break them. And that shift changes everything.

Now when I code, I ask myself:

Does this need authentication? If yes, add it. Don't rely on "nobody will guess this URL." They will. They have automation. They have patience. They have wordlists with millions of entries.

Am I validating every input? Not just the required ones. Not just the ones I expect. Every single input. Reject unknowns. Sanitize everything. Assume every parameter is actively trying to hurt you (because it might be).

What happens if someone finds this? If the answer is "oh no," then it shouldn't be there. Delete old directories. Disable debug endpoints in prod. Remove test files. Assume everything you create will eventually be found, because it will be.

This isn't paranoia. This is pattern recognition after watching fuzzing find:

  • My own forgotten API keys
  • My own test endpoints with god-mode privileges
  • My own "temporary" admin accounts from six months ago that were very much not temporary
  • My own backup files with database credentials in plain text

Fuzzing is like speedrunning the question "what could go wrong?" And the answer is always, always, "more than you initially thought, and also that thing you forgot about from last Tuesday."

So now when I build stuff, I fuzzing-proof it. Or at least I try. Because someone will eventually point ffuf at whatever I build. Might as well be prepared for it.

Build things that are actually secure, not just obscure. Because obscurity isn't security: it's just security theater with extra steps that don't work.

(Also fuzzing is kind of fun once you get past the existential dread of finding your own vulnerabilities. There's something deeply satisfying about watching ffuf systematically map an entire application in seconds. Makes me feel all hackery. Just me? Cool cool cool.)

This whole journey has taught me that security is transparent but unbreakable, not hidden and hoping. You can't hide your way to security. You have to build it properly from the ground up.

And honestly? That's way more exciting than trying to come up with clever directory names.

#FromBoituWithCode #BoitusWeeklyUpdates

Comments

Peer Pressure (What other's liked reading)