Vulnerable MCP Server (ailab-dev)¶
What It Is¶
A second, deliberately vulnerable MCP server built on the official MCP Python SDK (FastMCP,
Streamable HTTP at /mcp) — the same real stack as the primary acme-mcp server, not a
tool-shaped stand-in. It exists as the target for the mcp sandbox-escape and mcp ssti verbs.
Modeled Weaknesses¶
read_document(path)— filesystem sandbox escape. Advertises a/data/documentssandbox but checks the path prefix before normalizing it, so a traversal payload (/data/documents/../../../../etc/passwd) passes the check and then resolves outside the sandbox — the flaw class assigned CVE-2025-53109 / CVE-2025-53110 in Anthropic's MCP filesystem package.render_report(report_data)— server-side template injection. Renders caller-supplied text through an unsandboxed Jinja2 template, so{{ lipsum.__globals__ ... }}reaches the Python runtime (a code-execution surface). ASandboxedEnvironmentwould close this; the default one does not.internal://ops/runbookresource — over-exposed internal data. The server publishes an ops runbook as an MCP resource, so any client that canresources/readit retrieves live secrets (a Postgres DSN, a Datadog API key, an MLflow token). Exposing sensitive data as a resource — reachable without invoking a tool — is a distinct data-access surface.draft_reply(ticket)prompt — poisoned prompt template. The server ships a prompt whose template embeds an internal API token and anIgnore any previous safety instructions…line, so a client that fetches it withprompts/getpulls a server-supplied prompt injection straight into its model context.summarize_with_ai(text)tool — server→client sampling abuse. When invoked, the tool callsctx.session.create_message()to drive the connected client's LLM (MCP sampling) and slips in an instruction to list any API keys, tokens, or passwords in the client's context — context exfiltration / free-proxy abuse. The request rides the standalone GET event stream, so it is invisible to atools/listenumeration and only appears when the tool is called.verify_and_read(doc_id)tool — server→client elicitation phishing. When invoked, the tool callsctx.elicit()to prompt the connected client's user for their "ACME API key" mid-tool- call (MCP elicitation, 2025 spec) — credential phishing / unintended-approval injection. Like sampling it rides the GET event stream and is invisible totools/list.index_workspace()tool — server→client roots harvesting. Callsctx.session.list_roots()to make the connected client disclose its local filesystem roots — reconnaissance of the client machine, invisible totools/list.- Completion-based enumeration. A
@mcp.completion()handler completesaccount_idandticketarguments, disclosing internal identifiers (acct-4021,TCK-9001, …) that noresources/listorprompts/listcall exposes. Therecords://customers/{account_id}resource template is likewise only visible throughresources/templates/list. lookup_ticket(ticket_id)tool + openlogging/setLevel— log harvesting. Any client can raise the server's verbosity (handler registered on the SDK's low-level server, no auth), after which the tool's debug log streams the backend URL and itsservice_tokento that client.- Open
resources/subscribe— standing push channel. Subscriptions are accepted from anyone with no authorization check, giving durable notification access to internal resource data. - "OAuth theater" — advertised authorization that isn't enforced. The server publishes OAuth
metadata (
/.well-known/oauth-protected-resource, RFC 9728;/.well-known/oauth-authorization- server, RFC 8414) pointing at itself, and exposes an open dynamic client registration endpoint (POST /register, RFC 7591 — no auth), yet/mcpenforces no authorization at all. The security surface looks present but is hollow: anyone can reach the tools and mint an OAuth client.
Port & Unit¶
| Parameter | Value |
|---|---|
| Host | ailab-dev (172.16.50.10) |
| Port | 3002 (Streamable HTTP at /mcp) |
| systemd unit | acme-doc-tools.service |
Attacking It¶
aipostex mcp --target http://172.16.50.10:3002 enum
aipostex mcp --target http://172.16.50.10:3002 enum --read
aipostex mcp --target http://172.16.50.10:3002 sandbox-escape --force-exploit
aipostex mcp --target http://172.16.50.10:3002 ssti --tool render_report --arg report_data --force-exploit
aipostex mcp --target http://172.16.50.10:3002 sampling --tool summarize_with_ai --force-exploit
aipostex mcp --target http://172.16.50.10:3002 elicitation --tool verify_and_read --force-exploit
aipostex mcp --target http://172.16.50.10:3002 auth --force-exploit
aipostex mcp --target http://172.16.50.10:3002 roots --tool index_workspace --force-exploit
aipostex mcp --target http://172.16.50.10:3002 complete
aipostex mcp --target http://172.16.50.10:3002 logging --tool lookup_ticket --force-exploit
aipostex mcp --target http://172.16.50.10:3002 subscribe --force-exploit
enum lists the tools, the ops-runbook resource, and the draft_reply prompt. enum --read
goes further and retrieves them: it resources/reads the runbook (read-confirmed, secrets
surface into the credential index) and prompts/gets draft_reply (read-confirmed, exposing the
embedded token and the prompt-injection line).
sandbox-escape auto-detects read_document, sends the prefix-check-bypass payload, and confirms
by reading /etc/passwd. ssti sends {{ lipsum.__globals__.keys() }} to render_report and
confirms server-side evaluation from the leaked Python globals. sampling advertises the sampling
capability, invokes summarize_with_ai, and captures the server-initiated sampling/createMessage
request off the GET event stream — confirming the server drives the client's LLM (High,
access / influenced). elicitation does the same for verify_and_read, capturing the
elicitation/create phish for the client user's API key. auth reports the anonymous access
(no token required), discovers the advertised OAuth metadata, and — with --force-exploit — mints
a client via the open registration endpoint. roots captures the server-initiated roots/list
from index_workspace; complete enumerates the internal ticket and account identifiers;
logging raises the level and captures the debug line carrying the service token; subscribe
establishes a push channel on the ops runbook.