TeamPCP v21: Inside a 45-Second Docker Escape to Multi-Persistence Cryptojacking
Docker exposes a management interface, the Docker daemon API, that lets you create, start, and control containers. When it is accidentally published to the internet with no authentication, anyone who can reach that port can take over the host underneath it. We watched a fully automated toolkit do exactly that in about 45 seconds.
Docker exposes a management interface, the Docker daemon API, that lets you create, start, and control containers. On a developer’s laptop it listens only locally. But it is routinely, and accidentally, published straight to the internet with no authentication: a CI/CD runner opened to a build network, a “quick” remote-management setup, a cloud image shipped with the wrong default. Anyone who can reach that port can ask the daemon to run a container, and a container, asked the right way, is a foothold on the host underneath it.
We run internet-facing devices that present an exposed Docker daemon, and in June 2026 we watched a fully automated toolkit find one, walk through that foothold, and turn it into complete host takeover in about 45 seconds. The toolkit names itself in its own source: “TeamPCP v21.”
It is nominally a cryptominer. That is the least interesting thing about it. In the same automated run it harvests credentials from 18 categories, plants four independent persistence mechanisms, reaches for cluster-wide Kubernetes propagation, and, caught live, in the same session, rewrites the command-and-control (C2) addresses of miners it had already planted elsewhere, exposing its own previous infrastructure in the process. (Command-and-control, or C2, is the server an attacker’s malware calls home to for instructions and to report stolen data.)
We captured the whole chain verbatim, the recon, the escape, the persistence, and the credential sweep, and recovered the dropper, the miner configuration, and the miner binary into a one-way sandbox for analysis.
This is the step-by-step walkthrough.
Every command, payload string, URL, and credential quoted below is captured attacker input recorded on our own device, reproduced here as evidence so defenders can recognize it. This is not a HowTo for Evil.
Step 1: Recon, fingerprinting the exposed daemon
The toolkit opens by asking the daemon what it is and what it is running:
GET /version
GET /version
GET /v1.54/images/json
What the attacker is doing here:
Two version probes, back to back. The duplicate is a tell about the tooling, not a mistake: the run uses two different client libraries,
python-requests/2.34.2anddocker-sdk-python/7.1.0, and each one issues its own version handshake. The toolkit is a Python program driving the Docker API through both a raw HTTP client and the official Docker SDK.Enumerating images.
GET /v1.54/images/jsonlists the container images already present on the host, so the toolkit can prefer an image that is already pulled and avoid the noise and delay of downloading one.
No exploit yet, this is reconnaissance against an API that answered without asking who was calling. That missing authentication is the entire vulnerability, and it is a total one. The Docker daemon API is not a read-only status page: it is the remote control for the whole container engine. Reaching it without a password means the toolkit can now tell the daemon to do anything it can do.
Two of those powers drive everything below:
- Run commands inside a container the host is already
running (
exec). The toolkit uses this first, in Step 2. - Create brand-new containers, including ones wired to the
host’s filesystem (
create). The toolkit uses this next, in Step 3, and that is the actual escape to the host.
So nothing in Step 2 or Step 3 is a separate “hack.” Both are just the toolkit spending the control that the open API in Step 1 already handed it.
Step 2: The operator moves house, caught live
The toolkit spends that control on housekeeping before it deploys
anything new. Using the first power listed above, exec, it
runs commands inside a container already present on the host
(no new container needed yet) and makes three find-and-replace edits to
a miner source file at /opt/.cgroup-fs/miner.c. Captured
verbatim (defanged):
sed -i 's/192.168.1.34/31.56.48[.]179/g' /opt/.cgroup-fs/miner.c
sed -i 's/45.76.202[.]47/31.56.48[.]179/g' /opt/.cgroup-fs/miner.c
sed -i 's/120.26.46[.]187/31.56.48[.]179/g' /opt/.cgroup-fs/miner.c
This step is baked into the toolkit’s standard playbook, it runs everywhere, patching any pre-existing miner it finds to point at the operator’s current C2. Because it runs everywhere, it ran on our device too, and in doing so it leaked the operator’s own infrastructure history.
What each line tells us:
192.168.1.34→ new C2. That first address is a private, non-routable one (the kind a machine has on a home or office LAN, never on the public internet). It is the operator’s own development/test box, a hardcoded leftover from where the toolkit was built and tried out.45.76.202[.]47→ new C2. A prior C2 the operator has now abandoned (a Vultr server in Japan). Every host still pointing at it is being migrated forward.120.26.46[.]187→ new C2. This is the scanning server that exploited us in Step 1. Rewriting it here confirms the operator’s scanner had been doing double duty as an early miner callback, and is now being separated from the dedicated C2.
Why this matters. Scan data shows you where an attacker is now. Catching an infrastructure migration mid-flight shows you where they were, a dev box, a retired C2, and a scanner-that-was-also-a-C2. Each is a retrospective hunting pivot a defender would otherwise never see.
Step 3: Four privileged containers, the escape
Now the toolkit uses the second power, create. It makes
four containers in a row, each one a complete container
escape. Every one carries the same two settings, this is the entry
signature for the whole campaign:
POST /v1.54/containers/create
{
...
"HostConfig": {
"Privileged": true,
"Binds": ["/:/host:rw"]
}
}
What these two settings do.
Privileged: true strips away the isolation that normally
keeps a container fenced off from the host.
Binds: ["/:/host:rw"] mounts the entire host
filesystem, read-write, inside the container at
/host. Together they mean the container can read and write
any file on the machine underneath it, the container wall is now a
formality. This combination has almost no legitimate use; seeing it on a
containers/create call is the single highest-value thing to
alert on.
Each of the four containers does one job:
Shell dropper. Fetches the main payload (
/w) from the C2 using a five-method download fallback, bash’s built-in/dev/tcp, busyboxwget,curl,wget, and Python 3’surllib, each tried twice, so the download succeeds on almost any Linux host regardless of what tools are installed. It then runs the fetched script. (The five-way fallback is the same “build for scale” pattern seen across commodity Linux cryptojacking.)SSH backdoor. Writes an attacker-controlled SSH public key into the host’s
rootaccount (/host/root/.ssh/authorized_keys), giving the operator direct remote login as root that survives reboots and password changes. The key carries the comment41832@CAI, a stable fingerprint that lets a defender hunt for the same operator across many victims. It then runs the dropper inside the host’s own process space (viachroot/nsenter), so the payload executes as if it had never been in a container at all.Systemd persistence. Installs a system service named
cgroup-fs.service, labeled “Control Group FS” to blend in with legitimate Linux plumbing, fetches the miner binary (/.real_mnd) and its configuration (/.mconf), and sets the service to run as root and restart automatically forever (Restart=always).Kubernetes lateral movement. Reads the host’s Kubernetes configuration (
/host/etc/kubernetes/) and its service-account tokens, the credentials a node uses to talk to the rest of a cluster, then attemptskubectl get nodesand akubectl applyof a DaemonSet. A DaemonSet is a Kubernetes object that runs a copy of a chosen container on every node in the cluster; applied here it would schedule the miner cluster-wide from this one foothold. (The manifest body itself was not captured.)
The point of four containers. This is not redundancy. Delivery, remote access, host persistence, and cluster spread are split into four independent operations so that if a defender catches and kills one, the other three still land. It is infrastructure-aware: the toolkit knows the difference between a lone Docker host and a Kubernetes node, and acts accordingly.
Step 4: Host takeover, what the dropper does once it lands
The shell dropper from container #1 is the workhorse. Once running on the host it executes a long, automated post-compromise routine. Here is the sequence, in order:
Privilege escalation, if needed. If the toolkit did not already land as root, it exploits CVE-2026-31431, a Linux kernel local privilege-escalation flaw in the
algif_aeadcryptographic interface, flagged for container-breakout use and added to CISA’s Known Exploited Vulnerabilities catalog in May 2026. On-thesis for a Docker-escape chain: it is the exact bug that turns “code in a container” into “root on the host” when the easy path is blocked. (Defenders should confirm the CVE against NVD and the vendor advisory before acting on it.)Blinding the cloud. It removes the monitoring and security agents of the major cloud providers, Alibaba, Tencent, AWS, Google Cloud, and Azure, so the host stops reporting its own compromise to the cloud console the owner watches.
Evicting the competition. It terminates rival miners already on the box and locks the Docker management ports shut behind it (
iptablesDROP on 2375–2377), denying the same door it just walked through to any other attacker. Stolen compute is a finite resource; the operator wants it all.Mining. It launches XMRig 6.22.2, a well-known open-source Monero miner, under a watchdog “guardian” process that relaunches it if it is killed.
Four-layer persistence. It installs itself four independent ways, so removing any one leaves the other three:
- a cron job
(
/etc/cron.d/net-check), - the systemd service from Step 3
(
cgroup-fs.service), - a login-profile hijack
(
/etc/profile.d/fs-cgroup.sh, which runs every time someone logs in), - and the SSH key from Step 3.
- a cron job
(
Worming outward. It reads SSH keys harvested from the host and uses
masscan(a high-speed internet port scanner) to find more SSH servers, then tries to spread to them using the stolen keys, turning each victim into the next attacker.The credential sweep, the real payload. It enumerates and reads credentials from 18 categories and POSTs them to a dedicated exfil port (
667), kept separate from the mining traffic on other ports. The categories include: SSH private keys; cloud-metadata tokens for AWS, Google Cloud, Azure, Alibaba, and Tencent; Kubernetes service-account tokens; Docker registry logins; cryptocurrency wallets; database credentials; CI/CD publishing tokens (npm, PyPI); and, new for 2026, AI coding-tool configurations, reading files such as~/.claude/settings.jsonand the contents of~/.cursor/.
That last category is the evolution to watch. As developers wire AI assistants into their workflows, the API keys and configs those tools store on disk become just another secret worth stealing, and this toolkit already knows to look for them. This is a credential harvester that happens to also mine.
Step 5: The mining relays, laundering stolen compute
The miner does not connect straight to a public mining pool. The C2
host (31.56.48[.]179) also runs the operator’s own
stratum proxies on ports 80, 443, 3333, and 8080 (stratum is
the protocol miners use to talk to a pool). Every infected machine
points at these proxies, which aggregate all the stolen hashpower and
forward it upstream to a public pool
(gulf.moneroocean[.]stream) under a single Monero
wallet.
Why bother with the middle layer? Two reasons. It means the public pool only ever sees the operator’s proxies, not the thousands of victim IPs, so a pool-side abuse report or ban hits the proxy, not the botnet. And it gives the operator a single switch: change where the proxies forward, and the entire fleet follows, without touching a single victim. It is the same resilience logic as the live C2 migration in Step 2, applied to the payout path.
The operator
The toolkit is unusually talkative about itself. Its own source
comments name the group (# TeamPCP v21) and even credit the
campaigns whose techniques it borrowed:
# Pattern: 0cl/boatnet(ELLIO)+TeamTNT+PCPJack+RedTail+Perfctl+Kiss-a-dog.
That places it squarely in the TeamTNT lineage of
container-cryptojacking, borrowing container escape from PCPJack,
persistence from RedTail, process hiding from Perfctl, and credential
harvesting from Kiss-a-dog. The “v21” revision and the structured,
modular code indicate an actively maintained toolkit, not a one-off
script.
The operator made several mistakes that hand defenders durable leverage:
- It named itself and its sources. The comments above identify the group, the build revision, and every technique family reused.
- The migration leaked prior infrastructure (Step 2): a dev/test box, a retired Vultr C2, and a scanner that doubled as an early C2.
- The miner binary leaks its build pipeline. It was
compiled from a CI/CD path
/home/buildbot/xmrig/dated 2024-11-03, a build-environment anchor that links any other binary from the same pipeline. - Stable cross-victim fingerprints. The SSH key
comment
41832@CAIand the Monero wallet are fixed across victims, usable to track the operator across many compromises (the wallet’s pool-side earnings are noted under Monero wallet below).
Command-and-control infrastructure
All characterization is from passive sources only, we did not probe the operator’s infrastructure.
| Indicator | Role | Note |
|---|---|---|
31.56.48[.]179 (ports 666, 667, 80/443/3333/8080) |
Primary C2, dropper delivery (666), credential exfil (667), self-hosted stratum proxies | Capture-exclusive |
95.182.96[.]193:8888 |
Fallback C2 (miner config) | Already public (ThreatFox) |
120.26.46[.]187 |
Scanner / exploiter (Alibaba Cloud, CN) | Already public (GreyNoise) |
45.76.202[.]47 |
Prior C2, rotated out (Vultr, JP) | Retrospective-hunting value |
192.168.1.34 |
Operator’s own dev/test box (private address) | Leaked in the migration |
gulf.moneroocean[.]stream |
Upstream public mining pool | Commodity |
The primary C2 sits on AS56971 (CGI GLOBAL LIMITED), an abuse-prone reseller; the scanning leg sits on Alibaba Cloud in China. These are different networks in different jurisdictions: the cloud-scanning leg is disposable compute, while the load-bearing anchor is the shared C2 autonomous system. The geolocation of that C2 network is contested across data sources, so we do not assert a single country for it, the verified anchor is the network (ASN), not a flag.
Payload URLs (defanged):
hxxp://31.56.48[.]179:666/w (dropper) ·
hxxp://31.56.48[.]179:666/.real_mnd (miner) ·
hxxp://31.56.48[.]179:666/.mconf (config)
Sample hashes
| Artifact | SHA-256 | Size | Note |
|---|---|---|---|
Dropper /w |
0f63bea320d768fb12bb53a287f210b8b9ccec563ac66dc80b7967628e455566 |
21,060 B | Capture-exclusive |
Miner config .mconf |
27cc6cf232ba7ed8dc92dcb0795bdb7185197928ec3061a8d6de097f9efc5440 |
1,035 B | Capture-exclusive |
XMRig .real_mnd |
b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d |
8,297,712 B | Reused commodity (in MalwareBazaar since 2024-12, tagged Mirai) |
The dropper and config hashes are exclusive to this capture and absent from public malware repositories. The XMRig binary is reused commodity, useful as a known-bad, but its presence alone does not identify this campaign.
Monero wallet
85BVSXMzxy74NTecwdiNbZZ83gxibFSszZhwCHBwcCCfW2xdrTWzMT14EgGihhQWC9fi3m5ZNfFAcaR4u5QYFP19ArnSAsK
As of publication (June 2026) this wallet was still actively mining on the MoneroOcean pool and had earned roughly 0.018 XMR to date, a small sum consistent with a limited number of infected hosts rather than a large botnet. Monero is a privacy coin, so the address itself yields no on-chain balance or transaction history; the figure above is the mining pool’s own published per-wallet statistic.
Timeline
| When | Event |
|---|---|
| Nov 2024 | XMRig payload compiled (/home/buildbot/xmrig/, build
date 2024-11-03) |
| May 2026 | CVE-2026-31431 added to CISA Known Exploited Vulnerabilities catalog |
| June 2026 | Full chain captured, including the live C2 migration; dropper, config, and miner recovered |
The full automated chain, recon to host takeover, runs in about 45 seconds.
What defenders should look for
Behavioral rules first (durable), then host artifacts, then atomic indicators (cheap for the operator to change), then what will not work.
Behavioral (most durable)
- The entry signature: any Docker
containers/createwith"Privileged": trueand a bind mount of the host root ("Binds": ["/:/host..."]). This combination is the single highest-value alert; it has almost no legitimate use. - In-container
sed -iedits to files under/opt/.cgroup-fs/(the live C2-rewrite behavior). - A systemd unit named
cgroup-fs*whose start command fetches from a raw IP address. - A login script under
/etc/profile.d/that launches a binary from/dev/shm/(a memory-backed temp directory attackers favor). - Cloud security/monitoring agents being stopped or removed on a host that should be running them.
- A burst of outbound SSH connection attempts
preceded by a
masscan-style port sweep (worm spreading). - Outbound HTTP POSTs to an unusual high port (here, 667) carrying credential-shaped data, distinct from any mining traffic.
Host artifacts
/etc/systemd/system/cgroup-fs.service ·
/etc/cron.d/net-check ·
/etc/profile.d/fs-cgroup.sh ·
/opt/.cgroup-fs/miner.c · /dev/shm/.x · an SSH
authorized_keys entry with the comment
41832@CAI.
Atomic indicators (see IOC CSV)
C2 IP and ports, payload URLs, sample hashes, the Monero wallet, the
client User-Agent strings (python-requests/2.34.2,
docker-sdk-python/7.1.0).
What will NOT detect this
- Cloud-console alerts alone, the toolkit removes the cloud monitoring agents early (Step 4.2).
- Hash-matching the miner, the XMRig binary is reused commodity shared with unrelated campaigns; matching it tells you little.
- IP reputation alone, the primary C2 is absent from commodity feeds at publication.
Detection content
Shipped alongside this brief:
- A YARA rule for the dropper.
- Sigma rules for the systemd-persistence unit and for the privileged- container-with-host-bind Docker API call.
- A machine-ingestible IOC CSV (defanged).
Recommendations
Close the door. Never expose the Docker daemon API to the internet. Bind it to localhost or a protected management network, require TLS client authentication, and confirm ports 2375–2377 are not reachable from outside. The same applies to the Kubernetes API.
Patch. CVE-2026-31431 is a CISA KEV entry used here for the host escalation step. Patch Linux kernels on internet-facing and container hosts.
Hunt. Run the behavioral checks above across your
container and Linux fleet, and search for the host artifacts, especially
the cgroup-fs* service, the 41832@CAI SSH key,
and sed edits under /opt/.cgroup-fs/. The
retired Vultr C2 (45.76.202[.]47) is worth a retrospective
sweep of historical logs.
Block. Block the primary C2
(31.56.48[.]179) at egress and deploy the Sigma and YARA
rules.
Closing
The mining is the noise; the credential harvest is the signal. In one automated 45-second run, a misconfigured Docker port became root on the host, four independent footholds, a reach into the Kubernetes cluster above it, and 18 categories of stolen secrets, now including the configs of developers’ AI coding tools. And because we caught the operator rewriting their own infrastructure mid-migration, the campaign handed us a map of where it had been: a dev box, a retired C2, and a scanner that used to double as a callback.
The lesson for defenders is the oldest one in container security, sharpened: an exposed daemon API is not a configuration nit, it is host-level remote code execution waiting for the first scanner to find it, and in 2026, the scanner that finds it is automated, infrastructure-aware, and after far more than your CPU cycles.
State of the Attack is a threat-research program publishing curated technical findings for defenders. Indicators are defanged; locations are given by region only.
Indicators of Compromise
31[.]56[.]48[.]17995[.]182[.]96[.]193120[.]26[.]46[.]18745[.]76[.]202[.]47gulf[.]moneroocean[.]streamhxxp://31[.]56[.]48[.]179:666/whxxp://31[.]56[.]48[.]179:666/.real_mndhxxp://31[.]56[.]48[.]179:666/.mconf66666788880f63bea320d768fb12bb53a287f210b8b9ccec563ac66dc80b7967628e45556627cc6cf232ba7ed8dc92dcb0795bdb7185197928ec3061a8d6de097f9efc5440b0e1ae6d73d656b203514f498b59cbcf29f067edf6fbd3803a3de7d21960848d/etc/systemd/system/cgroup-fs.service/etc/cron.d/net-check/etc/profile.d/fs-cgroup.sh/opt/.cgroup-fs/miner.c/dev/shm/.x41832@CAI85BVSXMzxy74NTecwdiNbZZ83gxibFSszZhwCHBwcCCfW2xdrTWzMT14EgGihhQWC9fi3m5ZNfFAcaR4u5QYFP19ArnSAsKpython-requests/2.34.2docker-sdk-python/7.1.0CVE-2026-31431/v1.54/images/jsonDocker containers/create with Privileged true and Binds containing /:/host:rw
Detection Rules & IOCs
Four rules across three detection tiers. A SOC analyst imports all four: together they cover the campaign at initial access (Docker API abuse), host persistence (systemd + cron + profile.d + SSH key), and the dropper payload itself.
docker-privileged-host-mount-create.yml
cgroup-fs-persistence-and-backdoor.yml
scattershell-teampcp-v21-yara.yar
scattershell-teampcp-v21-iocs.csv
In plain terms
- Sigma (Docker) — "is something creating a privileged container with host root access?" (daemon level, inbound)
- Sigma (host) — "has TeamPCP persistence landed on this Linux host?" (file-event level)
- YARA — "is this file the TeamPCP v21 dropper?" (file level)
- IOC CSV — machine-ingestible indicator list for SIEM/TIP import
Frequently asked
What is TeamPCP v21?
Docker exposes a management interface, the Docker daemon API, that lets you create, start, and control containers. When it is accidentally published to the internet with no authentication, anyone who can reach that port can take over the host underneath it. We watched a fully automated toolkit do exactly that in about 45 seconds.
What vulnerability does TeamPCP v21 exploit?
TeamPCP v21 is delivered by exploiting cve-2026-31431. The full exploitation chain is documented in the analysis above.
What are the indicators of compromise (IOCs) for TeamPCP v21?
Key indicators include 31[.]56[.]48[.]179, 95[.]182[.]96[.]193, 120[.]26[.]46[.]187, 45[.]76[.]202[.]47, gulf[.]moneroocean[.]stream, hxxp://31[.]56[.]48[.]179:666/w, and more. The full list and a downloadable IOC CSV are in the Detection Rules & IOCs section.
How do I detect TeamPCP v21?
TeamPCP v21 can be detected with Sigma: Docker Privileged Container with Host Root Bind Mount, Sigma: TeamPCP Host Persistence and SSH Backdoor, YARA: TeamPCP v21 Dropper, IOC CSV — all downloadable on this page. Four rules across three detection tiers. A SOC analyst imports all four: together they cover the campaign at initial access (Docker API abuse), host persistence (systemd + cron + profile.d + SSH key), and the dropper payload itself.
What MITRE ATT&CK techniques does TeamPCP v21 use?
TeamPCP v21 maps to T1190, T1610, T1609, T1611, T1068, T1543.002, T1053.003, T1546.004, T1098.004, T1552.005, T1552.001, T1528, T1562.001, T1027, T1046, T1613, T1021.004, T1496.
External references
These indicators are published to the threat-intelligence community. Verify or pull them from: