
Your port scan comes back with 80 and 8000. Nothing in it says "AI", yet the company has a chatbot. You'll find it in the front-end JavaScript, and from there the endpoint itself will tell you it runs Llama 3.2 on Ollama.
This is the first post in an active recon series on AI apps. In four steps you'll pull the AI endpoint out of a chat widget's config, read the model and server from its reply, fingerprint the API gateway on the second port and map its routes using nothing more than status codes. Every command runs against a practice lab called NovaTech, and you can repeat it on any app you're allowed to test.
Why a port scan misses AI services
Most AI features aren't a separate service on their own port. The model server, often Ollama on port 11434 or a hosted API, sits on an internal network. The web app calls it for you and hands back the answer.
So from outside you only see the web server. To find the AI part, you look where the app tells the browser how to reach it: the JavaScript config files, the API endpoints and the HTTP headers. If you missed the headers, the passive recon post covers those.
Step 1: Find the chat widget in the JavaScript
Grab the home page and list every script it loads:
curl -s http://<target>/ | grep -iE "<script"
grep -iE searches without caring about case and lets you use a regex. Two scripts, and chat-widget.js is the obvious one to read first:
curl -s http://<target>/js/chat-widget.js
Everything in this file runs in every visitor's browser, so "Internal Use Only" is wishful thinking. Here's what it hands you:
assistantEndpoint: /api/v2/assistantis the AI route you'll hit next, andapiBase: /api/v2is where the rest of the API lives.enableAI: trueconfirms the feature's switched on.debugMode: falsemeans debug is off for now, so it's worth checking if the server accepts it being turned on.legacySupport: truehints that older API versions still answer, so try/api/v1/assistanttoo.timeout: 30000is 30 seconds per request, which tells you how long the backend's allowed to think.
Don't stop at the widget. Grep the other scripts for paths and secrets:
curl -s http://<target>/js/main.js | grep -iE "api|endpoint|key|token|model"
Step 2: Call the AI endpoint and read its metadata
Send the assistant a harmless message in the shape the widget would use:
curl -s -X POST http://<target>/api/v2/assistant \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}' | jq .
The JS told you where the AI lives. The reply tells you what it is. provider: ollama is the inference server, the program that loads and runs the model. model: llama3.2:1b is Meta's Llama 3.2 at 1 billion parameters, a small model.
The other fields are Ollama's own, passed straight through. prompt_eval_count is input tokens and eval_count is output tokens. The _duration fields are in nanoseconds, so divide by a billion to get seconds.
Do the sums and they hold up. Loading took 0.096 s, reading the prompt 0.068 s and writing 18 tokens 0.327 s (about 55 tokens a second). That's 0.491 s, close to the 507 ms latency_ms. Real counts and timings that add up mean a real model answered. A canned reply wouldn't have them.
Look at prompt_eval_count: 26 too. "Hello" plus Llama's chat template is only a handful of tokens, so 26 leaves room for a line or two at most. There's little or no system prompt on this route. Compare that with the 297 tokens for the same "Hello" in the passive recon post.
Step 3: Read the API gateway headers
Back to the second port. A full TCP scan is what turned up 8000:
nmap -p- --min-rate 1000 <target>
Port 8000 is a partner API gateway. Gateways sit in front of backend services and tend to stamp their own headers on every reply. Ask for just the headers on one of its routes:
curl -sI http://<target>:8000/v1/billing
Server and Via both say Kong 3.9.1, a popular open-source API gateway, and the version you'll check for known issues. The X-Kong-* headers are Kong's own: upstream latency is how long the backend took (10 ms), and proxy latency is Kong's own time (14 ms).
The rate limit headers matter for how you test. 60 requests a minute, 59 left, reset in 9 seconds. Blast a wordlist at it and you'll be locked out in a minute, so slow your tools to about one request a second.
Step 4: Map endpoints with 401 vs 404 status codes
You don't need credentials to find out which routes exist. The status code tells you:
| Code | Meaning | Exists? |
|---|---|---|
| 200 | Open, no auth needed | Yes |
| 401 | Needs auth | Yes |
| 403 | Blocked for you | Yes |
| 405 | Wants a different method (try POST) | Yes |
| 404 | Nothing there | No |
Loop over a few likely paths and print only the code:
for endpoint in auth billing chat/completions models users; do
code=$(curl -s -o /dev/null -w "%{http_code}" http://<target>:8000/v1/$endpoint)
echo "/v1/$endpoint - HTTP $code"
done
-o /dev/null throws the body away and -w "%{http_code}" prints just the status code.
/v1/chat/completions is OpenAI's route name, so behind this gateway there's either a proxy to OpenAI or an OpenAI-compatible model server. It's real, and it's locked. Meanwhile /v1/auth and /v1/billing answer anyone, which is worth a look of its own on a partner API. Confirm the lock:
curl -si http://<target>:8000/v1/chat/completions
-i prints headers and body together. It comes back 401 Unauthorized, so it wants an Authorization header. You can't use it yet, but you now know an AI endpoint sits behind the gateway and what it'll need.
Grow the loop as you go. Add embeddings, completions, admin, health and keys, and keep it under the 60-a-minute limit.
What the four steps found
/api/v2/assistant, legacy on
Ollama, llama3.2:1b, tiny prompt
Kong 3.9.1, 60 req/min
/v1/chat/completions 401
Two AI routes, one open and one behind auth, a known model and server, and a gateway version with its rate limit. That's the map the rest of your testing hangs off. The open assistant is ready for prompt injection tests now, and once you've got a key for the gateway route, check what tools the model can call.
If it doesn't work
No script tags show up. Single-page apps often load one bundle, like /static/js/main.4f2a.js, and build the page in the browser. Grep that bundle for api, assistant or chat. Or open the browser's dev tools, send a chat message and watch the Network tab for the request.
The assistant returns 400 or 422. Your body doesn't match what it expects. Copy the exact JSON the widget sends from the Network tab. It might want query, prompt or a messages list instead of message.
Everything starts returning 429. That's Too Many Requests: you hit the gateway's rate limit. Wait for RateLimit-Reset seconds and slow down.
Every path returns 200. Some apps answer anything with a 200 and a "not found" page. Compare the response sizes with -w "%{size_download}". A real route usually looks different from the catch-all.
Fix it, and detect it
If you're building the app, the fixes are small.
Treat front-end JS as public. Strip "internal" comments and unused flags like debugMode and legacySupport from production builds. Turn off old API versions instead of hiding them.
Don't pass the model server's metadata through. Return the reply text and nothing else. provider, model and the Ollama timing fields help nobody outside your team.
Quiet the gateway. Kong lets you turn off its Server, Via and latency headers with the headers setting in kong.conf, and the rate limiting plugin can hide its client headers. Put auth on /v1/auth and /v1/billing if they shouldn't be open.
Watch for the pattern. One client fetching the widget JS and then calling paths it never links to is mapping you. So is a run of 404s on /v1/ paths from one IP, and a burst that hits the rate limit. Alert on 429s from a single source, because real users rarely trigger them.
The AI pentest notes keep every command here in one place.
Quick recap
- AI features live inside the web app, so a port scan won't show them. Read the JavaScript, the API and the headers.
- The chat widget config gave away
/api/v2/assistantand alegacySupportflag worth chasing. - The assistant's reply named Ollama and
llama3.2:1b, and its token counts and timings proved a real model answered. - Port 8000's headers named Kong 3.9.1 and a 60-a-minute rate limit, which sets your scan speed.
- A status code loop found
/v1/chat/completionsbehind a 401 without any credentials.
Try it yourself
Run Ollama on your own machine with llama3.2:1b, put a tiny Flask or Express app in front that passes its metadata through, and serve a widget JS that names the route. Then find it from the outside using only these four steps. After that, strip the metadata and see how much harder step 2 becomes.
If you're mapping AI apps on your own tests and have found other places they hide, I'd be glad to swap notes on LinkedIn.
References
- Ollama API usage fields - token counts and nanosecond timings.
- OWASP Top 10 for LLM Applications (2025) - where recon findings feed in.
- curl man page - the -o, -w, -I and -i options.
FAQ
Why doesn't a port scan find AI services?
Most AI features run inside the existing web app, behind the same port 80 or 443 as everything else. The model server sits on an internal network and the app calls it for you. A port scan sees a web server, so you find the AI part by reading the front-end JavaScript, the API routes and the response headers.
What does a 401 mean when enumerating API endpoints?
A 401 Unauthorized means the endpoint exists but wants credentials, while a 404 means there's nothing there. A 403 also means it exists but you're blocked, and a 405 means it exists but wants a different HTTP method. So you can map protected routes, like an AI chat endpoint, without any credentials.
How can you tell a real model answered and not a canned reply?
Look for token counts and timing in the response metadata. Ollama returns prompt_eval_count and eval_count for input and output tokens, plus durations in nanoseconds. Real generation shows non-zero counts and timings that add up to the total latency, which a hard-coded reply wouldn't have.
Related reading
- LLM Recon: Three Requests That Leak an AI App's Stack (the passive step before this one)
- LLM Function Calling: The Model Isn't Your Auth Check (what to test once you've found the endpoint)
- Red Teaming Generative AI: The Model Is the Wrong Target (why the app around the model matters)
- Browse the whole AI / LLMs track