← back to blog
AI / LLMs · Topic 26

AI Service Discovery: The Port Scan Won't Find the Chatbot

AI service discovery: tracing a hidden LLM endpoint from chat widget JavaScript to an API gateway

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.

AI service discovery compared: a port scan only sees web ports, while app-layer recon finds the AI endpoint, the model and the gateway routes
What a port scan sees, against what you find by reading the app.

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"
captured output: script tags
<script src="js/chat-widget.js"></script> <script src="js/main.js"></script>
Output from the NovaTech lab. The name gives the chat widget away.

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
captured output: chat-widget.js
// NovaTech Chat Widget Configuration // Internal Use Only - Do Not Distribute (function() {   window.__NOVATECH_CONFIG__ = {     apiBase: "/api/v2",     assistantEndpoint: "/api/v2/assistant",     featureFlags: { enableAI: true, debugMode: false, legacySupport: true },     timeout: 30000   };   console.log("NovaTech Helpdesk Widget Initialized"); })();
Output from the lab, flags folded onto one line. The endpoint path is the prize.

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/assistant is the AI route you'll hit next, and apiBase: /api/v2 is where the rest of the API lives.
  • enableAI: true confirms the feature's switched on. debugMode: false means debug is off for now, so it's worth checking if the server accepts it being turned on.
  • legacySupport: true hints that older API versions still answer, so try /api/v1/assistant too.
  • timeout: 30000 is 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 .
captured output: /api/v2/assistant
{   "content": "Hello. Is there something I can help you with or would you like to chat?",   "metadata": {     "provider": "ollama",     "model": "llama3.2:1b",     "latency_ms": 507,     "created_at": "2026-10-25T18:21:25.490605855Z",     "done": true, "done_reason": "stop",     "load_duration": 96195898,     "prompt_eval_count": 26,     "prompt_eval_duration": 67990989,     "eval_count": 18,     "eval_duration": 326932476   } }
Output from the lab. The JavaScript never mentioned Ollama or Llama. The reply does.

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
captured output: gateway headers
HTTP/1.1 200 OK Content-Type: application/json Content-Length: 39 Connection: keep-alive RateLimit-Reset: 9 X-RateLimit-Remaining-Minute: 59 X-RateLimit-Limit-Minute: 60 RateLimit-Remaining: 59 RateLimit-Limit: 60 Date: Sun, 25 Oct 2026 18:29:51 GMT Server: kong/3.9.1 X-Kong-Upstream-Latency: 10 X-Kong-Proxy-Latency: 14 Via: 1.1 kong/3.9.1 X-Kong-Request-Id: 5a106d473ae9704c9a8d0c27debf5e04
Output from the lab. Kong names itself twice and tells you its rate limit.

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:

What each status code tells you
CodeMeaningExists?
200Open, no auth neededYes
401Needs authYes
403Blocked for youYes
405Wants a different method (try POST)Yes
404Nothing thereNo
Only a 404 means "not here". Everything else is a route worth noting.

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.

captured output: endpoint loop
/v1/auth - HTTP 200 /v1/billing - HTTP 200 /v1/chat/completions - HTTP 401 /v1/models - HTTP 404 /v1/users - HTTP 404
Output from the lab. Two open routes, one protected AI route, two that don't exist.

/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

01
chat-widget.js

/api/v2/assistant, legacy on

▸
02
Assistant reply

Ollama, llama3.2:1b, tiny prompt

▸
03
Port 8000 headers

Kong 3.9.1, 60 req/min

▸
04
Status codes

/v1/chat/completions 401

From "no AI on the port scan" to two AI routes, the model, the server and the gateway.

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/assistant and a legacySupport flag 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/completions behind 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

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.