PoeLLM / Canto Incognito: Poem-C2 Mining Botnet on Exposed AI Servers
Executive Summary
PoeLLM is a Linux ELF botnet implant tracked by Lumen Black Lotus Labs as the Canto Incognito campaign. It has been active since at least April 2026 (first poem commit 2026-04-13) and has impacted 3,400+ victim servers, with a mid-June peak of nearly 2,200 affected servers and roughly 800 active per day. Victims are mostly in the United States and Western Europe. Lumen assesses with moderate confidence that an Italian-speaking, financially motivated actor is behind it.
Entry and targets: the actor scans for and exploits internet-facing open-source and AI services: LiteLLM (default port 4000), Ollama, Gotenberg (port 3000) and Gitea, with possible targeting of Ivanti Sentry. The LiteLLM path is a crafted POST to /mcp-rest/test/connection (command injection, CVE-2026-42271) that tells the target to download the payload from port 81 on the C2. Lumen first found the infrastructure while investigating Ivanti Sentry CVE-2026-10520.
Tradecraft: the payload is an ELF named libgcrypt with a remote shell, XMRig and Iron miners, HTTP/S scanning and exploit delivery. Victims mine to Kryptex endpoints and are reused as scanners and exploit servers, which is how the botnet grows. Its signature trick is the C2 dead drop: the current C2 IPv4 address is derived from four words in a poem titled "On the Nature of Connection" in dash.css of a GitHub repo owned by ejejejdfbbebe (a fork of the nodejs.org source). Changing four words rotates every victim to a new C2, so IP blocklists age quickly. Many C2s were compromised routers running Boa web servers with vulnerable admin pages (CVE-2018-21027 and CVE-2018-21028 are named for the first C2 router; Lumen saw no direct evidence they were exploited by the actor).
Defender priority: this pack is built for the Falcon Linux sensor. First find every internet-reachable LiteLLM, Gotenberg, Gitea, Ollama and Ivanti Sentry instance and patch or hide it (Q10, section 12). Then hunt the host-side signals that survive C2 rotation: a service process spawning a shell or downloader (Q5), a fetch from port 81 (Q6), a server talking to a mining pool (Q3, Q9), and a server fanning out to many public hosts on 3000, 4000 or 22 (Q8). Treat the IP list (Q1) as a retro-hunt, not the defence.
Source Review & Web Hunter Notes
| Tier | Source | Key finding | Carry forward |
|---|---|---|---|
| Primary | Lumen Black Lotus Labs: Canto incognito: tracking the PoeLLM malware | Original research. Campaign and malware names, poem C2 mechanism, 12 C2 IPs with dates, Kryptex endpoints, sample hash, targets, CVEs, ports, attribution. | yes |
| Corroborating | The Hacker News (2026-10-07) | Reports Lumen's findings plus a Lumen engineer quote on poem rotation. Confirms 3,400+ victims, GitHub repo name, targets, XMRig and Iron, Kryptex. | partial |
| Corroborating | Help Net Security (2026-10-08) | Reports Lumen's findings. Confirms dash.css, the endpoint path, ports 3000 and 4000, the Ivanti Sentry CVE and the 11 poem edits. | partial |
Web-hunter notes and decisions
- Independence: both news articles derive from the Lumen report and add no atomic indicators of their own. Every atomic IOC is therefore single-sourced, so every indicator in this pack ships detect. None is eligible for prevent or critical.
- Raw text, not summaries: the three pages were fetched and their plain text saved verbatim as source snapshots (
PoeLLM-CantoIncognito-Hunt-sources/); the provenance gate reads those files. - Corrections to the request brief, after checking the primary source: (1) port 81 is the payload download port, not a beacon port; the beacon ports are 3778, 5001, 5002 and 9999. (2) CVE-2018-21027 and CVE-2018-21028 are flaws in the router admin pages of C2 hosts; Lumen reports no direct evidence the actor exploited them, so they are not listed as PoeLLM exploits. (3) Ivanti Sentry is "possible targeting" in Lumen's words, tied to CVE-2026-10520 through one compromised victim. (4) Lumen's own text gives both "more than 3,400 victim servers" and "almost 2,200" for the mid-June peak; both figures are reported as stated.
- Not in the sources: no persistence mechanism, no Iron miner binary name or hash, no fixed-version numbers for CVE-2026-42271 or CVE-2026-10520, and no Ollama CVE. These are flagged as gaps rather than filled in.
- Prompt-injection check: no embedded instructions or requests directed at an AI agent were found in any fetched page. No source was disqualified.
- Actor admin host: Lumen assesses, with moderate confidence, that an Italy-hosted server running Prometheus and Uptime Kuma is the operator's management interface. No victim contacts it, so it has no endpoint-detection value and its address is deliberately not shipped as an indicator.
- Dedupe: name and IOC checks against the published library were clear. The published LiteLLM and Ivanti Sentry packs cover the CVEs; this pack covers the PoeLLM campaign and shares none of their indicators.
Hunt Brief & Attack Chain
Hypotheses (ordered by fidelity)
| # | Hypothesis | MITRE | Telemetry | Confidence |
|---|---|---|---|---|
| H1 | A Linux host executes an ELF named libgcrypt or the Lumen sample hash. | T1059.004, T1036.005 | Process creation (Falcon: ProcessRollup2) | High (Q4) |
| H2 | A server connects to a Kryptex mining endpoint or resolves the Kryptex node. | T1496 | Network connections and DNS lookups (Falcon: NetworkConnectIP4, DnsRequest) | High (Q3, Q2) |
| H3 | A command or shell fetches a payload from a C2 port 81 or the /private/ tool paths. | T1105 | Process creation (Falcon: ProcessRollup2) | High (Q6) |
| H4 | An exposed AI or dev service is exploited and spawns a shell or downloader. | T1190, T1059.004 | Process creation (Falcon: ProcessRollup2) | Medium (Q5) |
| H5 | A compromised server fans out to many public hosts on 3000, 4000 or 22. | T1595.002, T1110 | Network connections (Falcon: NetworkConnectIP4) | Medium (Q8) |
| H6 | A server contacts known C2 or supporting infrastructure. | T1071, T1584.008 | Network connections and DNS lookups (Falcon: NetworkConnectIP4, DnsRequest) | High for active IPs, lower once rotated (Q1, Q2, Q7) |
| H7 | A miner runs on the host (XMRig, stratum command lines). | T1496 | Process creation (Falcon: ProcessRollup2) | Medium (Q9) |
| H8 | The vulnerable services are reachable on their default ports. | T1190 | NetworkListenIP4 | Low, posture only (Q10) |
Attack chain
| Step | Behavior | Telemetry | Hunt angle |
|---|---|---|---|
| 1 Dead drop | Malware reads the poem in dash.css on GitHub and derives the C2 IPv4 from four words | Proxy, TLS or DNS logs (not endpoint) | Section 7 native hunt; gap in Falcon |
| 2 Scan | Infected servers and the operator scan the internet, mainly ports 3000 and 4000 | Network connections (Falcon: NetworkConnectIP4) | Q8 |
| 3 Exploit | Crafted POST to /mcp-rest/test/connection (LiteLLM) tells the target to download from C2 port 81 | Web/app logs; ProcessRollup2 for aftermath | Section 7, Q5, Q6 |
| 4 Install | ELF libgcrypt downloaded and run | Process creation (Falcon: ProcessRollup2) | Q4, Q6 |
| 5 C2 beacon | Beacons to a C2 on 3778, 5001, 5002 or 9999 | Network connections (Falcon: NetworkConnectIP4) | Q1, Q7 |
| 6 Monetise | XMRig and Iron miners connect to Kryptex | NetworkConnectIP4, DnsRequest, ProcessRollup2 | Q2, Q3, Q9 |
| 7 Reuse | Victim becomes scanner and exploit server; SSH and login-portal brute force experiments | Network connections (Falcon: NetworkConnectIP4) | Q8 |
Consolidated IOC Table
All indicators trace to the Lumen report (snapshot 01). Action vocabulary: detect alerts, hunt is run manually. Nothing is prevent: every atomic indicator is single-sourced because the news articles only restate Lumen. The poem rotates the C2, so treat IPs as perishable.
| Type | Value | Confidence | Action | Context | Expiry |
|---|---|---|---|---|---|
| IPv4 | 92.119.164.50 | high | detect | Active C2 per Lumen (first seen 2026-07-23). Single source | 2027-04-08 |
| IPv4 | 103.249.201.108 | high | detect | Active C2 per Lumen (first seen 2026-08-13). Single source | 2027-04-08 |
| IPv4 | 178.128.14.204 | high | detect | Active C2 per Lumen (first seen 2026-06-16). Single source | 2027-04-08 |
| IPv4 | 191.37.28.160 | medium | detect | Historical C2, active April 13 to May 16, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 89.39.253.46 | medium | detect | Historical C2, active April 14 to June 24, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 120.224.114.212 | medium | detect | Historical C2, active May 9 to September 8, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 5.78.73.122 | medium | detect | Historical C2, active June 8 to August 22, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 15.204.178.28 | medium | detect | Historical C2, active June 14 to August 17, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 92.119.165.74 | medium | detect | Historical C2, active July 1 to July 23, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 45.133.73.28 | medium | detect | Historical C2, active July 18 to July 22, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 185.132.53.158 | medium | detect | Historical C2, active July 19 to September 12, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 136.148.69.233 | medium | detect | Historical C2, active September 12 to September 25, 2026. Router or VPS, may be reassigned | 2027-01-08 |
| IPv4 | 57.131.5.211 | medium | detect | Server on port 80 hosting malwarescan.xyz, contacted by the first C2 | 2027-01-08 |
| Domain | malwarescan.xyz | medium | detect | Registered February 2026; likely operator-owned | 2027-01-08 |
| IPv4 | 5.180.174.162 | medium | detect | Kryptex endpoint, port 8029. Shared third-party service | 2027-01-08 |
| IPv4 | 46.21.245.211 | medium | detect | Kryptex endpoint, port 7029. Shared third-party service | 2027-01-08 |
| Domain | edge-ru-07.kryptex.network | medium | detect | Kryptex node hostname for the endpoint above | 2027-01-08 |
| SHA256 | 6fab94577364beec314afae3b082dd680933f08a8349b9f35b92667e8231b501 | high | detect | ELF sample named libgcrypt analysed by Lumen. Single source, so detect not prevent | 2027-04-08 |
| Filename | libgcrypt | medium | hunt | ELF basename with no extension; the genuine library is libgcrypt.so.N | 2027-01-08 |
| Ports | 3778, 5001, 5002, 9999 | medium | hunt | Beacon ports to the C2. 5001 and 9999 are common, so behavioral only | n/a |
| Port | 81 | medium | hunt | Payload download port on the C2 | n/a |
| URL path | hxxp://5.78.73[.]122:81/private/bins.sh | medium | hunt | Malicious URL listed in VirusTotal for the early C2 (per Lumen) | 2027-01-08 |
| URL path | hxxp://5.78.73[.]122:81/private/python3.6 | medium | hunt | As above; both /private/ paths recurred on later C2s | 2027-01-08 |
| GitHub account | github.com/ejejejdfbbebe | high | hunt | Hosts the poem in dash.css (fork of the nodejs.org site source). Do not block GitHub; hunt for fetches of this repo | n/a |
Affected Surface & Telemetry Matrix
| Surface | Required telemetry | Priority | Gap risk |
|---|---|---|---|
| Linux servers running LiteLLM, Ollama, Gotenberg, Gitea (VM, bare metal, container host) | Process creation, network connections, listening ports and DNS lookups on Linux (Falcon Linux sensor: ProcessRollup2, NetworkConnectIP4, NetworkListenIP4, DnsRequest) | High | Containers: the sensor sees the host kernel, so confirm container processes appear with the host aid; DnsRequest coverage varies by sensor version |
| Ivanti Sentry appliance | Appliance logs and netflow | Medium | No sensor on the appliance; use native logs and network telemetry |
| Routers with vulnerable admin pages used as C2 (Boa web server) | Netflow, firewall logs | Low | Unmanaged third-party devices; only your side of the traffic is visible |
| Proxy, DNS and egress firewall | Web proxy URL logs, DNS logs, flow logs | Medium | The GitHub poem fetch is only visible here, if TLS inspection or URL logging exists |
ATT&CK Mapping
| Tactic | Technique | Observed behavior | Query or control |
|---|---|---|---|
| Initial Access | T1190 Exploit Public-Facing Application | LiteLLM CVE-2026-42271 via /mcp-rest/test/connection; Gotenberg, Gitea, Ollama, Ivanti Sentry targeted | Q5, Q10, section 7, section 12 |
| Execution | T1059.004 Unix Shell | Remote shell in the ELF; shell spawned by the exploited service | Q4, Q5 |
| Command and Control | T1105 Ingress Tool Transfer | Payload fetched from C2 port 81 | Q6 |
| Command and Control | T1102.001 Dead Drop Resolver | C2 IPv4 derived from poem words on GitHub | Section 7 (gap in Falcon) |
| Command and Control | T1071 / T1571 | Beacons on 3778, 5001, 5002, 9999 | Q1, Q7 |
| Resource Development | T1584.008 Network Devices | Compromised Boa routers reused as C2 and malware hosts | Section 7 netflow |
| Reconnaissance | T1595.002 Vulnerability Scanning | Infected hosts scan for ports 3000 and 4000 | Q8 |
| Credential Access | T1110 Brute Force | Experimental distributed brute force at SSH and login portals (maturity uncertain) | Q8 |
| Impact | T1496 Resource Hijacking | XMRig and Iron mining to Kryptex | Q2, Q3, Q9 |
| Defense Evasion | T1036.005 Match Legitimate Name | Payload named libgcrypt, mimicking a real library | Q4 |
Native Audit-Log Hunts
| Source | What to check | Why |
|---|---|---|
| Web proxy / TLS inspection / egress logs | Server-class hosts requesting dash.css from the ejejejdfbbebe GitHub repo (github.com or raw.githubusercontent.com). Servers should not read this file. | Direct sign of the poem dead drop. Do not block GitHub itself. |
| LiteLLM access logs / reverse proxy | POST requests to /mcp-rest/test/connection, especially from unknown public sources or followed within seconds by a new outbound connection | CVE-2026-42271 exploitation path named by Lumen |
| Gotenberg, Gitea, Ollama access logs | Unexpected POSTs from scanning IPs; requests on ports 3000 and 4000 from many unrelated sources | Initial scan and exploit traffic |
| Ivanti Sentry (MICS) logs and appliance egress | Outbound connections from the appliance to unfamiliar public hosts and new scanning behavior after patch-level checks for CVE-2026-10520 | Lumen discovered PoeLLM through a compromised Sentry that began scanning |
| NetFlow / firewall | Any flow to the Section 4 addresses; internal servers sending port 81 flows to public hosts; high-fan-out sources | Retro-hunt where there is no sensor |
| External attack-surface scan | Ports 3000, 4000 and the Ollama listener reachable from the internet; Boa web servers on unusual ports such as 2222 on your edge routers | Gotenberg's own guide warns against exposure; Lumen found a Boa router on port 2222 |
| Linux host (read-only) | ss -ltnp for unexpected listeners; look for an executable named libgcrypt outside library directories; review crontab and systemd units (the sources document no persistence, so this is a precaution) | Triage on hosts without full sensor coverage |
Hunt Queries: CrowdStrike CQL and SentinelOne PowerQuery
Use your tenant's own address. SentinelOne does not document a link that pre-fills a query, so each "Copy & open SentinelOne" button copies the query and opens this console: paste it into PowerQuery.
All queries target the Falcon Linux sensor (event_platform=Lin) and use only documented events: ProcessRollup2, SyntheticProcessRollup2, NetworkConnectIP4, NetworkListenIP4 and DnsRequest. Set the time window in the console; each query records its intended lookback in a // LOOKBACK comment.
Looks for: Any Linux sensor outbound connection to the 12 Lumen-published C2 addresses or the supporting server that hosts malwarescan.xyz. FP: the nine historical C2s were compromised routers or short-lived VPS hosts and may have been reassigned since; judge hits against the active windows in section 4.
Known limitations and deployment notes
- PowerQuery: A connection through a proxy or NAT the agent did not observe as a direct IP Connect is not captured.
- PowerQuery assumes: endpoint.os holds the lowercase value linux on Linux agents.
- PowerQuery assumes: The acting process on an IP Connect event is src.process.*, so src.process.name is the process that made the connection.
// HUNT: PoeLLM C2 and supporting infrastructure sweep (Linux outbound) // MITRE: T1071.001, T1571, T1584.008 // CONF: high FP: medium COST: low | REQUIRES: Falcon Linux sensor, NetworkConnectIP4 // FALSE POSITIVES: historical C2 addresses were compromised routers and rented servers that may now belong to unrelated owners // TUNING: for the nine historical addresses compare the hit timestamp to the active window in section 4 and escalate only inside that window; the three active addresses and the supporting server need no exclusion // LOOKBACK: 30d (set in the console time picker) #event_simpleName=NetworkConnectIP4 event_platform=Lin | in(field=RemoteAddressIP4, values=["92.119.164.50", "103.249.201.108", "178.128.14.204", "191.37.28.160", "89.39.253.46", "120.224.114.212", "5.78.73.122", "15.204.178.28", "92.119.165.74", "45.133.73.28", "185.132.53.158", "136.148.69.233", "57.131.5.211"]) | table([@timestamp, aid, ComputerName, RemoteAddressIP4, RemotePort, ContextProcessId_decimal]) | sort(@timestamp, order=desc)
3 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typeendpoint.os: Operating systemdst.ip.address: Remote address
Recommended fields
event.time: Event timeendpoint.name: Hostnamesrc.process.name: Connecting process
Optional fields
dst.port.number: Remote port
Fallbacks
agent.uuid→endpoint.name: Changes the host identifier only.
Readiness test
event.type = 'IP Connect' endpoint.os = 'linux' dst.ip.address = * | limit 5 | columns event.time, endpoint.os, dst.ip.address
// HUNT: PoeLLM C2 and supporting infrastructure sweep (Linux outbound)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1071.001, T1571, T1584.008
// CONF: high | FP: medium
// FALSE POSITIVES: historical C2 addresses were compromised routers and rented servers that may now belong to unrelated owners
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate as the CQL: Linux outbound connections to the thirteen published PoeLLM C2 and supporting addresses, on the SentinelOne IP Connect event.
// REQUIRES: SentinelOne agent on Linux with network (IP Connect) telemetry in the Singularity Data Lake
// TUNING: For the nine historical addresses compare the hit time to the active window in section 4 and escalate only inside that window; the three active addresses and the supporting server need no exclusion.
// LOOKBACK: 30d; set the console time picker or the LRQ window
// CONFIGURATION: No custom detection or lookup objects required.
// KNOWN GAPS: A connection through a proxy or NAT the agent did not observe as a direct IP Connect is not captured.
// VALIDATION LEVEL: static-reviewed
event.type = 'IP Connect' endpoint.os = 'linux' dst.ip.address in ('92.119.164.50', '103.249.201.108', '178.128.14.204', '191.37.28.160', '89.39.253.46', '120.224.114.212', '5.78.73.122', '15.204.178.28', '92.119.165.74', '45.133.73.28', '185.132.53.158', '136.148.69.233', '57.131.5.211')
| columns event.time, endpoint.name, agent.uuid, dst.ip.address, dst.port.number, src.process.name, src.process.pid
| sort -event.time
| limit 200Looks for: Linux hosts resolving the operator-linked domain or the Kryptex node hostname that PoeLLM victims contacted.
Known limitations and deployment notes
- PowerQuery: Lookups made over DNS-over-HTTPS or by a resolver the agent does not observe are not captured.
- PowerQuery assumes: event.dns.request (pySigma mapping) is the populated lookup-name field; some schema versions carry dns.request instead.
- PowerQuery assumes: The matches operator is case-insensitive by default, mirroring the CQL /i flag.
- PowerQuery assumes: Linux agents emit DNS Resolved events for the lookups of interest.
// HUNT: DNS lookups for the operator domain and the Kryptex node // MITRE: T1071.004, T1496 // CONF: medium FP: low COST: low | REQUIRES: Falcon Linux sensor with DnsRequest telemetry // FALSE POSITIVES: researchers or sandboxes resolving the published indicators; a sanctioned Kryptex user on the same host would also match the second name // LOOKBACK: 30d (set in the console time picker) #event_simpleName=DnsRequest event_platform=Lin | DomainName=/(^|\.)(malwarescan\.xyz|edge-ru-07\.kryptex\.network)$/i | table([@timestamp, aid, ComputerName, DomainName, ContextProcessId_decimal]) | sort(@timestamp, order=desc)
3 required fields · View requirements
Required telemetry
- DNS Resolved
Required fields
event.type: Event typeendpoint.os: Operating systemevent.dns.request: Looked-up name
Recommended fields
event.time: Event timeendpoint.name: Hostnamesrc.process.name: Requesting process
Fallbacks
event.dns.request→dns.request: Same value under the alternate field name used by some schema versions.
Readiness test
event.type = 'DNS Resolved' endpoint.os = 'linux' event.dns.request = * | limit 5 | columns event.time, endpoint.os, event.dns.request
// HUNT: DNS lookups for the operator domain and the Kryptex node // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1071.004, T1496 // CONF: medium | FP: low // FALSE POSITIVES: researchers or sandboxes resolving the published indicators; a sanctioned Kryptex user on the same host would also match the second name // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate as the CQL: Linux DNS lookups for malwarescan.xyz, edge-ru-07.kryptex.network or any subdomain, on the SentinelOne DNS Resolved event. // REQUIRES: SentinelOne agent on Linux with DNS Resolved telemetry in the Singularity Data Lake // TUNING: Exclude analyst sandboxes and research hosts that resolve the published indicators on purpose. // LOOKBACK: 30d; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: Lookups made over DNS-over-HTTPS or by a resolver the agent does not observe are not captured. // VALIDATION LEVEL: static-reviewed event.type = 'DNS Resolved' endpoint.os = 'linux' event.dns.request matches '(^|\\.)(malwarescan\\.xyz|edge-ru-07\\.kryptex\\.network)$' | columns event.time, endpoint.name, agent.uuid, event.dns.request, src.process.name, src.process.pid | sort -event.time | limit 200
Looks for: Server connections to the two Kryptex endpoint and port pairs Lumen observed from PoeLLM victims (5.180.174.162:8029, 46.21.245.211:7029). A server talking to a mining pool is the payoff signal.
Known limitations and deployment notes
- PowerQuery: Mining traffic relayed through a proxy the agent did not observe as a direct connection is not captured.
- PowerQuery assumes: dst.port.number is stored as a number, so the comparison uses numeric literals.
- PowerQuery assumes: endpoint.os holds the lowercase value linux on Linux agents.
// HUNT: Kryptex mining endpoint egress from servers // MITRE: T1496, T1071 // CONF: high FP: low COST: low | REQUIRES: Falcon Linux sensor, NetworkConnectIP4 // FALSE POSITIVES: an authorised mining rig or a lab host; neither belongs on a production server // LOOKBACK: 30d (set in the console time picker) #event_simpleName=NetworkConnectIP4 event_platform=Lin | (RemoteAddressIP4=5.180.174.162 RemotePort=8029) or (RemoteAddressIP4=46.21.245.211 RemotePort=7029) | table([@timestamp, aid, ComputerName, RemoteAddressIP4, RemotePort, ContextProcessId_decimal]) | sort(@timestamp, order=desc)
4 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typeendpoint.os: Operating systemdst.ip.address: Remote addressdst.port.number: Remote port
Recommended fields
event.time: Event timeendpoint.name: Hostnamesrc.process.name: Connecting process
Fallbacks
agent.uuid→endpoint.name: Changes the host identifier only.
Readiness test
event.type = 'IP Connect' endpoint.os = 'linux' dst.ip.address = * dst.port.number = * | limit 5 | columns event.time, endpoint.os, dst.ip.address, dst.port.number
// HUNT: Kryptex mining endpoint egress from servers // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1496, T1071 // CONF: high | FP: low // FALSE POSITIVES: an authorised mining rig or a lab host; neither belongs on a production server // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate as the CQL: Linux connections to the two Kryptex mining endpoints, each pinned to its published port. // REQUIRES: SentinelOne agent on Linux with network (IP Connect) telemetry in the Singularity Data Lake // TUNING: No exclusion expected; an authorised mining rig or lab host is the only legitimate source. // LOOKBACK: 30d; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: Mining traffic relayed through a proxy the agent did not observe as a direct connection is not captured. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' endpoint.os = 'linux' ((dst.ip.address = '5.180.174.162' dst.port.number = 8029) or (dst.ip.address = '46.21.245.211' dst.port.number = 7029)) | columns event.time, endpoint.name, agent.uuid, dst.ip.address, dst.port.number, src.process.name, src.process.pid | sort -event.time | limit 200
Looks for: Execution of a process whose basename is exactly libgcrypt (no .so suffix, so not the legitimate shared library) or whose SHA256 matches the Lumen-analysed sample.
Known limitations and deployment notes
- PowerQuery: If tgt.process.image.sha256 is not populated on the tenant, a renamed copy of the sample is missed by the hash arm.
- PowerQuery assumes: tgt.process.image.sha256 is the Process Creation hash field that the independent pySigma backend maps; whether a tenant populates it is unproven, which the readiness test shows. If the tenant leaves it null the name arm still fires.
- PowerQuery assumes: matches is case-insensitive by default, mirroring the CQL /i flag.
// HUNT: libgcrypt ELF execution or PoeLLM sample hash // MITRE: T1059.004, T1036.005 // CONF: high FP: low COST: low | REQUIRES: Falcon Linux sensor, ProcessRollup2 // FALSE POSITIVES: the genuine libgcrypt is a shared object named libgcrypt.so.N and is loaded, not executed; an exact-name executable is not expected // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ event_platform=Lin | (FileName=/^libgcrypt$/i or SHA256HashData=6fab94577364beec314afae3b082dd680933f08a8349b9f35b92667e8231b501) | table([@timestamp, aid, ComputerName, FileName, ImageFileName, CommandLine, ParentBaseFileName, SHA256HashData]) | sort(@timestamp, order=desc)
3 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typeendpoint.os: Operating systemtgt.process.name: Created process name
Recommended fields
event.time: Event timeendpoint.name: Hostnamesrc.process.name: Creator processtgt.process.cmdline: Created command line
Optional fields
tgt.process.image.sha256: Hash. Mapped by the pySigma backend; unproven on a tenant. The name arm works without it.
Fallbacks
tgt.process.image.sha256→src.process.image.sha256: The src form is attested but describes the creating process, so it only helps for the hash on a different event type.
Readiness test
event.type = 'Process Creation' endpoint.os = 'linux' tgt.process.name = * | limit 5 | columns event.time, endpoint.os, tgt.process.name
// HUNT: libgcrypt ELF execution or PoeLLM sample hash // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1059.004, T1036.005 // CONF: high | FP: low // FALSE POSITIVES: the genuine libgcrypt is a shared object named libgcrypt.so.N and is loaded, not executed; an exact-name executable is not expected // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate as the CQL: a Linux process whose own name is exactly libgcrypt, or whose image hash is the published PoeLLM sample. On Process Creation the new process is tgt.process.*. // REQUIRES: SentinelOne agent on Linux with Process Creation telemetry in the Singularity Data Lake // TUNING: No exclusion expected: the genuine libgcrypt is a shared object that is loaded, not executed. // LOOKBACK: 30d; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: If tgt.process.image.sha256 is not populated on the tenant, a renamed copy of the sample is missed by the hash arm. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' endpoint.os = 'linux' (tgt.process.name matches '^libgcrypt$' or tgt.process.image.sha256 = '6fab94577364beec314afae3b082dd680933f08a8349b9f35b92667e8231b501') | columns event.time, endpoint.name, agent.uuid, tgt.process.name, tgt.process.image.path, tgt.process.cmdline, src.process.name, tgt.process.image.sha256 | sort -event.time | limit 200
Looks for: A LiteLLM (Python/uvicorn), Gotenberg, Gitea or Ollama parent spawning sh, bash, curl, wget or chmod. This is what a command-injection hit such as CVE-2026-42271 looks like on the host after the crafted POST tells the target to fetch a payload. FP: Gitea legitimately spawns sh for git hooks, and Python apps shell out for health checks; scope to hosts running the exposed AI services and exclude known hook and health-check command lines.
Known limitations and deployment notes
- PowerQuery: A shell spawned by a grandchild of the service (for example through an intermediate wrapper) is not matched because only the direct creator is tested.
- PowerQuery assumes: tgt.process.image.sha256 is the Process Creation hash field that the independent pySigma backend maps; whether a tenant populates it is unproven, which the readiness test shows. It is shown for triage only; it does not affect matching.
- PowerQuery assumes: matches is case-insensitive by default, mirroring the CQL /i flag.
// HUNT: AI or dev service spawning a shell or downloader // MITRE: T1190, T1059.004, T1105 // CONF: medium FP: medium COST: medium | REQUIRES: Falcon Linux sensor, ProcessRollup2 with ParentBaseFileName // FALSE POSITIVES: Gitea git hooks, Python health checks, container entrypoint scripts // TUNING: scope to a host group of exposed AI service servers and add a line such as | CommandLine!=/healthcheck|githooks/i after reviewing 7 days of hits; keep curl, wget and chmod children unexcluded // LOOKBACK: 14d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ event_platform=Lin | ParentBaseFileName=/^(python3?(\.[0-9]+)?|uvicorn|gunicorn|litellm|gotenberg|gitea|ollama)$/i | FileName=/^(sh|bash|dash|curl|wget|chmod|busybox)$/i | table([@timestamp, aid, ComputerName, ParentBaseFileName, FileName, CommandLine, SHA256HashData]) | sort(@timestamp, order=desc)
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typeendpoint.os: Operating systemsrc.process.name: Creator process nametgt.process.name: Created process name
Recommended fields
event.time: Event timeendpoint.name: Hostnametgt.process.cmdline: Created command line
Optional fields
tgt.process.image.sha256: Hash. Mapped by the pySigma backend; unproven on a tenant. Triage column only.
Fallbacks
agent.uuid→endpoint.name: Changes the host identifier only.
Readiness test
event.type = 'Process Creation' endpoint.os = 'linux' src.process.name = * tgt.process.name = * | limit 5 | columns event.time, endpoint.os, src.process.name, tgt.process.name
// HUNT: AI or dev service spawning a shell or downloader // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1190, T1059.004, T1105 // CONF: medium | FP: medium // FALSE POSITIVES: Gitea git hooks, Python health checks, container entrypoint scripts // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate as the CQL: a shell or downloader created by an AI or dev service. On Process Creation the creator is src.process.* and the new process is tgt.process.*, so the CQL parent process maps to src.process.name and the child to tgt.process.name. // REQUIRES: SentinelOne agent on Linux with Process Creation telemetry in the Singularity Data Lake // TUNING: Scope to a host group of exposed AI service servers and exclude health-check and git-hook command lines after reviewing 7 days of hits; keep curl, wget and chmod children unexcluded. // LOOKBACK: 14d; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: A shell spawned by a grandchild of the service (for example through an intermediate wrapper) is not matched because only the direct creator is tested. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' endpoint.os = 'linux' src.process.name matches '^(python3?(\\.[0-9]+)?|uvicorn|gunicorn|litellm|gotenberg|gitea|ollama)$' tgt.process.name matches '^(sh|bash|dash|curl|wget|chmod|busybox)$' | columns event.time, endpoint.name, agent.uuid, src.process.name, tgt.process.name, tgt.process.cmdline, tgt.process.image.sha256 | sort -event.time | limit 200
Looks for: curl, wget, shells or Python fetching from host:81/ or the /private/bins.sh and /private/python3.6 paths Lumen saw on PoeLLM C2s. Victims are told to download from port 81 on the C2.
Known limitations and deployment notes
- PowerQuery: A fetch performed in-process by a language runtime without a curl or wget command line is not captured.
- PowerQuery assumes: tgt.process.image.sha256 is the Process Creation hash field that the independent pySigma backend maps; whether a tenant populates it is unproven, which the readiness test shows. It is shown for triage only; it does not affect matching.
- PowerQuery assumes: matches is case-insensitive by default, mirroring the CQL /i flag.
// HUNT: Payload fetch from a C2 on port 81 or the /private/ tool paths // MITRE: T1105, T1059.004 // CONF: high FP: low COST: low | REQUIRES: Falcon Linux sensor, ProcessRollup2 with CommandLine // FALSE POSITIVES: an internal web service legitimately served on port 81 // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ event_platform=Lin | FileName=/^(curl|wget|sh|bash|dash|busybox|python3?)$/i | CommandLine=/:81\/|\/private\/(bins\.sh|python3\.6)/i | table([@timestamp, aid, ComputerName, FileName, CommandLine, ParentBaseFileName, SHA256HashData]) | sort(@timestamp, order=desc)
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typeendpoint.os: Operating systemtgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
event.time: Event timeendpoint.name: Hostnamesrc.process.name: Creator process
Optional fields
tgt.process.image.sha256: Hash. Mapped by the pySigma backend; unproven on a tenant. Triage column only.
Fallbacks
agent.uuid→endpoint.name: Changes the host identifier only.
Readiness test
event.type = 'Process Creation' endpoint.os = 'linux' tgt.process.name = * tgt.process.cmdline = * | limit 5 | columns event.time, endpoint.os, tgt.process.name, tgt.process.cmdline
// HUNT: Payload fetch from a C2 on port 81 or the /private/ tool paths // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1105, T1059.004 // CONF: high | FP: low // FALSE POSITIVES: an internal web service legitimately served on port 81 // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate as the CQL: a downloader or shell on Linux whose own command line references a port 81 URL or the /private/ tool paths. The command line of the new process is tgt.process.cmdline. // REQUIRES: SentinelOne agent on Linux with Process Creation telemetry in the Singularity Data Lake // TUNING: Exclude an internal web service that is legitimately served on port 81 by its destination host. // LOOKBACK: 30d; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: A fetch performed in-process by a language runtime without a curl or wget command line is not captured. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' endpoint.os = 'linux' tgt.process.name matches '^(curl|wget|sh|bash|dash|busybox|python3?)$' tgt.process.cmdline matches ':81/|/private/(bins\\.sh|python3\\.6)' | columns event.time, endpoint.name, agent.uuid, tgt.process.name, tgt.process.cmdline, src.process.name, tgt.process.image.sha256 | sort -event.time | limit 200
Looks for: Outbound Linux connections to ports 3778, 5001, 5002, 9999 or 81 on non-private addresses, grouped per host and destination. Lumen lists these as the beacon ports and the payload download port, but 5001 and 9999 are common, so use this to find rare destinations, not as an alert. FP: 5001 and 9999 are used by many legitimate services; review only destinations seen from one or a few hosts and exclude your own vendor and cloud ranges.
Known limitations and deployment notes
- PowerQuery: Low-confidence, high-noise hunt by design: legitimate services on these ports will appear until a baseline exclusion is applied.
- PowerQuery assumes: estimate_distinct is probabilistic, so the host count can differ slightly from the CQL distinct count, which LogScale also documents as an estimate on very large data.
- PowerQuery assumes: dst.port.number is stored as a number, so the comparison uses numeric literals.
- PowerQuery assumes: net_ipsubnet performs the same private-range exclusion as the CQL cidr list.
// HUNT: Egress to PoeLLM beacon ports on public addresses // MITRE: T1571, T1071 // CONF: low FP: high COST: medium | REQUIRES: Falcon Linux sensor, NetworkConnectIP4 // FALSE POSITIVES: legitimate services on 5001, 9999 and 81 (storage gateways, admin consoles, internal tools) // TUNING: after a 14 day baseline exclude each sanctioned destination with | !in(field=RemoteAddressIP4, values=["REPLACE_WITH_SANCTIONED_IP"]) and keep only destinations contacted by fewer than 3 hosts // LOOKBACK: 14d (set in the console time picker) #event_simpleName=NetworkConnectIP4 event_platform=Lin | RemotePort=/^(3778|5001|5002|9999|81)$/ | !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16","127.0.0.0/8","169.254.0.0/16"]) | groupBy([RemoteAddressIP4, RemotePort], function=[count(as=connections), count(aid, distinct=true, as=hosts)]) | sort(hosts, order=asc) | table([RemoteAddressIP4, RemotePort, hosts, connections])
5 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typeendpoint.os: Operating systemdst.ip.address: Remote addressdst.port.number: Remote portagent.uuid: Host identity for counting
Fallbacks
agent.uuid→endpoint.name: Host counts then depend on hostnames being unique.
Readiness test
event.type = 'IP Connect' endpoint.os = 'linux' dst.ip.address = * dst.port.number = * agent.uuid = * | limit 5 | columns event.type, endpoint.os, dst.ip.address, dst.port.number, agent.uuid
// HUNT: Egress to PoeLLM beacon ports on public addresses // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1571, T1071 // CONF: low | FP: high // FALSE POSITIVES: legitimate services on 5001, 9999 and 81 (storage gateways, admin consoles, internal tools) // EQUIVALENCE: equivalent-with-schema-assumptions - Same analytic as the CQL: Linux connections to the five beacon ports on public addresses, grouped per destination and sorted so rarely contacted destinations come first. Distinct hosts use estimate_distinct where the CQL uses count with distinct=true; both are estimates on very large data. // REQUIRES: SentinelOne agent on Linux with network (IP Connect) telemetry in the Singularity Data Lake // TUNING: After a 14 day baseline exclude each sanctioned destination with a negated in-list on dst.ip.address and keep only destinations contacted by fewer than 3 hosts. // LOOKBACK: 14d; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: Low-confidence, high-noise hunt by design: legitimate services on these ports will appear until a baseline exclusion is applied. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' endpoint.os = 'linux' dst.port.number in (3778, 5001, 5002, 9999, 81) | filter !(net_ipsubnet(dst.ip.address, '10.0.0.0/8') or net_ipsubnet(dst.ip.address, '172.16.0.0/12') or net_ipsubnet(dst.ip.address, '192.168.0.0/16') or net_ipsubnet(dst.ip.address, '127.0.0.0/8') or net_ipsubnet(dst.ip.address, '169.254.0.0/16')) | group connections = count(), hosts = estimate_distinct(agent.uuid) by dst.ip.address, dst.port.number | sort +hosts | limit 200
Looks for: A single Linux host contacting many distinct public addresses on 3000 (Gotenberg), 4000 (LiteLLM) or 22 (SSH). PoeLLM turns victims into scanning and exploit workers and has been seen directing distributed brute force at SSH and login portals. FP: legitimate scanners, bastion or jump hosts and CI runners fan out the same way; exclude them by aid or host group.
Known limitations and deployment notes
- PowerQuery: A slow scan spread over more than the lookback window stays under the threshold.
- PowerQuery assumes: estimate_distinct is probabilistic, so the target count near the threshold of 24 can differ slightly from the CQL distinct count, which LogScale also documents as an estimate on very large data.
- PowerQuery assumes: dst.port.number is stored as a number, so the comparison uses numeric literals.
// HUNT: Scanner or exploit worker fan-out to ports 3000, 4000 and 22 // MITRE: T1595.002, T1046, T1110 // CONF: medium FP: medium COST: medium | REQUIRES: Falcon Linux sensor, NetworkConnectIP4 // FALSE POSITIVES: sanctioned vulnerability scanners, jump hosts, CI runners, uptime monitors // TUNING: exclude sanctioned scanner and jump host aids with | !in(aid, values=["REPLACE_WITH_AID"]) and raise the threshold if the baseline is higher // LOOKBACK: 24h (set in the console time picker) #event_simpleName=NetworkConnectIP4 event_platform=Lin | RemotePort=/^(22|3000|4000)$/ | !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16","127.0.0.0/8","169.254.0.0/16"]) | groupBy([aid, ComputerName, RemotePort], function=[count(RemoteAddressIP4, distinct=true, as=distinctTargets), count(as=connections)]) | distinctTargets > 24 | sort(distinctTargets, order=desc) | table([aid, ComputerName, RemotePort, distinctTargets, connections])
5 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typeendpoint.os: Operating systemdst.ip.address: Remote addressdst.port.number: Remote portagent.uuid: Host identity
Recommended fields
endpoint.name: Hostname
Fallbacks
agent.uuid→endpoint.name: Grouping then depends on hostnames being unique.
Readiness test
event.type = 'IP Connect' endpoint.os = 'linux' dst.ip.address = * dst.port.number = * agent.uuid = * | limit 5 | columns event.type, endpoint.os, dst.ip.address, dst.port.number, agent.uuid
// HUNT: Scanner or exploit worker fan-out to ports 3000, 4000 and 22 // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1595.002, T1046, T1110 // CONF: medium | FP: medium // FALSE POSITIVES: sanctioned vulnerability scanners, jump hosts, CI runners, uptime monitors // EQUIVALENCE: equivalent-with-schema-assumptions - Same analytic as the CQL: a Linux host reaching more than 24 distinct public addresses on one of ports 22, 3000 or 4000. Distinct targets use estimate_distinct where the CQL uses count with distinct=true; both are estimates on very large data. // REQUIRES: SentinelOne agent on Linux with network (IP Connect) telemetry in the Singularity Data Lake // TUNING: Exclude sanctioned scanner and jump host agent.uuid values with a negated in-list and raise the threshold if the baseline is higher. // LOOKBACK: 24h; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: A slow scan spread over more than the lookback window stays under the threshold. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' endpoint.os = 'linux' dst.port.number in (22, 3000, 4000) | filter !(net_ipsubnet(dst.ip.address, '10.0.0.0/8') or net_ipsubnet(dst.ip.address, '172.16.0.0/12') or net_ipsubnet(dst.ip.address, '192.168.0.0/16') or net_ipsubnet(dst.ip.address, '127.0.0.0/8') or net_ipsubnet(dst.ip.address, '169.254.0.0/16')) | group target_count = estimate_distinct(dst.ip.address), connections = count() by agent.uuid, endpoint.name, dst.port.number | filter target_count > 24 | sort -target_count | limit 200
Looks for: Processes named xmrig or command lines with stratum+tcp, stratum+ssl or XMRig-only flags. PoeLLM ships XMRig and a second miner called Iron; Lumen publishes no Iron binary name, so only XMRig is matched by name. XMRig is a public tool, so this is detect only.
Known limitations and deployment notes
- PowerQuery: A miner renamed and launched with a configuration file instead of command-line flags matches neither arm.
- PowerQuery assumes: tgt.process.image.sha256 is the Process Creation hash field that the independent pySigma backend maps; whether a tenant populates it is unproven, which the readiness test shows. It is shown for triage only; it does not affect matching.
- PowerQuery assumes: matches is case-insensitive by default, mirroring the CQL /i flag.
// HUNT: Cryptominer execution on Linux (XMRig and stratum command lines) // MITRE: T1496 // CONF: medium FP: low COST: low | REQUIRES: Falcon Linux sensor, ProcessRollup2 // FALSE POSITIVES: sanctioned mining or benchmarking on a lab host // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ event_platform=Lin | (FileName=/^xmrig$/i or CommandLine=/stratum\+(tcp|ssl)|--donate-level|--cpu-priority/i) | table([@timestamp, aid, ComputerName, FileName, CommandLine, ParentBaseFileName, SHA256HashData]) | sort(@timestamp, order=desc)
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typeendpoint.os: Operating systemtgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
event.time: Event timeendpoint.name: Hostnamesrc.process.name: Creator process
Optional fields
tgt.process.image.sha256: Hash. Mapped by the pySigma backend; unproven on a tenant. Triage column only.
Fallbacks
agent.uuid→endpoint.name: Changes the host identifier only.
Readiness test
event.type = 'Process Creation' endpoint.os = 'linux' tgt.process.name = * tgt.process.cmdline = * | limit 5 | columns event.time, endpoint.os, tgt.process.name, tgt.process.cmdline
// HUNT: Cryptominer execution on Linux (XMRig and stratum command lines) // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1496 // CONF: medium | FP: low // FALSE POSITIVES: sanctioned mining or benchmarking on a lab host // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate as the CQL: a Linux process named xmrig, or a command line carrying stratum URLs or XMRig-specific flags. The command line of the new process is tgt.process.cmdline. // REQUIRES: SentinelOne agent on Linux with Process Creation telemetry in the Singularity Data Lake // TUNING: Exclude lab hosts where mining or benchmarking is sanctioned. // LOOKBACK: 30d; set the console time picker or the LRQ window // CONFIGURATION: No custom detection or lookup objects required. // KNOWN GAPS: A miner renamed and launched with a configuration file instead of command-line flags matches neither arm. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' endpoint.os = 'linux' (tgt.process.name matches '^xmrig$' or tgt.process.cmdline matches 'stratum\\+(tcp|ssl)|--donate-level|--cpu-priority') | columns event.time, endpoint.name, agent.uuid, tgt.process.name, tgt.process.cmdline, src.process.name, tgt.process.image.sha256 | sort -event.time | limit 200
Looks for: Which Linux hosts listen on Gotenberg (3000), LiteLLM (4000) or the Ollama default (11434, not named in the cited sources, included for exposure checking only) on a non-loopback address. This finds the attack surface, not compromise. FP: every legitimate instance matches; use the result as an inventory to check against what is actually reachable from the internet.
Known limitations and deployment notes
- PowerQuery: SentinelOne listening-port inventory is not covered by this pack; run the CQL tab on Falcon or query the application inventory.
// HUNT: Exposure inventory of hosts listening on 3000, 4000 or 11434 // MITRE: T1190 // CONF: low FP: high COST: low | REQUIRES: Falcon Linux sensor, NetworkListenIP4 // FALSE POSITIVES: every sanctioned LiteLLM, Gotenberg, Gitea or Ollama instance, and unrelated apps on port 3000 // TUNING: compare the list to your external attack-surface scan and exclude hosts that are provably internal-only // LOOKBACK: 7d (set in the console time picker) #event_simpleName=NetworkListenIP4 event_platform=Lin | LocalPort=/^(3000|4000|11434)$/ | LocalAddressIP4!=127.0.0.1 | groupBy([aid, ComputerName, LocalAddressIP4, LocalPort], function=count(as=listenEvents)) | table([aid, ComputerName, LocalAddressIP4, LocalPort, listenEvents])
1 required field · View requirements
Required telemetry
- IP Connect (nearest available event; it does not record listeners)
Required fields
event.type: Event type
Readiness test
event.type = 'IP Connect' endpoint.os = 'linux' | limit 5 | columns event.type, endpoint.os
// HUNT: Exposure inventory of hosts listening on 3000, 4000 or 11434 // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1190 // CONF: low | FP: high // FALSE POSITIVES: every sanctioned LiteLLM, Gotenberg, Gitea or Ollama instance, and unrelated apps on port 3000 // EQUIVALENCE: unsupported-without-redesign - The CQL inventories listening sockets from the Falcon NetworkListenIP4 sensor event. SentinelOne lists an IP Listen event type, but this pack's field model has no verified field names for it, and IP Connect records outbound or accepted connections, not listeners. A faithful port needs the IP Listen fields confirmed on a tenant, or a different source such as a vulnerability or application inventory, or a Remote Ops script. // REQUIRES: A listening-socket inventory source; SentinelOne lists an IP Listen event type whose fields are not verified here // TUNING: Use the CQL tab, or compare the external attack-surface scan to the hosts running LiteLLM, Gotenberg, Gitea or Ollama. // LOOKBACK: 7d on the CQL tab // CONFIGURATION: Use the CQL tab for this exposure inventory. // KNOWN GAPS: SentinelOne listening-port inventory is not covered by this pack; run the CQL tab on Falcon or query the application inventory. // VALIDATION LEVEL: static-reviewed
CrowdStrike Custom IOA Recommendations
| IOA name | Pattern | Exclusions | Deployment |
|---|---|---|---|
| PoeLLM-libgcrypt-Exec | Linux process creation where the image file name is an exact libgcrypt with no .so suffix | None expected | Host group: Linux servers; Action: Detect |
| PoeLLM-ServiceSpawnsShell | Linux process creation of sh, bash, curl or wget whose parent is the LiteLLM Python process, Gotenberg, Gitea or Ollama | Gitea git hooks; sanctioned health checks | Host group: exposed AI service servers; Action: Detect |
| PoeLLM-C2-Port81-Fetch | Process creation of curl or wget with a command line containing :81/ | Internal services on port 81 | Host group: Linux servers; Action: Detect |
| PoeLLM-Miner-Cmdline | Command line containing stratum+tcp or stratum+ssl | Sanctioned mining lab | Host group: all Linux; Action: Detect |
| PoeLLM-Kryptex-Egress | Network connection to the two Kryptex endpoint and port pairs in section 4 | None expected on servers | Host group: Linux servers; Action: Detect |
All IOAs ship as Detect. Promoting any of them to Prevent needs a second, independent source or confirmed hits from your own telemetry. XMRig is a public miner and must never be prevented by name or hash.
Machine-Readable IOC Appendix
type,value,action,severity,expiration,description,tags ipv4,92.119.164.50,detect,high,2027-04-08,PoeLLM active C2 as of Lumen publication (first seen 2026-07-23),campaign:CantoIncognito;malware:PoeLLM ipv4,103.249.201.108,detect,high,2027-04-08,PoeLLM active C2 as of Lumen publication (first seen 2026-08-13),campaign:CantoIncognito;malware:PoeLLM ipv4,178.128.14.204,detect,high,2027-04-08,PoeLLM active C2 as of Lumen publication (first seen 2026-06-16),campaign:CantoIncognito;malware:PoeLLM ipv4,191.37.28.160,detect,medium,2027-01-08,PoeLLM historical C2 (April 13 to May 16, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,89.39.253.46,detect,medium,2027-01-08,PoeLLM historical C2 (April 14 to June 24, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,120.224.114.212,detect,medium,2027-01-08,PoeLLM historical C2 (May 9 to September 8, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,5.78.73.122,detect,medium,2027-01-08,PoeLLM historical C2 (June 8 to August 22, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,15.204.178.28,detect,medium,2027-01-08,PoeLLM historical C2 (June 14 to August 17, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,92.119.165.74,detect,medium,2027-01-08,PoeLLM historical C2 (July 1 to July 23, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,45.133.73.28,detect,medium,2027-01-08,PoeLLM historical C2 (July 18 to July 22, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,185.132.53.158,detect,medium,2027-01-08,PoeLLM historical C2 (July 19 to September 12, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,136.148.69.233,detect,medium,2027-01-08,PoeLLM historical C2 (September 12 to September 25, 2026). Compromised router or VPS: may be reassigned,campaign:CantoIncognito;malware:PoeLLM;status:historical ipv4,57.131.5.211,detect,medium,2027-01-08,Server hosting malwarescan.xyz contacted by the first C2 (port 80),campaign:CantoIncognito;malware:PoeLLM domain,malwarescan.xyz,detect,medium,2027-01-08,Domain registered February 2026 on likely operator-owned infrastructure,campaign:CantoIncognito;malware:PoeLLM ipv4,5.180.174.162,detect,medium,2027-01-08,Kryptex mining endpoint (port 8029) contacted by PoeLLM victims. Shared third-party service,campaign:CantoIncognito;role:mining-pool ipv4,46.21.245.211,detect,medium,2027-01-08,Kryptex mining endpoint (port 7029) contacted by PoeLLM victims. Shared third-party service,campaign:CantoIncognito;role:mining-pool domain,edge-ru-07.kryptex.network,detect,medium,2027-01-08,Kryptex node hostname for 46.21.245.211 contacted by PoeLLM victims,campaign:CantoIncognito;role:mining-pool sha256,6fab94577364beec314afae3b082dd680933f08a8349b9f35b92667e8231b501,detect,high,2027-04-08,PoeLLM ELF sample named libgcrypt (single source: Lumen),campaign:CantoIncognito;malware:PoeLLM
This pack does not ship a SentinelOne import block: the import format could not be verified against SentinelOne's own documentation. Export a sample from your console and the block can be added with confidence.
# Poem dead drop (Lumen)
repo : github.com/ejejejdfbbebe (fork of the nodejs.org source)
file : dash.css
poem title : On the Nature of Connection
anchors : "In the silent hum of " | "each pulse of " | "Beyond the wall of " | "of distant servers"
rotation : only the four key words change; the parsing pattern stays fixed
# Exploit and staging
LiteLLM path: /mcp-rest/test/connection (CVE-2026-42271)
download : target told to fetch from C2 port 81
URL paths : hxxp://5.78.73[.]122:81/private/bins.sh
hxxp://5.78.73[.]122:81/private/python3.6
# Network
beacon ports: 3778, 5001, 5002, 9999
scan ports : 3000 (Gotenberg), 4000 (LiteLLM), plus SSH and login portals
miner pools : 5.180.174.162:8029, 46.21.245.211:7029
campaign : Canto Incognito (Lumen Black Lotus Labs) malware : PoeLLM payload : ELF named libgcrypt (sha256 in the CSV) capabilities: remote shell, HTTP/S scanning, exploit delivery miners : XMRig (public tool, detect only), Iron (no binary name published) pool : Kryptex attribution : Italian-speaking actor, moderate confidence
[ ] LiteLLM: patched for CVE-2026-42271 (confirm fixed version in the vendor advisory)
[ ] Ivanti Sentry: patched for CVE-2026-10520
[ ] Gotenberg on 3000, LiteLLM on 4000, Ollama listener: not reachable from the internet
[ ] Gitea: current release, not exposed without authentication and MFA
[ ] Boa-based edge routers: admin pages not exposed (CVE-2018-21027, CVE-2018-21028
are named for the first C2 router, not as actor exploits)
[ ] Egress from AI service hosts restricted to required destinations
[ ] SSH: key-only auth, source-restricted
Detection Validation Gates
| Gate | Check | Pass criteria |
|---|---|---|
| Telemetry ready | Process creation, network connections, listening ports and DNS lookups flowing for the AI and dev service hosts, container hosts included (Falcon Linux sensor: ProcessRollup2, NetworkConnectIP4, NetworkListenIP4, DnsRequest) | Each event type returns rows for those aids in the last 24h |
| Benign baseline | Run Q5, Q7, Q8 and Q10 for 7 to 14 days; build the allowlist for Gitea hooks, jump hosts, scanners and sanctioned destinations | Documented exclusions before any alerting |
| Positive test | On a lab Linux host copy a harmless binary to a file named libgcrypt and run it; run curl against a lab web server on port 81; run a sleep via a shell launched from a Python parent | Q4, Q6 and Q5 fire |
| Atomic sweep | Run Q1, Q2 and Q3 over 30 days | Zero hits in a clean estate; any hit on an active IP is an incident |
| Promotion | Move IOAs from Detect only after the baseline; keep every PoeLLM IOA at Detect unless a second independent source confirms | Written sign-off per IOA |
Hardening: Tiered
- Remove internet exposure of LiteLLM (4000), Gotenberg (3000), Ollama and Gitea admin surfaces: bind to loopback or a private interface and publish only through an authenticating reverse proxy or VPN. M1035 Limit Access to Resource Over Network, M1030 Network Segmentation; CIS Controls v8 4.4 (server firewall); Gotenberg's own install guide warns against internet exposure.
- Patch LiteLLM for CVE-2026-42271 and Ivanti Sentry for CVE-2026-10520, and update Gitea, Gotenberg and Ollama to current releases. Take the fixed version from the vendor advisory (not stated in the cited sources). M1051 Update Software, M1016 Vulnerability Scanning.
- Interim LiteLLM control until patched: deny
/mcp-rest/test/connectionat the reverse proxy (playbook 3). M1050 Exploit Protection; ⚠ best-practice, no formal benchmark. - Run Q1 to Q10; if any host hits, treat it as compromised and rotate the LiteLLM master key and every upstream model-provider key stored on it.
- Egress control: default-deny outbound for AI service hosts with an allowlist for the model providers and package mirrors they truly need. This removes mining-pool, C2 and scan traffic regardless of which IP the poem names. M1037 Filter Network Traffic; CIS Controls v8 13.4 and 4.4.
- Container and process hardening: non-root user, read-only root filesystem,
noexectemp storage, dropped capabilities,no-new-privileges(playbook 2). M1038 Execution Prevention, M1048 Application Isolation and Sandboxing; NIST SP 800-190, CIS Docker Benchmark. - Deploy the section 9 IOAs in Detect on the Linux fleet.
- SSH: key-only authentication, restrict by source, disable root login. This blunts the experimental brute-force capability. M1032 Multi-factor Authentication, M1036 Account Use Policies; CIS Linux Benchmark SSH section.
- Add AI/LLM and developer tooling to attack surface management and the patch cycle with named owners, as Lumen recommends. M1016 Vulnerability Scanning; CIS Controls v8 7.1 and 12.
- Consider a managed SASE or private access layer so self-hosted AI services never face the internet. M1035.
- Keep edge routers and IoT devices patched and rebooted on a schedule; PoeLLM reused routers with vulnerable admin pages as C2 hosts. M1051; vendor hardening guidance.
- Automate egress allowlist drift review and alert on first-time public destinations from AI service hosts.
Deployable Playbooks
1 · Exposure inventory (read-only)
#!/bin/bash
# Prerequisites: shell on the host; iproute2 (ss); docker CLI if containers are used.
# Reboot required: no
# Rollback: none required, read-only.
ss -H -ltn | awk '$4 ~ /:(3000|4000|11434)$/ {print "LISTEN", $4}'
docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null | grep -E '(3000|4000|11434)'
# Anything shown as 0.0.0.0 or [::] is reachable from every network the host is on.
2 · Harden the container for LiteLLM or Gotenberg (compose fragment)
# Prerequisites: a copy of the current compose file (cp compose.yml compose.yml.bak); a reverse proxy
# with authentication already in front of the service; test in staging, the app may need extra writable paths.
# Reboot required: no (container is recreated, brief service interruption).
# Rollback: cp compose.yml.bak compose.yml && docker compose up -d
services:
litellm:
ports:
- "127.0.0.1:4000:4000" # loopback only; publish through the authenticating proxy
user: "10001:10001"
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=256m
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
3 · Interim deny for the LiteLLM test-connection endpoint (nginx reverse proxy)
# Prerequisites: LiteLLM sits behind nginx; confirm nobody legitimately uses the MCP test-connection feature.
# This is a stop-gap for CVE-2026-42271, not a patch.
# Reboot required: no (nginx reload).
# Rollback: remove the location block, then nginx -t && systemctl reload nginx
location = /mcp-rest/test/connection {
return 403;
}
4 · Restrict the service ports to an admin range (nftables)
# Prerequisites: nftables; out-of-band or console access (a wrong rule can lock you out);
# replace REPLACE_WITH_ADMIN_CIDR. Docker-published ports bypass INPUT: bind to 127.0.0.1 (playbook 2) or use DOCKER-USER.
# Reboot required: no (runtime rules; persist in /etc/nftables.conf after testing).
# Rollback: nft delete table inet poellm_expose
nft add table inet poellm_expose
nft add chain inet poellm_expose input '{ type filter hook input priority -10; policy accept; }'
nft add rule inet poellm_expose input tcp dport '{ 3000, 4000, 11434 }' ip saddr != REPLACE_WITH_ADMIN_CIDR drop
5 · Log-only watch on the three active C2 addresses (no blocking)
# Prerequisites: nftables; kernel log or journald collected by your SIEM.
# Reboot required: no.
# Rollback: nft delete table inet poellm_watch
# Log only, not drop: these indicators are single-sourced (Lumen). Promote to drop after a second
# independent source or your own confirmed hits.
cat <<'EOF' | nft -f -
table inet poellm_watch {
chain out {
type filter hook output priority 0; policy accept;
ip daddr { 92.119.164.50, 103.249.201.108, 178.128.14.204 } log prefix "poellm-c2 " counter
}
}
EOF
6 · Verify
# Exposure gone: from an external vantage point, ports 3000, 4000 and the Ollama listener must not answer.
# Hardening in place:
docker inspect --format '{{.HostConfig.ReadonlyRootfs}} {{.HostConfig.CapDrop}}' litellm
# Behavior gone: re-run Q5 and Q10 in Falcon; Q10 should list only intended listeners.
Nothing in these playbooks blocks a PoeLLM indicator by IP, domain or hash. That is deliberate: every indicator is single-sourced, so network blocking of them is left to your own judgment and to Lumen's disruption of the C2s.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Network-contain the host in Falcon; remove public exposure of the service (playbooks 2 and 4); do not power off, to keep memory | SOC + Infra | Containment timestamp, firewall change record |
| Isolate: by platform | CrowdStrike: network-contain the host. SentinelOne: network quarantine (Disconnect from Network; confirm the label in your console version). Keep the host powered on for memory capture on all of them. | SOC + Infra | Containment timestamp, firewall change record |
| Preserve | Capture process list, ss -tanp, the executable behind any suspect PID (/proc/PID/exe), reverse-proxy and application logs, and the exploit request (POST to /mcp-rest/test/connection or the equivalent) | IR | Hashes of artifacts, log exports |
| Scope | Run Q1 to Q10 estate-wide for 30 days; check which other hosts show fan-out (Q8) or Kryptex contact (Q3); the infected host may have been used to attack others | IR + Threat hunting | Affected aid list |
| Eradicate | Kill the miner and libgcrypt process, remove the binary, review cron, systemd and shell profiles even though the sources document no persistence; rebuild from a clean image rather than clean in place when root was obtained | Infra | Rebuild record, clean verification scan |
| Credentials | Rotate the LiteLLM master key, upstream model-provider API keys, Gitea tokens, and any secret stored on or reachable from the host; review provider billing for unauthorised usage | Platform + Security | Rotation log, billing review |
| Recover | Redeploy patched and hardened (section 12), behind an authenticating proxy, with egress allowlist; confirm Q5 and Q10 are clean | Infra | Version and config proof |
| Monitor | Keep IOAs in Detect; re-run Q1 weekly since the poem rotates C2s; watch Lumen's GitHub IOC page for new addresses | SOC | Weekly hunt results |
Detection Coverage Map
| Technique | Behavior | CQL | IOA | CrowdStrike coverage | SentinelOne |
|---|---|---|---|---|---|
| T1059.004 / T1036.005 | libgcrypt ELF or known hash executes | Q4 | PoeLLM-libgcrypt-Exec | Good | Good, with assumptions |
| T1496 | Mining pool egress, DNS, miner process | Q2, Q3, Q9 | PoeLLM-Kryptex-Egress, PoeLLM-Miner-Cmdline | Good | Good, with assumptions |
| T1105 | Fetch from C2 port 81 or /private/ paths | Q6 | PoeLLM-C2-Port81-Fetch | Good | Good, with assumptions |
| T1190 / T1059.004 | Service spawns shell or downloader after exploitation | Q5 | PoeLLM-ServiceSpawnsShell | Partial | Partial: same limit as CrowdStrike |
| T1071 / T1571 / T1584.008 | C2 IP contact and beacon ports | Q1, Q7 | IOC import | Partial | Partial: same limit as CrowdStrike |
| T1595.002 / T1046 / T1110 | Scanner and brute-force fan-out | Q8 | n/a | Partial | Partial: same limit as CrowdStrike |
| T1190 (exposure) | Vulnerable services reachable | Q10 | n/a | Partial | GAP |
| T1190 (the exploit request itself) | Crafted POST to the service | none | n/a | GAP | GAP |
| T1102.001 | Poem fetch from GitHub | none | n/a | GAP | GAP |
| Persistence / file write | Not documented in sources; no ELF-write query shipped | none | n/a | GAP | GAP |
| Iron miner, Ivanti Sentry host | No binary name published; appliance has no sensor | none | n/a | GAP | GAP |
Coverage gaps: the exploit request and the GitHub poem fetch are network and application events that the Falcon sensor does not record; close them with reverse-proxy logs and proxy or TLS-inspection URL logs (section 7). Lumen documents no persistence mechanism and no Iron binary name, and the Ivanti Sentry appliance cannot run a sensor, so those are open. Because the poem rotates the C2 address, Q1 is a retro-hunt; Q3, Q4, Q5, Q6 and Q8 are the durable detections. Validation: see section 11.
Hunt Summary Ticket
TITLE : PoeLLM / Canto Incognito (Lumen Black Lotus Labs) SEVERITY : High. Active botnet, 3,400+ victims, exploits internet-facing AI and dev services (Linux) SCOPE : Linux servers running LiteLLM, Ollama, Gotenberg, Gitea; Ivanti Sentry (no sensor) HYPOTHESIS : Exposed service exploited (LiteLLM CVE-2026-42271) -> payload from C2 port 81 -> ELF libgcrypt -> XMRig/Iron to Kryptex -> host reused as scanner QUERIES : Q1 C2 sweep | Q2 DNS | Q3 Kryptex egress | Q4 libgcrypt/hash | Q5 service spawns shell | Q6 port-81 fetch | Q7 beacon ports | Q8 fan-out | Q9 miner | Q10 exposure DO FIRST : Find and hide exposed 3000/4000/Ollama/Gitea/Sentry; patch CVE-2026-42271 and CVE-2026-10520; run Q3-Q6 and Q8 over 30d FINDINGS : <fill from hunt> GAPS : Exploit request and GitHub poem fetch outside endpoint telemetry; no persistence or Iron binary published; Sentry has no sensor ACTIONS : Deploy IOAs (sect 9, all Detect); import CSV as detect only; egress allowlist; rotate LiteLLM and provider keys on any hit OWNER : HuntPack VERSION : v0.3 · 2026-10-09
Changelog
References
| Tier | Source | Used for | Access date |
|---|---|---|---|
| Research | Lumen Black Lotus Labs: Canto incognito: tracking the PoeLLM malware | Primary intel: campaign, mechanism, IOCs, ports, CVEs | 2026-10-08 |
| Reporting | The Hacker News: PoeLLM infects 3,400+ servers | Corroboration: victim count, targets, repo name, attribution | 2026-10-08 |
| Reporting | Help Net Security: cryptomining botnet hides C2 in a GitHub poem | Corroboration: dash.css, endpoint path, ports, CVEs | 2026-10-08 |
| Framework | MITRE ATT&CK T1102.001 Dead Drop Resolver | Technique mapping for the poem C2 | 2026-10-08 |
| Framework | MITRE ATT&CK T1496 Resource Hijacking | Technique mapping for mining | 2026-10-08 |
Generated 2026-10-09 by HuntPack · defensive hunt, detection and hardening content only