You land on an internal AI service. Some FastAPI app running on a random port, backing a chatbot, a RAG pipeline, an agent framework, who knows yet. No documentation link, no README. First question is always the same: what's actually here?
Modern APIs built on FastAPI love to expose a self documenting openapi.json file. If you find it, you get the full endpoint list for free, every path, every method, every request body, straight from the app itself. Problem is, that only works if you already know the file is there, and if whoever built the thing didn't lock it down. So before I go looking for the spec, I go looking for the app itself. That's where forced browsing comes in.
Feroxbuster brute forces a wordlist of likely paths against a target and records how the server responds to each. It's the same idea as gobuster or dirb, just faster and with better filtering built in. The catch is that a standard web wordlist, things like admin, login, backup.zip, wp-content, mostly comes back empty against an AI backed API. That's not where the interesting paths live.
AI apps have their own conventions. OpenAI compatible services expose things like /v1/chat/completions and /v1/embeddings. LangServe apps expose /invoke, /batch, /stream, /playground. LiteLLM proxies expose their own admin surface, things like /key/generate, /spend/logs, /model/info. And RAG based apps almost always have some kind of ingestion endpoint, usually something like /kb/add or /upload, where documents get fed into the vector store.
So the first move was building a wordlist tuned to that structure instead of reusing a generic one. I used Claude to pull real route conventions from the OpenAI API reference, the LangServe repo, and the LiteLLM docs, and put together an ai-api-endpoints.txt I could point feroxbuster at.
Flag breakdown: -u is the target base URL. -w is the wordlist to fuzz with. -x json,yaml appends those extensions to every word in the list, so it also tries things like openapi.json and config.yaml on top of the plain paths. --filter-status 404 hides plain misses from the output so the terminal stays readable.
Link extraction is on by default too. Once feroxbuster lands on something like /docs or openapi.json, it parses the response body for more paths and queues them up automatically. That turned out to matter a lot here, more on that below.
786 requests, 17 hits, 2 seconds. Fast, and worth actually reading properly instead of just skimming for 200s.
200 OK means the endpoint exists and answered GET successfully. Here that's /, /health, /openapi.json, /docs, /redoc, the FastAPI boilerplate, confirming the spec is live and both auto generated doc UIs are reachable.
Two entries stand out from the boilerplate though: /logs/latest and /logs/last-tool-call. Those aren't default FastAPI routes, they're custom to this app, and "last-tool-call" is a strong hint this thing does function or tool calling internally. An unauthenticated logs endpoint on an agentic app is worth digging into, it can leak prompts, tool arguments, or reasoning traces you were never supposed to see.
405 Method Not Allowed is the one people misread as a miss when it's actually the opposite. A 405 means the route exists and is registered on the server, it just doesn't accept GET. Most real application logic in a REST API lives behind POST, so scanning with GET only will turn up a pile of 405s on exactly the endpoints that matter. In this scan that's /chat, /session/new, /reset, /browse, /review, /summarize, /kb/add, /upload, eight confirmed, live, functional endpoints, mapped out without sending a single POST. /kb/add in particular is almost certainly the RAG ingestion route, the one you'd target for document injection or poisoning testing later.
307 Temporary Redirect shows up on /docs/ redirecting to /docs. Just FastAPI normalizing the trailing slash. Not a finding by itself, but always worth a glance in case a redirect points somewhere you didn't expect.
404 Not Found is filtered out of the output entirely, since it just means nothing's there.
Here's the thing worth calling out: none of /chat, /session/new, /reset, /browse, /review, /summarize, /kb/add, /upload, /logs/latest, or /logs/last-tool-call were actually in my wordlist. They came from link extraction. Once feroxbuster reached /openapi.json, the file that lists every real route the app has, it parsed it and queued up everything it found. The wordlist got the door open, link extraction did the rest.
Forced browsing plus link extraction did the whole job here. Feroxbuster didn't just find that openapi.json existed, it read it and handed back the app's real, functional endpoints without me ever touching the spec by hand. That's the payoff: one scan, one command, and the attack surface is mapped.
This workflow maps directly onto the reconnaissance phase of OffSec's OSAI (AI-300) course. Before you can test prompt injection, RAG poisoning, or jailbreaks, you need to know what's actually exposed. Same principle as AD recon before you have credentials: don't rush it, the phase before you can attack anything is the one that decides how effective everything after it is.