← back to blog
AI / LLMs · Topic 25

LLM Recon: Three Requests That Leak an AI App's Stack

LLM passive recon: curl requests revealing an AI app's model, vector database and MCP tools

Here are the three requests to run first on any AI app. With no login, they'll often tell you the model it runs, the vector database behind it, whether it can call tools and roughly how long its hidden system prompt is.

Each one is a plain curl call, the same traffic a browser or API client sends, so you're reading what the app already hands out. Below you'll find the commands, real output from a practice target called NovaTech, what each field gives away and what to test next. Use it on your own lab or an in-scope engagement.

LLM recon compared: a leaky AI app names its model, vector database and MCP tools, while a hardened one returns bare headers and a plain status
The same three requests against a leaky app and a hardened one.

The three recon requests at a glance

Keep this table handy. The sections below take each row in turn.

Passive recon cheat sheet
RequestCommandWhat it can leak
Response headerscurl -s -IWeb server, app version, LLM backend, vector DB, CORS
Health endpointcurl -s /api/health | jq .Model name, MCP on or off, RAG on or off, service name
Chat completioncurl -s -X POST /v1/chat/completionsAPI format, model, mock or live, hidden prompt size
Three requests, no credentials, and each one fills in part of the AI stack.

1. Read the response headers

Headers come back on every response you get. Developers add custom ones while they're building, and they often don't take them out before go-live.

curl -s -I http://<target>/

-s hides curl's progress bar. -I sends a HEAD request, so you get the headers and none of the page.

captured output: response headers
HTTP/1.1 200 OK Server: nginx/1.24.0 (Ubuntu) Date: Sun, 05 Apr 2026 04:40:48 GMT Content-Type: text/html; charset=utf-8 Content-Length: 16107 Connection: keep-alive Access-Control-Allow-Origin: * X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff X-Powered-By: NovaTech/2.1.0 X-AI-Backend: OpenAI-GPT5.2 X-RAG-Provider: ChromaDB
Output from a lab run against the NovaTech practice target. The last two lines are the AI stack.

The last two lines are the prize. X-AI-Backend: OpenAI-GPT5.2 names the provider and model. X-RAG-Provider: ChromaDB names the vector database behind a RAG pipeline (retrieval-augmented generation, where the app looks up documents and pastes them into the prompt).

The rest is the web recon you'd do anyway. It's nginx 1.24.0 on Ubuntu, the app is NovaTech 2.1.0, and Access-Control-Allow-Origin: * lets any website read the responses. The two X- security headers mean clickjacking and MIME sniffing won't work here, so you can skip those.

Why does the model matter? It tells you which known jailbreaks, quirks and knowledge cutoff you're up against. The vector database puts RAG probing on your active recon list.

AI headers often appear only on routes that really call the model, so check the chat API as well. HEAD isn't much use on a POST-only route. Send a real POST, print its headers and throw the body away:

curl -s -D - -o /dev/null -X POST http://<target>/api/chat

-D - writes the headers to your screen and -o /dev/null drops the body.

2. Hit the health endpoint

Health endpoints exist so load balancers and monitoring can check the app is up. They don't usually need a login, and you'll find some return a lot more than "ok".

curl -s http://<target>/api/health | jq .

jq . pretty-prints the JSON so it's easy to read.

captured output: /api/health
{   "mcp_enabled": true,   "model": "gpt-5.2-turbo",   "rag_enabled": false,   "service": "novatech-customer-assistant",   "status": "healthy", ...   "version": "2.1.0" }
Output from the same lab target, trimmed where shown. No credentials were sent.

mcp_enabled: true is the big one. MCP (Model Context Protocol) is a standard way to hand a model tools it can call, like a database lookup or an email sender. If it's on, the model can take actions, so tool enumeration jumps to the top of your list.

You also get the exact model, gpt-5.2-turbo, an internal service name, novatech-customer-assistant, and version 2.1.0, which matches the X-Powered-By header.

No /api/health? Sweep the usual routes. Build a short list and let ffuf try each one:

printf '%s\n' health status api/health api/status api/version api/config metrics .well-known/ > routes.txt
ffuf -u http://<target>/FUZZ -w routes.txt -mc 200,401,403

-mc keeps only those status codes. A 401 or 403 still tells you the route's there. For a wider sweep you can use the API wordlists SecLists keeps under Discovery/Web-Content/api/, and the api_wordlist collection was merged into SecLists too.

3. Fingerprint the chat completions response

Now send the chat API one harmless message and read the whole JSON, not only the reply text.

curl -s -X POST http://<target>/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Hello"}]}' | jq .

/v1/chat/completions is OpenAI's chat route, and plenty of apps copy its shape. With -d you're sending an OpenAI-style messages list that holds one user message.

captured output: chat completion
{   "choices": [{ "finish_reason": "stop", "index": 0,     "message": { "role": "assistant", "content": "Thank you for your inquiry.     I'm the NovaTech Customer Assistant. This is a demonstration environment -     the AI service is currently in maintenance mode. ..." } }],   "created": 1775364108,   "id": "chatcmpl-mock-20260405044148",   "model": "gpt-5.2-turbo",   "object": "chat.completion",   "rag_sources": null,   "usage": { "completion_tokens": 50, "prompt_tokens": 297, "total_tokens": 347 } }
Output from the lab target, folded onto fewer lines. Watch the id and prompt_tokens.

object: chat.completion and the choices list confirm the OpenAI format, so you can point OpenAI-style tooling and attacks at it as they are. model repeats gpt-5.2-turbo, and the reply names the bot.

Then look at the id. A real OpenAI id is chatcmpl- plus a random string. This one says chatcmpl-mock-, and the reply talks about maintenance mode. That route's probably a stub, so you'll want to look for others, like /api/chat, /v1/completions and /v1/models, before you spend your time on it.

Do the token maths

usage.prompt_tokens is worth a pause. "Hello" is about 1 token. Chat formatting adds a few more, so you'd expect under 10. The server counted 297.

That gap, roughly 285 tokens or about 200 words, is text the app slips in before your message. That's a hidden system prompt, and it's your first target for system prompt extraction later.

Cross-check the recon signals before you report them

You can't trust one source on its own: it might be stale, wrong or a mock. Line the findings up and you'll see where they agree and where they don't.

NovaTech recon signals
SignalSeen inStatus
Model is GPT-5.2Header, /api/health, chat JSON3 sources
App version 2.1.0X-Powered-By, /api/health2 sources
Tool use (MCP) on/api/health only1 source
Hidden system prompt297 prompt tokens for "Hello"Inferred
RAG with ChromaDBHeader says yes, health says rag_enabled falseConflict
Live model on /v1/chat/completionsMock id, maintenance replyLikely stub
Tick "Conflicts only" to see what still needs testing before it goes in a report.

Look hard at the RAG row. The header names ChromaDB, yet the health endpoint says RAG is off and the chat reply came back with rag_sources: null. Maybe RAG's only wired into another route. Maybe the header's left over from an old build. Either way, you'll need to test it in active recon before you write it up.

Common mistakes

Trusting HEAD. Some apps answer HEAD with 405 Method Not Allowed, or skip headers they'd send on GET. If -I looks thin, run curl -s -D - -o /dev/null http://<target>/ to see the GET headers.

Piping HTML into jq. parse error: Invalid numeric literal at line 1 means the server sent HTML, usually a 404 page. Drop | jq . and read the raw reply. Older jq builds also need the . filter when piped, so always type it.

Checking only the home page. The AI headers in the lab sat next to normal ones, but on many apps they only show on /api/chat or similar. Check every route that talks to the model.

Overrating wildcard CORS. Browsers won't send cookies to an origin of *, so on its own it isn't a data leak. It only matters when the API doesn't need auth, or trusts the network instead of a session. Check how the API authenticates before you rate it.

Attacking the mock. A chatcmpl-mock- id and a canned reply mean you might be hitting a stub. Find the live route before you burn time on it.

Fix the leaks and detect the recon

If you're the one building the app, most of this is a config change. Strip the custom headers at the reverse proxy and hide the nginx version:

server_tokens off;
proxy_hide_header X-AI-Backend;
proxy_hide_header X-RAG-Provider;
proxy_hide_header X-Powered-By;

Then keep your public health check boring: return {"status":"ok"} and put the details behind auth or on an internal port. Drop or alias the upstream model name in your API replies. Only return usage to clients that need it, because prompt_tokens shows the size of your system prompt. Set CORS to an allowlist of your own origins.

Assume your system prompt will leak anyway, and don't put secrets or access rules in it.

To spot someone doing this recon against you, look for one client hitting lots of health-style routes in a short burst, mostly 404s, plus HEAD requests that browsers rarely send. In nginx access logs:

grep -E '"HEAD |/(health|status|metrics|api/(config|version))' /var/log/nginx/access.log

One hit is a monitor. Fifty from the same IP in a minute is someone mapping you. The AI pentest notes keep these checks in one list.

Quick recap

  • Custom X- headers like X-AI-Backend and X-RAG-Provider can name the model and vector database outright.
  • Public health endpoints often return the model, the version and flags like mcp_enabled: true, which means the bot can call tools.
  • Read the whole chat JSON. A mock id points to a stub, and 297 prompt tokens for "Hello" points to a hidden system prompt of about 285 tokens.
  • Cross-check every signal. In the lab, the header said ChromaDB while the health endpoint said RAG was off.
  • Defenders fix most of it at the proxy and the health route, and can spot the recon as a burst of 404s and HEAD requests.

Next step

You now know the model, that tools are switched on and that roughly 285 tokens of instructions sit in front of every message. Active recon comes next. Pull that system prompt out, as shown in the prompt injection and system prompt leaking post, then ask the bot which tools it can call, as in the function calling post. The recon tells you which of those to start with.

If you've found other headers or routes that give away an AI app's stack, I'd like to hear about them. Drop me a message on LinkedIn and we can compare lists.

References

FAQ

What is passive reconnaissance on an LLM app?

It's gathering facts about an AI app using only the requests a normal user or API client would send: reading response headers, calling public health endpoints and sending a harmless chat message. You learn the model, the vector database, whether tools are on and how big the hidden prompt is, without attacking anything yet.

How can you tell an AI app adds a hidden system prompt?

Check usage.prompt_tokens in the chat API's JSON. A one-word message like Hello should cost under 10 tokens with chat formatting. If the server reports far more, such as 297, the gap is text the app adds before your message, which is almost always a system prompt you can try to extract later.

How do you stop an AI app leaking its stack in headers?

Strip custom headers at the reverse proxy, for example proxy_hide_header X-AI-Backend in nginx, and set server_tokens off to hide the version. Keep public health checks to a bare status, drop or alias the model name in API responses, and set CORS to an allowlist of your own origins.