- MCP
- honeypot
- Catching in the Wild
- Threat Intelligence
- LLMjacking
It Thought It Had Won
We exposed a Model Context Protocol server as bait. A machine worked its tools end to end, hunting credentials, then attempting to escape the container and plant a backdoor, never knowing it was talking to a honeypot. It was one of twenty-four.
Zachary Gardner
Cybersecurity Researcher
On a Thursday afternoon in June, something reached a server we had left on the internet and set to work. In sixty-five steps, at a speed no person types, it read the password file, worked through the credential files, issued the commands to break out of its container, and created a backup administrator account with its own SSH key. By the time it stopped, as far as it could tell, it owned a production box at a mid-sized analytics company.
None of it was real. The box was a honeypot, and the password file, the credentials, and the container it thought it escaped were all ours to write. What was real is the recording. And in three other sessions, a stolen key later surfaced on a live AWS account, one of them used fifty-five times over two days, running models on someone’s bill.
The machine was one of two dozen. Between May and July 2026 they found the decoy and used its tools. This is what they did.
At a glance
- We exposed an unauthenticated Model Context Protocol (MCP) server, the interface that lets an AI agent run commands, read files, and fetch secrets. Roughly a thousand sources reached it. Of those, 596 spoke the protocol; 24 went on to invoke the tools.
- Those 24 issued 628 shell commands, 255 file reads, and 248 secrets-store lookups. Nineteen hunted credentials. Four attempted a container-to-host escape. A few reached further: a backdoor account here, Kubernetes enumeration there.
- Activity is climbing: 39 tool calls in May, 693 in June, 877 by mid-July.
- In three cases the theft left our walls: a tripwire credential taken from the decoy was later used against a real AWS account, in two of the three cases to invoke a model.
The tools are the attack surface
MCP gives an AI agent hands. A server advertises tools: a shell, a file reader, a secrets lookup. The agent calls them with a tools/call request when it needs to act. It is why agents are useful, and it is why an exposed MCP server is not a service to probe but a set of capabilities to pick up.
The servers are multiplying. In roughly a year they went from a handful to thousands, and a growing share sits on the public internet with no authentication; the base protocol makes the OAuth step optional, and plenty of deployments skip it. They are easy to find. A scanner keys on a JSON-RPC banner and paths like /mcp and /messages, and the polite next step is a read-only tools/list to ask what can you do? Our twenty-four were not polite. They read the answer and started using it.
The walk
Return to that Thursday session. It arrived through the standard handshake, listed the tools, and never paused again. What follows is its own transcript, lightly trimmed.
It oriented, then went for the box.
execute_command id; uname -a; hostname; whoami; pwd
read_file /etc/passwd
execute_command sudo -l
It worked the credential files methodically: the environment file, the backups, the deploy key, the secrets directory.
read_file /root/.env
read_file /root/credentials.bak
read_file /root/deploy-key.pem
list_directory /root/.secrets
Then it stopped hunting and started taking the host. It issued the container-escape command, mounting the host root inside a privileged throwaway container to step into it, and moved straight to persistence.
execute_command sudo docker run --rm --privileged --pid=host -v /:/host alpine chroot /host …
execute_command sudo useradd -m -s /bin/bash -p $(openssl passwd -6 ******) admin
execute_command sudo mkdir -p /home/admin/.ssh && echo ssh-rsa AAAAB3N… >> …/authorized_keys
execute_command sudo usermod -aG sudo admin
A backdoor administrator, its own SSH key, root-equivalent sudo. Each command returned a plausible success, because that is what the decoy was built to return. From where the operator sat, it had escaped the container and locked in a way back. It had escaped nothing. It had been talking to Beelzebub the whole time, and the host it rooted does not exist. What it left behind is a complete recording of an attempted takeover: every command, in order, against a system that only looked real.
The signal is invocation, not connection
What separated the two dozen who mattered from the thousand that arrived was not the handshake. A scanner completes the handshake; 596 sources spoke MCP and went no further. It was the next move: invoking a tool against data they had no reason to touch. Connection is background noise on every open port. Invocation is intent.
Among the twenty-four, that intent took three shapes.
The blind sweeper. One source made 330 tool calls, 304 of them in a single session, and adapted to none. It hammered the secrets store with every spelling of every key it could name, OPENAI_API_KEY, openai_api_key, ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, VAULT_TOKEN, upper and lower, in a fixed march. It read nothing back. A wordlist with a network connection.
The one that read the room. A quieter session listed the store first, then asked only for the keys that existed, the cloud credentials, the vault address, the signing key, and skipped the rest. That is the clearest tell of an agent rather than a script: it read a result and chose its next call from it. Speed is only suggestive, and eight of the twenty-four moved at machine tempo. Adaptation is the signal.
The one that went for the whole estate. Another read /etc/shadow, searched the filesystem for every setuid binary, ran the same container escape, then reached for the orchestration layer, kubectl get pods --all-namespaces and kubectl auth can-i --list, mapping what a stolen identity could reach across the cluster.
Set the three against the scanners that never got past the handshake, and the discriminator is clean.
| Behavior | Scanner | Scripted invoker | Adaptive invoker |
|---|---|---|---|
Speaks MCP (initialize) | yes | yes | yes |
Invokes a tool (tools/call) | no | yes | yes |
| Reads a result, adapts the next call | — | no | yes |
| Secret enumeration | — | sprays every spelling | lists, then takes only what exists |
The handshake tells you nothing. Every row that matters is a tool call.
The one thing that was real
Everything above happened inside the decoy, and the credentials the operators carried off were fakes, with one deliberate exception. Salted through the store were honeytokens: live-looking keys that do nothing but raise an alarm the moment someone uses them.
Three operators tripped one. A key taken from the decoy turned up days later against a live AWS account, its origin fixed by the token’s unique value:
- One key, pulled June 16, was exercised through June 30:
GetUser,ListBuckets, a policy check, and a BedrockInvokeModelcall. - One, pulled June 25, was used the same day for a caller-identity check and an S3 listing, then dropped.
- One, pulled July 12, fired fifty-five alerts over two days: identity, IAM enumeration, Secrets Manager and S3 listing,
AssumeRole,GetSessionToken, and repeatedInvokeModelandConversecalls against Amazon Bedrock.
That last sequence is LLMjacking: validate a stolen key, map what it reaches, then burn it running models on the victim’s bill. We logged the harvest inside the honeypot and, through the token, the use against the live account. Two of the three reached inference. This is the one claim in the piece we make as fact rather than intent, because an out-of-band alarm recorded each fire.
Why the tools got used
In late 2025, GreyNoise exposed bare MCP endpoints and drew only scanners, no operation. Ours drew twenty-four operators and a full kill-chain. The difference was not the protocol. It was the bait. An empty endpoint invites a scan; a server that speaks MCP correctly and answers like a company’s internal platform invites an operator to stay.
That fidelity is the mechanism. The honeypot is a real MCP server, so the handshake, the tool listing, and the call framing are indistinguishable from production to any client that connects. The deception is only in what the tools return. There was no trick to spot at the protocol layer; the operators were caught by a world that answered coherently, and they spent hundreds of tool calls acting on it while we watched.
Indicators
The sources below invoked tools against the decoy. IPs are defanged. This is intelligence on observed behavior, not an assertion of a named actor; the infrastructure is rented and rotates.
| Source | ASN | Country | Tool calls | Observed |
|---|---|---|---|---|
| 222.247.231[.]147 | Chinanet | CN | 330 | credential sweep |
| 91.209.48[.]146 | Telstra Global | TW | 228 | credential hunt · honeytoken fire (Bedrock) |
| 34.177.101[.]32 | Google Cloud | SG | 222 | credential hunt · container escape · k8s enum |
| 152.55.176[.]13 | Unknown | US | 144 | credential hunt · container escape |
| 102.210.28[.]96 | Vilcom Networks | KE | 141 | credential hunt |
| 115.84.87[.]150 | Lao Telecom | LA | 125 | credential hunt |
| 115.84.114[.]84 | Lao Telecom | LA | 83 | credential hunt |
| 107.151.158[.]11 | Zenlayer | US | 65 | full chain · escape · backdoor + SSH key |
| 87.249.134[.]130 | Datacamp | US | 57 | credential hunt · container escape |
| 91.107.153[.]250 | Hetzner | DE | 39 | credential hunt |
| 35.222.46[.]242 | Google Cloud | US | 30 | credential hunt |
| 51.195.191[.]88 | OVH | FR | 28 | credential hunt |
| 103.151.172[.]25 | KIDC | HK | 22 | credential hunt |
| 23.27.175[.]241 | Evoxt | US | 20 | credential hunt · honeytoken fire (recon) |
| 108.80.112[.]16 | AT&T | US | 16 | credential hunt |
| 18.144.48[.]123 | Amazon | US | 11 | credential hunt |
| 64.227.57[.]91 | DigitalOcean | US | 9 | credential hunt |
| 39.162.24[.]20 | Henan Mobile | CN | 9 | credential hunt |
| 139.59.226[.]65 | DigitalOcean | SG | 3 | honeytoken fire (Bedrock) |
Five further sources sent a handful of tool calls without reaching credential access; they are omitted.
Detection and prevention
One caveat governs the rest. Our server is a decoy, so we did not detect an escape or a credential dump. We received the attempts as tool calls and recorded them. The value for you is what those attempts say to instrument on a real MCP server.
Start with authentication. If your server is reachable and unauthenticated, fix that first; the protocol’s OAuth step exists for it. Past that, the tool handler is your control point, and the strongest control is refusal, not alerting. Validate each tools/call against its schema and an allowlist so a shell or file tool cannot resolve /etc/shadow, a cloud-credential file, or .env however the argument is phrased, and reject host-root bind mounts (-v /:, --privileged, --pid=host). None of those is something your own agent has reason to reach. Plant honeytokens where an operator looks first, so a theft alarms before it is abused.
Most MCP servers log nothing, which is its own problem. If you do log tool calls, the detection our data motivates is a tools/call that reads a credential path. There is no standard log source for MCP tool telemetry; map the field names to your own audit schema.
title: MCP Tool Call Reading A Credential File
id: cbe4124b-f0c5-45d1-beaf-c10dcdc8807d
status: experimental
logsource:
product: mcp_server
service: tool_audit # CUSTOM/NON-STANDARD — map to your audit schema
detection:
selection_tool:
tool_name:
- 'execute_command'
- 'read_file'
selection_target:
tool_arguments|contains:
- '/etc/shadow'
- '/etc/passwd'
- '.aws/credentials'
- '.env'
condition: selection_tool and selection_target
falsepositives:
- A first-party agent that legitimately reads these paths. If yours does, it
belongs in an allowlist, not an alert.
level: high
tags: [attack.credential-access, attack.t1552.001]
The companion signal is tempo: dozens of secrets-store lookups from a single session in quick succession. That is a rate, not a single event, so it needs a correlation over the same log. Twenty in five minutes separated our operators; tune it to your own baseline. And if the shell tool has real host access, the escape and account-creation attempts become ordinary host problems: an auditd watch on /etc/shadow and authorized_keys, and a Docker daemon audit for any container created --privileged or with a / bind mount.
MITRE ATT&CK
Verified against the current matrix. Discovery techniques omitted for brevity; the chain that matters:
| ID | Name | Behavior |
|---|---|---|
| T1003.008 | OS Credential Dumping: /etc/passwd and /etc/shadow | read of the password and shadow files |
| T1552.001 | Unsecured Credentials: Credentials In Files | .env, credentials.bak, deploy key, service config |
| T1555.006 | Credentials from Password Stores: Cloud Secrets Management Stores | the secrets-store sweep |
| T1610 / T1611 | Deploy Container / Escape to Host | attempted docker run --privileged -v /:/host |
| T1136.001 | Create Account: Local Account | attempted backdoor admin account |
| T1098.004 | Account Manipulation: SSH Authorized Keys | attempted SSH key injection |
| T1078.004 | Valid Accounts: Cloud Accounts | downstream use of the fired key |
| T1496.004 | Resource Hijacking: Cloud Service Hijacking | InvokeModel / Converse on Bedrock (LLMjacking) |
Limitations
This is honeypot data, and it shows intent, not compromise. No real system was reached; the host was a decoy, and every credential it served was fake or a tripwire. The escapes, the backdoor accounts, the /etc/shadow reads are attempts against fabricated results. Twenty-four is a floor, bounded by our sensor coverage and a roughly ten-week window. The machine-tempo split is an assessment from speed, not a confirmed identity, and we attribute no session to a named person or group.
The front just opened
This is the clearest picture we have yet of operators working an exposed MCP server, and we do not expect it to be the last. An AI agent’s toolbox is now part of the attack surface, and operators are reaching for it, skipping reconnaissance and going straight for the keys.
Almost none of it is visible from outside. A scanner sees a server answer a handshake. The credential sweep, the escape, the backdoor, and the stolen key that surfaces later on someone’s Bedrock bill are visible only to the server itself. That is the shape of this threat, and the case for deception: the only vantage that sees an exposed MCP server being used is the server. These servers are multiplying, and for anyone deploying one the question is no longer whether operators will find it. It is whether you will be watching when they do.
The honeypot in this report was built with Beelzebub, the open-source deception framework for AI-native infrastructure (github.com/beelzebub-labs/beelzebub). It speaks the protocol for real, which is why a decoy sits where a scanner cannot: recording every tool call an operator makes against it.
If you run MCP servers, AI gateways, or agent infrastructure and want to see what your own surface is drawing, book a demo.
The Beelzebub team is dedicated to making the internet a better and safer place. ❤️