Malware

PoeLLM hides its next control address in a poem

PoeLLM derives its control-server address from a poem on GitHub and uses compromised AI and open-source service hosts for mining and further attacks. Recovery needs to stop the malicious process, restrict exposed services and repair authorization, while old-IP blocks leave an opportunity to reconnect.

Warm-paper conceptual illustration of a marked poetry book connected to a server whose side mechanism drives a mining device.
In this article

When an attacker moves a control server, how do already infected hosts find its new address? PoeLLM puts the answer in a poem on GitHub. The malware reads the text, selects four words, maps them through an embedded dictionary and assembles the resulting numbers into an IPv4 address. Change a few words and the hosts look for a different controller.

Lumen's threat research team Black Lotus Labs disclosed the investigation on October 7, naming the campaign Canto Incognito and the malware PoeLLM. Activity dates back to at least April and targets internet-exposed AI services and other open-source applications. Compromised hosts mine cryptocurrency and help find and attack further servers. Operators need to reclaim the host's compute, outbound connections and authority to start programs.

The poem separates finding the controller from the controller itself. An old IP can disappear while the text file still supplies the next destination. Investigation should therefore trace the local process reading the file and making the connection, with known-address blocking as one intermediate action. GitHub remains an ordinary software collaboration platform. Blocking the entire site can interrupt builds and updates while leaving the program on the host intact.

1 The poem supplies the next destination

The published analysis describes a fairly rigid text parser. It locates the first three words between fixed surrounding phrases, ignoring letter case. For the fourth word, it searches backward from of distant servers and requires the immediately before the word. Each word corresponds to a byte in a dictionary embedded in the program. Joining four bytes produces the connection address. The program's predefined parsing agreement gives the text its control function. The meaning a human reader finds in the poem plays no part in the calculation.

Invented dictionary entries and the documentation-only 192.0.2.0/24 range make the process easy to follow. Four words map to 192, 0, 2 and 10, producing 192.0.2.10 in our local model. Replacing only the last word with another registered entry changes the result to 192.0.2.11. Unrelated text changes leave it unchanged. This is a string-processing demonstration, with no malware execution or connection to any address.

Three stages locate four words, map them to four numbers and assemble the documentation example address 192.0.2.10.
Figure 1: An explanatory text-to-address model using invented words and a documentation address. The program must recognize both the words and their locating rules for a text change to produce a valid destination. Open full-size image.

This structure makes changing addresses inexpensive for the operator. A defense that retains only the IP derived at one moment can lose its target after the file changes. The parser's fixed phrases and embedded dictionary also leave more stable investigation features. Preserve the file version actually fetched, the requesting program and its subsequent destination to establish whether a GitHub request and later control traffic belong to the same sequence.

The investigated poem lived in an attacker-maintained fork of the nodejs.org repository, inside an added dash.css file. The public account identifies no modification of upstream Node.js code or its website. A CSS extension supplies no trust guarantee either. Its operational meaning comes from the reader, the extracted value and the action that follows.

2 Why a connection test starts a program

Before concealing an address, the attacker needs a program running on the server. The PoeLLM investigation found exploitation logic targeting LiteLLM's /mcp-rest/test/connection in one sample and identified CVE-2026-42271 as a likely entry path. Request and execution records from an individual host are still needed to establish its actual entry. LiteLLM's own advisory explains the endpoint's authorization failure.

LiteLLM manages model requests and can connect to MCP tools. MCP's stdio transport starts a local program and communicates through its standard input and output. Testing a submitted MCP configuration can therefore create a subprocess as part of the intended feature. The important permission difference is who may supply that configuration. The vendor advisory identifies >=1.74.2, <1.83.7 as affected. Low-privilege users holding a valid proxy API key could invoke the test endpoints without an administrator-role check.

In the pinned vulnerable rest_endpoints.py, lines 1021–1042, test_connection() receives the authenticated identity and passes the submitted configuration directly to a general execution helper. Lines 918–960 copy the request's command, args and env into a temporary MCPServer, create a client and run the test. Fields that arrived in an HTTP request have become local program-launch configuration.

Downstream, experimental_mcp_client/client.py, lines 240–248, passes those values to StdioServerParameters and stdio_client. Entering that communication context during the test runs the program with the proxy process's privileges. Whether the tool is subsequently saved successfully has no bearing on the earlier launch. A preview endpoint also needs to enforce execution authority.

The repaired test endpoints require PROXY_ADMIN and return 403 for other roles before creating the test session. The repair also adds a command allowlist during client construction. Administrators can still configure interpreters and their arguments, so the service process should have limited host privileges. The role check closes the shortcut from a low-privilege user to program launch. The runtime environment constrains which files and credentials an approved tool may affect.

1.83.7 is the historical first-fixed floor for this issue. As of October 13, the latest official non-prerelease is v1.105.0. Current upgrades should evaluate that maintained release and its dependencies, without treating the six-month-old repair floor as the installation recommendation. If an upgrade must wait, the vendor recommends blocking POST /mcp-rest/test/connection and POST /mcp-rest/test/tools/list at the gateway, at the cost of connection and tool-list previews before adding a tool. Acceptance should check rejection for ordinary users and continued operation of administrators' existing legitimate tools. Keep those endpoints restricted if the upgrade fails. Restoring business service should not reopen a vulnerable old test interface.

3 A compromised server goes looking for another

Mining explains the revenue. Black Lotus Labs also observed some controlled servers becoming scanning and exploitation nodes. AI and document-conversion services often run continuously, have compute resources and have legitimate reasons to reach external networks. An attacker who obtains execution can reuse those ordinary conditions. High CPU consumption attracts attention as a performance problem, while scanning may appear in a different set of network logs. Even after CPU use returns to normal, check whether this host made related outbound requests.

Black Lotus Labs analyzed the sample's poem retrieval, control-address calculation and malicious-file downloads. Network traffic separately showed controlled hosts making large numbers of connections to ports 3000 and 4000, associated with the Gotenberg and LiteLLM deployments investigated. On a local host, begin with the process retrieving that file, then follow its subsequent destination, downloaded file and child processes, and check for corresponding outbound requests. Ports and banners help identify the applications that may be present. Execution records are needed to establish whether a particular exploit succeeded.

The pinned IOC file lists 12 control addresses, their observed time windows and three sample hashes. It explicitly defines Present as October 7. The entry for 92.119.165[.]74, for example, runs from July 1 through July 23. Those objects support retrospective traffic searches in their relevant periods. Removing the end date and attributing every host contacting the same IP today to this campaign would discard information needed for that judgment.

File names also need content and process context. The published sample name libgcrypt borrows the name of a familiar cryptographic library. A matching name still calls for the full hash, actual path, file type and launching process. Once malicious execution is established, extend the investigation along what the process could read and which internal services it could reach. Model-provider keys, configuration files and service tokens are reachable assets to examine. Their actual disclosure requires logs or other evidence.

4 Installation leaves the access question open

Deployment can slip past an important unresolved step. The container starts, a browser opens the service and the application can call it, so installation is considered finished. The listening address and port-publication setting answer a different question. Is access limited to a program on the same host, applications on the container network, or anyone who can reach the machine? Functionality and access scope need separate acceptance checks.

The Gotenberg installation guide explicitly requires keeping the service behind a firewall. Its 3000:3000 example publishes a host port. The documentation separately provides 127.0.0.1:3000:3000 for deployments needing same-host access only. Services on the same Compose network can also use gotenberg:3000 without adding a published host port. The correct choice depends on where legitimate callers run. After changing it, complete a conversion from that caller. A local health check leaves the caller's access untested.

Ollama's authentication documentation distinguishes the local API from its cloud API. The default local address, http://localhost:11434, requires no authentication. The cloud service uses separate API keys. Binding the local service to an external interface does not cause the cloud key to add access control to that new entrance. Environments requiring remote callers need explicit restrictions on callers and authority, with listening sockets, container ports, reverse proxies and firewalls expressing the same intended scope.

After closing an unnecessary external port, complete a real request from the existing internal caller. After adding authentication, check missing credentials, low-privilege users and the legitimate workflow separately. Model downloads and software updates still need outbound access. Preserve those paths according to their initiating program and purpose so repair does not also sever normal dependencies.

5 Return the server to its own work

When related execution is found, restrict the host's outbound access and exposed entrances, preserve process, startup, file, container-configuration and network records, and establish the earliest confirmed intrusion time. Terminating a busy process before recording its path and parent-child relationships loses runtime evidence and leaves other launch points unexamined. Rebuild an environment confirmed to be remotely controlled from trusted images or packages. Review its configuration and mounts before restoration so the original execution entrance does not return with them.

Credential handling should follow reachability. Rotate keys readable by the compromised process and narrow permissions to actual use, then verify replacements through normal application requests. Restored model calls are one result. Also observe whether the service recreates unfamiliar children, reads the address file or connects to unapproved destinations. Keep a baseline of expected programs so a new anomaly cannot disappear into the explanation that this service normally needs internet access.