Threat Overview
ThreatDown disclosed Carbonato on September 22, 2026, and The Hacker News highlighted it on September 28. The operation compromises Linux hosts whose unauthenticated Docker Engine API is reachable on TCP/2375. The operator uses the API to create a privileged container with the host filesystem mounted and host PID/network namespaces, then invokes nsenter to execute on the host. The implant installs reverse-SSH access, adds an SSH key, persists through cron, systemd timers, rc.local, and OpenRC, and uses paired watchdogs plus immutable file attributes to restore removed components. Every five minutes it scans attached and Docker-bridge /24 networks for more open Docker APIs.
The operator deploys the legitimate Hermes Agent framework unchanged but replaces SOUL.md with a malicious GH0ST persona. Telegram carries tasks and deployment reports; the agent submits tasks to an LLM gateway and executes returned commands. The prompt prioritizes AI-provider API keys, then SSH credentials, access tokens, databases, and exposed AI endpoints. ThreatDown observed most known infrastructure online on September 3. Attribution to Costa Rica is a researcher assessment based on infrastructure, timezone, language, and handle clues, not a confirmed actor identity.
References
- ThreatDown — Carbonato: a botnet built around an AI agent — September 22, 2026.
- The Hacker News — Carbonato Botnet Compromises Docker Hosts — September 28, 2026.
Impacted Systems
- Vendor/product: Docker Engine API on Linux; the initial access condition is an unauthenticated daemon reachable over TCP/2375.
- Platform/deployment: Internet-facing or internally reachable self-managed Linux Docker hosts; cloud and on-premises hosts are both exposed if the API is reachable without mutual TLS/authentication.
- Role/exposure: Docker daemon, container host, neighboring Docker bridge or host subnets; privileged-container creation must be permitted.
- Versions: No version-specific software flaw is claimed; the exposure is unsafe configuration. Exact Docker versions are therefore not a reliable scoping control.
- Downstream components: cron, systemd timers,
rc.local, OpenRC, SSH, Hermes Agent, Telegram and external LLM/API services. - Unaffected by the stated entry path: Docker daemons not network-reachable by the attacker and APIs requiring effective authentication/authorization. This does not exclude other initial-access paths.
Why this matters
The initial-access condition is directly discoverable from the Internet, compromise provides host-level execution through Docker’s own control plane, and the worm can propagate without further operator action. Credential collection and privileged container execution make a single exposed host a material cross-environment risk for MSSP customers.
Exploitation Status
ThreatDown recovered the operation’s live toolchain from an exposed registry and documented working deployment scripts, repositories, infrastructure, persistence, and IOCs. This is confirmed operational threat activity, not a proof of concept. The public reporting does not provide a verified victim count or a definitive campaign start date. The known registry was exposed from May 2026, while archived artifacts span October 2024 through August 2026.
What this hunt looks for
Docker API access on TCP/2375, privileged-container and namespace execution, Carbonato files and masquerading, cron/systemd/OpenRC persistence, reverse SSH tunnels, Telegram or known-infrastructure egress, and repeated /24 scanning.
Required logs
Microsoft Defender for Endpoint DeviceProcessEvents, DeviceFileEvents, and DeviceNetworkEvents; Linux Syslog or audit telemetry; Docker daemon or reverse-proxy logs; and firewall or flow data for TCP/2375 and outbound connections.
Hunt 1 — High-likelihood IOC and artifact sweep
let CarbonatoIPs = dynamic(["45.79.183.61","91.99.195.164","213.136.79.115","213.136.83.197","190.211.124.187"]);
union isfuzzy=true
(
DeviceNetworkEvents
| where TimeGenerated > ago(30d)
| where RemoteIP in (CarbonatoIPs)
| project TimeGenerated, DeviceName, Evidence="Network", Detail=strcat(RemoteIP, ":", RemotePort), InitiatingProcessFileName, InitiatingProcessCommandLine
),
(
DeviceFileEvents
| where TimeGenerated > ago(30d)
| where FolderPath has_any ("/root/.hermes/", "/opt/gh0st/")
or FileName in~ ("SOUL.md", "auto-persist-host.sh", ".docker-network-monitor")
or (FileName =~ "systemd-logind" and FolderPath =~ "/usr/sbin")
| project TimeGenerated, DeviceName, Evidence="File", Detail=strcat(FolderPath, "/", FileName), InitiatingProcessFileName, InitiatingProcessCommandLine
),
(
DeviceProcessEvents
| where TimeGenerated > ago(30d)
| where ProcessCommandLine has_any ("CARBONATO_API_KEY", "GH0ST_C2", "FSOCIETY_DISABLE_TUNNEL", "GATEWAY_ALLOW_ALL_USERS", "[kworker/u2:0]")
| project TimeGenerated, DeviceName, Evidence="Process", Detail=ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
)
| order by TimeGenerated desc
Hunt 2 — Inbound access to unauthenticated Docker APIs
DeviceNetworkEvents
| where TimeGenerated > ago(30d)
| where LocalPort == 2375
| where ActionType in~ ("InboundConnectionAccepted", "ConnectionSuccess")
| where not(ipv4_is_private(RemoteIP))
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Connections=count(), RemoteIPs=make_set(RemoteIP, 50) by DeviceName, LocalIP
| order by Connections desc
Hunt 3 — Privileged container creation and host namespace access
DeviceProcessEvents
| where TimeGenerated > ago(30d)
| where FileName in~ ("docker", "dockerd", "curl", "nsenter")
| where ProcessCommandLine has_any ("--privileged", "Privileged\\\":true", "PidMode\\\":\\\"host", "NetworkMode\\\":\\\"host", "Binds\\\":[\\\"/:/host", "nsenter -t 1")
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by TimeGenerated desc
Hunt 4 — Carbonato persistence and immutable-file changes
DeviceProcessEvents
| where TimeGenerated > ago(30d)
| where FileName in~ ("crontab", "systemctl", "chattr", "rc-update", "update-rc.d", "sh", "bash")
| where ProcessCommandLine has_any (".docker-network-monitor", "auto-persist-host.sh", "/opt/gh0st/", "systemd-resolved", "chattr +i", "rc.local")
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by TimeGenerated desc
Hunt 5 — Reverse SSH tunnels and unauthorized key installation
DeviceProcessEvents
| where TimeGenerated > ago(30d)
| where (FileName in~ ("ssh", "autossh") and ProcessCommandLine matches regex @"(^|\s)-[Rr]\s")
or ProcessCommandLine has_all ("authorized_keys", "ssh-")
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by TimeGenerated desc
Hunt 6 — Telegram or known Carbonato egress from server processes
let CarbonatoIPs = dynamic(["45.79.183.61","91.99.195.164","213.136.79.115","213.136.83.197","190.211.124.187"]);
DeviceNetworkEvents
| where TimeGenerated > ago(30d)
| where RemoteIP in (CarbonatoIPs)
or RemoteUrl has_any ("api.telegram.org", "carbonato-proxy-drab.vercel.app", "carbonato-proxy-zeta.vercel.app", "carbonato-proxy-zeta-2.vercel.app")
| project TimeGenerated, DeviceName, RemoteIP, RemotePort, RemoteUrl, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by TimeGenerated desc
Hunt 7 — Repeated /24 scanning for Docker daemons
DeviceNetworkEvents
| where TimeGenerated > ago(14d)
| where RemotePort == 2375
| extend RemotePrefix = strcat(split(RemoteIP, ".")[0], ".", split(RemoteIP, ".")[1], ".", split(RemoteIP, ".")[2], ".0/24")
| summarize Targets=dcount(RemoteIP), SampleTargets=make_set(RemoteIP, 20), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by bin(TimeGenerated, 5m), DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine, RemotePrefix
| where Targets >= 10
| order by Targets desc
Hunt 8 — Linux Syslog fallback for host takeover and persistence
Syslog
| where TimeGenerated > ago(30d)
| where SyslogMessage has_any ("/containers/create", "nsenter -t 1", ".docker-network-monitor", "auto-persist-host.sh", "CARBONATO_API_KEY", "GH0ST_C2", "chattr +i", "system/resolved")
| project TimeGenerated, Computer, Facility, SeverityLevel, ProcessName, SyslogMessage
| order by TimeGenerated desc
Detection Notes
- Hunts 1, 3, and 4 are highest signal because they combine researcher-published artifacts with unusual host-control behavior. IOC matches remain time-sensitive and require enrichment before containment.
- Hunt 2 detects exposure use only when the sensor records inbound connections and the local port. A firewall permit log may be the only evidence if the Docker host lacks endpoint telemetry.
- Legitimate platform automation can use privileged containers, host mounts,
nsenter, or reverse tunnels. Validate parent process, orchestrator, image, destination, and change record. hermes-agentalone is not malicious. The altered persona, Carbonato-specific environment variables, persistence kit, or suspicious server egress provide stronger context.- Syslog often lacks full command lines unless Linux audit/process telemetry is configured. Container-only telemetry may miss commands executed in the host namespace.
- Network address translation can obscure the original scanner. Docker API/reverse-proxy logs and cloud flow logs improve attribution.