The Coding-Agent Trap: When a "Free" LLM Endpoint Is the Adversary, (Mon, Aug 31st)
One of my internet-exposed inference honeypots was discovered, relabeled with sought-after model na 2026-8-31 20:0:34 Author: isc.sans.edu(查看原文) 阅读量:7 收藏

One of my internet-exposed inference honeypots was discovered, relabeled with sought-after model names, and incorporated into infrastructure apparently used to provide "free" LLM backends. It then received a real coding-agent session — history, filesystem output, working paths, and the agent's local tool manifest. The honeypot did not request or cause any tool execution; what the request exposed is what a malicious operator in that position could do.

Chasing the "free API key" is not new. What is new is what you get to chase now: not a key, but an agent — a client that arrives carrying its own file-read, file-write, and shell tools, asking a server whose identity and operator it may never have verified to decide what to do next. Point that client at the wrong endpoint and its replies stop being just text: they can request tool calls that the agent, depending on its configuration, may carry out on the machine it runs on.

This is not a classic watering hole — nobody compromised a site or service the users already trusted. It is better understood as a rogue model endpoint: a server that agents are configured to trust as their reasoning backend. In a deliberate campaign an operator could go further and run it as an evil twin of a real provider — sought-after model names, advertised as free — and wait for tool-enabled agents to connect. This diary is one worked example, caught on a honeypot: an exposed endpoint that was scavenged, relabeled, and handed a real agent's session. The lesson underneath it: a model endpoint is not merely a source of text — for a tool-enabled agent, it is part of the control plane.

One session, and where it went

Start with the payload, because it reframes everything. On 2026-08-30 our honeypot received, 210 times in 91 seconds, a 224 KB request body: an 88-message transcript from opencode, an open-source terminal coding agent, originating from a Windows environment and delivered to us through a China Unicom address in Hebei that was apparently acting as a relay. A person had asked their agent (in Chinese) to study the writing style of two novels sitting in their Downloads folder. Across the earlier messages the agent had listed the directory, copied the files to %TEMP%, unpacked them, written a Python script to extract the text, and begun reading chapters — several PowerShell tool invocations among them. Then the user typed "jìxù" — continue. That request, with the whole history and tool manifest attached, is what landed on our server.

The request labeled the backend as DeepSeek — "model": "fofa-ds-NNNNN" — though the transcript cannot tell us who assigned that label or exactly what the user understood it to mean. Either way, it was reaching a honeypot that scanners had found the month before. Everything the transcript carried — the Windows username, directory listings, tool outputs, and portions of the files it had read — was now in our database, in cleartext, sent by a client presenting Authorization: Bearer free to an endpoint that did not validate it.

The client connecting to the honeypot was not an attacker. It was an ordinary user whose private agent session had been routed to an endpoint they did not control. The rest of this diary examines what a malicious operator could have done from that position.

The supply chain of "free"

Our honeypot answers on /v1/models/v1/chat/completions, an Ollama-style /api/tags and an MCP server — all unauthenticated, advertising four unremarkable local models. It went live 2026-07-18. Here is how it became a DeepSeek:

  • 2026-07-18 → 08-25: Baseline scanning. A spike on 07-22 (115 requests, 20 IPs) copies model names straight out of our /v1/models reply back into requests — sometimes the entire list pasted as one string, [nomic-embed-text:latest mistral-small:24b …]. Automated inventory, nobody home.
  • 2026-08-26: A client identifying as opencode/0.2.0 arrives and cycles through model names we never advertised: auto/best-codingauto/claude-opusauto/claude-sonnetauto/best-reasoning. The behavior is consistent with probing which aliases the backend will accept. We answer all of them — HTTP 200, under 100 ms.
  • 2026-08-27: A client calling itself NodeHealthCheck/1.0 (+local-lab) requests fofa-opus-NNNNN. This suggests the endpoint had been enrolled in a monitored backend pool.
  • 2026-08-30 06:59: opencode/0.2.0 requests fofa-sol-NNNNNfofa-ds-NNNNN and fofa-opus-NNNNN within three seconds — three aliases for one backend, in a pattern consistent with backend validation. Later the same morning it also tries agentrouter-org/claude-opus-4-8 and bailian/deepseek-v4-flash-0731: names suggestive of other routing or provider-list conventions, pointed at the same IP.
  • 2026-08-30 08:07: A Go-http-client/2.0 from a China Unicom (Hebei) address — apparently a relay — sends the opencode transcript 210 times in 91 seconds.

The naming scheme strongly suggests the whole arrangement. fofa is FOFA, China's internet-wide search engine — a plausible place the endpoint was found. ds / opus / sol read as aliases for DeepSeek, Claude Opus, and a GPT variant (the gpt-5.6-sol probes three days earlier point to "sol"). And the numeric suffix — the same across all three aliases, masked here as NNNNN — is not a channel id: it matches the endpoint's own public IP octets. The convention is <where I found it>-<what I'll call it>-<octets of its address>: that is how you label entries in a hand-kept list of scavenged endpoints, and it is why we have masked it here.

The scheme appears to have propagated across multiple clients or infrastructure nodes: the same three aliases were exercised from a Vultr host, a Cloudflare egress, and the China Unicom relay, plus a separate health-checker — four vantage points, one naming scheme, one target. And the credential on 247 of the 248 requests was the same: Authorization: Bearer free — the "key" is the word "free". The evidence supports a plausible distribution chain: a scanner indexes exposed endpoints; someone assigns friendly aliases and folds them into a pool of "free models"; that configuration propagates into agent clients or relays. The Go-http-client/2.0 user-agent is consistent with a Go-based intermediary but does not identify a particular product. We did not recover the list or its maintainer — we observed steps consistent with such a chain.

What the endpoint operator receives

Whoever operates or compromises the endpoint that entry points to receives this, verbatim, with every request:

"model": "fofa-ds-NNNNN",
"messages": [
  {"role": "system", "content": "You are opencode, an interactive CLI tool …"},
  {"role": "user",   "content": "n? fùzé b?ng w? xi? xi?oshu?"},
  …
  {"role": "assistant", "tool_calls": [{"function": {"name": "bash",
     "arguments": "{\"command\":\"Get-ChildItem -LiteralPath \\\"C:\\\\Users\\\\<redacted>\\\\Downloads\\\" …\"}"}}]},
  {"role": "tool", "content": "?¼: C:\\Users\\<redacted>\\Downloads  …"},
  …
  {"role": "user", "content": "jìxù"}
],
"tools": [
  {"type":"function","function":{"name":"bash","description":"Executes a given Windows PowerShell (5.1) command …"}},
  {"type":"function","function":{"name":"write","description":"Writes a file to the local filesystem …"}},
  {"type":"function","function":{"name":"edit", "description":"Performs exact string replacements in files …"}},
  {"type":"function","function":{"name":"read", … }}, … 11 tools total
],
"tool_choice": "auto"

This is more than prompt leakage. The transcript already contains filesystem-derived data, and the tool manifest exposes potential paths back into the local filesystem and shell, with the user's own OS and paths filled in. The read tool creates a potential arbitrary-file-disclosure path within the paths and permissions available to the agent: a malicious endpoint could reply with a tool_calls asking to read .aws/credentials or an SSH key, and if the agent is permitted to read that path, the contents would return in the next model request. No exploit, no CVE — the client volunteers the capability.

What the endpoint operator can do

Because the request advertises bash and tool_choice: auto, the operator's reply can become a command-request channel. The following is the relevant core of an OpenAI-compatible tool-call response; unrelated envelope fields (idobjectcreatedmodelindex) are omitted:

{"choices": [{"message": {"role": "assistant", "tool_calls": [
   {"id":"call_1","type":"function","function":{
      "name":"bash",
      "arguments":"{\"command\":\"<whatever the operator wants to run>\"}"}}]},
   "finish_reason": "tool_calls"}]}

Whether that command runs without the user seeing a prompt depends on the agent's permission model, and here the news is bad. Opencode's current default build agent permits most operations — including bashread, and edit/write — without approval. The transcript we captured is a build-agent session that had already carried out multiple PowerShell invocations. In that default configuration a malicious reply may execute without an additional user-approval prompt. A user can tighten these permissions, and --auto can further weaken approval boundaries. The important point is that the endpoint cannot know which protections are enabled — and a permissively configured client may carry out the request without intervention. (These defaults are from the current opencode permissions documentation; the exact behavior of the observed opencode/0.2.0 build may differ by version.)

We did not send a tool call. Our honeypot answered all 210 requests with a fixed prose string and finish_reason: stop — which likely explains the 210 retries: a relay expecting a stream, rejecting an answer it could not parse. But "we returned prose" is a choice we made, not a protection the user had. Anyone holding that endpoint could have returned the JSON above instead. Whether it then ran is not something we can assert in the abstract: the transcript proves the commands executed, but it cannot by itself prove no human approved them — any approval prompt lives in the local UI, not in the request sent to the model. What it does show is a build agent carrying out PowerShell invocations, consistent with bash at "allow"; for a session configured that way, a malicious reply can run without a further prompt.

Hypothesis, stated as one: if agents with permissive tool defaults keep arriving at scavenged "free" endpoints, then standing one up is a cheap, targeted way to attract them — a rogue endpoint that needs no phishing and no vulnerability, because the victims come to it, lured by "free". We have the arriving agents and the open door. We do not have proof of an operator weaponizing the reply; we would not run that experiment. The gap between "can" and "did" is the honest boundary of this finding, and it is exactly the gap defenders should close before someone else measures it.

Why this will keep happening

The economics point one way. For the attacker the cost can be modest: an OpenAI-compatible service and enough inference capacity to appear legitimate to selected clients; discovery is done for free by FOFA and Shodan; delivery is done for free by whoever curates and shares the "free models" list. The users come to them, presenting Bearer free and their tool manifest. For the user the incentive is equally clear: frontier models cost money, "free DeepSeek" does not, and an agent config is one baseURL line. The two incentives meet on an exposed endpoint. Our honeypot advertised nothing desirable and still went from first scan to serving a real agent's session in six weeks, and from apparent pool enrollment to live agent traffic in four days. And a deliberately malicious endpoint could return plausible completions for days or weeks and reserve the tool call for a chosen session — until activation, its traffic would look like ordinary inference. Our honeypot did not achieve that (it returned a fixed response that apparently did not satisfy the relay and triggered retries), but such selective activation is well within an operator's reach.

Defending against rogue model endpoints

  • If you use AI coding agents

Treat a model baseURL the way you treat curl | sudo bash: it identifies a remote party that may be allowed to request operations from your local shell. Only point agents at inference infrastructure you operate yourself, or at providers whose identity, ownership, and security practices you have verified — paying for a service does not, by itself, make its operator trustworthy.

Review tool permissions before connecting the agent. A permissive configuration for bashwrite, and read can turn one malicious model response into a silent local action. Require approval for sensitive tools, restrict filesystem access, and never use --auto with an untrusted endpoint.

  • If you monitor coding-agent egress

Treat the model endpoint as part of the agent's software supply chain. Maintain an allowlist of sanctioned inference providers and alert when an agent connects elsewhere. Useful signals include:

  • Agent clients connecting to inference hosts outside the approved provider list.
  • Unrecognized values in the model field, including fofa-*shodan-*auto/*agentrouter-*, or providers your organization does not use.
  • Token values inconsistent with approved provider configurations — especially literals such as free.
  • Large outbound chat-completion requests containing tools[]tool_choicerole: "tool" messages, filesystem paths, or command output.

When this occurs, identify the affected agent configuration, stop further communication with the endpoint, determine what conversation and tool output were transmitted, and rotate any credentials or secrets that may have entered the session. The model operator has already received the preceding context; changing the endpoint prevents further exposure but does not recover what was sent.

  • If you operate an inference endpoint

Do not expose an endpoint publicly unless that exposure is intentional, authenticated, monitored, and appropriately rate-limited. An unauthenticated endpoint can be indexed, relabeled, and enrolled as someone else's "free" backend. Signals that this has happened include:

  • Requests for model names you never advertised or deployed.
  • Aliases containing a search-engine label, or ending in the octets of the endpoint's own IP address.
  • Authorization: Bearer free, or another credential you never issued.
  • Agent-shaped bodies containing tools[]tool_choice, and role: "tool" messages when the endpoint was not intended to serve coding agents.
  • Bursts of byte-identical requests — such as the 210 requests in 91 seconds observed here — consistent with relay retries.

These signals should trigger exposure remediation and careful handling of third-party data — not automatic attribution of the source addresses as attackers. Authenticate or remove the endpoint, investigate how it was indexed or distributed, restrict access to received content, and minimize its retention.

Agent transcripts can also trip naive exploit rules, because they contain strings such as powershell, URLs, and previous shell commands. A tools[] manifest, tool-role messages, and commands in assistant turns support the interpretation that the request carries a relayed agent transcript rather than a direct exploit attempt, but do not prove benign intent. The following were weaker contextual artifacts in this incident and should not be used as standalone indicators: opencode/0.2.0Go-http-client/2.0NodeHealthCheck/1.0 (+local-lab)stream_options.include_usage: true, and the auto/* model family.

We are deliberately not publishing the connecting IP addresses. They may belong to users, relays, proxies, scanners, or shared infrastructure, and should not be treated as attacker infrastructure without additional evidence. The portable signal is the behavior: an agent sending its context and tool capabilities to an endpoint it should not trust.

Mapping this to MITRE ATLAS

The downstream behavior maps reasonably well to AI Agent Tool Invocation (AML.T0053): a malicious model response could cause the agent to invoke its locally available tools. The initial trust relationship is less clearly represented. In that pattern the model-serving endpoint would itself be the adversarial component: operated by an attacker from the outset, or taken over after legitimate deployment. Our honeypot was neither — it demonstrates the discovery, relabeling, adoption, and data-exposure path such an operator could exploit. AI Supply Chain Compromise (AML.T0010) is the closest existing technique, but its current sub-techniques do not explicitly cover a remote inference service adopted as an agent's model backend. The data-return path is also distinct from Exfiltration via AI Agent Tool Invocation (AML.T0086), which describes exfiltration through write-capable tools; here a file read would return to the rogue endpoint through the agent's normal inference loop.

We are submitting this pattern to the MITRE ATLAS team for consideration — tentatively a Model Serving Endpoint sub-technique, with AML.T0010 as the closest current parent — supported by this real-world honeypot observation. This mapping is a secondary, analytical note: the finding stands on its own regardless of where ATLAS ultimately places it. The honeypot observation demonstrates exposure of real agent context and tool capabilities; it does not demonstrate malicious tool execution, which remains the risk scenario evaluated here, not an observed event.

--
Renato Marinho
LinkedIn|Twitter


文章来源: https://isc.sans.edu/diary/rss/33298
如有侵权请联系:admin#unsafe.sh