Vistacraft
← All security research

INCIDENT REPORT · AUG–SEP 2026

28 days of stolen CPU

During a routine spec check on a production server, one process stood out. The biggest memory user on the box wasn't any of the web applications it hosted. It was a cryptocurrency miner, and it had been running as root for 28 days. This report covers how an automated bot got in, why our monitoring missed it, and everything we changed afterwards. The affected services and clients are deliberately not named.

The Threat: React2Shell (CVE-2025-55182) is an unauthenticated remote code execution flaw in React Server Components, which affects Next.js App Router applications. It was disclosed on 4 December 2025, and within a day Microsoft saw it being used in the wild to drop XMRig cryptominers. A fix had been public for nine months before this server was hit. The vulnerable application had simply never been upgraded.

Entry point
Unpatched Next.js app (React2Shell)
Privilege gained
root, inherited from the process manager
Payload
Stock XMRig 6.26.0 Monero miner
Dwell time
28 days
Resource cost
~50% of CPU, 2.3 GB RAM, continuously
Data impact
No evidence of access; card data never touches the server
01

Discovery & Containment

Nothing alerted us. The miner was found by a human during a routine hardware and spec review, when xmrig showed up as the largest memory consumer on the server. The load average had been sitting at a steady 6.0 on a 12-thread machine. That was high enough to slow every hosted app, but not high enough to look like an outage.

The entry point was closed and the miner removed within 15 minutes of discovery:

  1. 23:12Miner spotted during the spec check.
  2. 23:21Suspected vulnerable application stopped in the process manager, cutting off the entry point.
  3. 23:22Miner process killed. The application was upgraded to a patched Next.js release, audited and rebuilt. Load average fell from 6.0 to 1.6 within minutes.
  4. 23:29Two other apps on the server upgraded the same night against a second, separate Next.js RCE (the August 2026 image-optimisation advisory).
  5. 23:43A second server running the same stack was checked the same way and came up clean.

Forensic Finding: Stop the entry point before you kill the payload. If you kill the miner while the vulnerable app is still serving requests, the next scan simply puts it back.

02

How the Bot Got Root

No password, stolen SSH key or privilege-escalation exploit was needed. The vulnerable application was already running as root. Its process manager had been started by root, so every app it launched inherited root.

  1. Scan: a bot sweeping the internet for vulnerable Next.js sites reached the app through its CDN.
  2. Exploit: a single crafted POST / abused React2Shell, making the Next.js server run arbitrary commands with the app's own permissions.
  3. Inherit root: because the app ran as root, so did the attacker's commands.
  4. Drop the miner: the official XMRig release was downloaded into a hidden folder under /root, unpacked, and started detached from the app (parent PID 1). All of this took about 0.1 seconds.
  5. Mine: XMRig connected out to a public mining pool on port 443, so it blended in with ordinary HTTPS traffic.

The per-site access logs told the story. There were two probes overnight that errored out, then a run of successful exploit requests that afternoon and evening. Every one was a bare POST / answered with a 303 redirect and a tiny response body:

# probing
[CDN_EDGE_IP] - - [28/Aug/2026:02:53:.. +0100] "POST / HTTP/2.0" 500 ...
[CDN_EDGE_IP] - - [28/Aug/2026:03:41:.. +0100] "POST / HTTP/2.0" 500 ...
# first successful exploit, then a burst at 16:18–16:20
[CDN_EDGE_IP] - - [28/Aug/2026:15:07:.. +0100] "POST / HTTP/2.0" 303 ...
# the request that lines up, to the second, with the miner's process start time
[CDN_EDGE_IP] - - [28/Aug/2026:19:25:21 +0100] "POST / HTTP/2.0" 303 2272

Forensic Finding: The bug let the attacker in, but running as root is what handed over the whole server. Had the app run as its own unprivileged user, the same exploit would have been confined to one site's files.

03

The Payload & the Backdoor

This wasn't a bespoke tool. The miner's archive hash matched XMRig's published 6.26.0 checksum exactly, so it was the stock GitHub release with no custom backdoor compiled in. Its config.json still held the default YOUR_WALLET_ADDRESS placeholder. The bot passed its real pool and wallet on the command line instead, which overrides the config file. All of this points to an untargeted, internet-wide campaign rather than someone after this server in particular.

The bot didn't stop at mining, though. On 5 September, a week after the initial compromise, a script wrote an attacker SSH public key into both authorized_keys and the legacy authorized_keys2, 2 ms apart and with no login session attached. It was a backup way back in.

Our first sweep on the night of discovery missed it. A deeper pass the next day found it. SSH auth logs covering the whole compromise window, cross-checked against the systemd journal, show zero logins with that key. It was removed and preserved as evidence.

Indicators of Compromise

Exploit patternPOST / → 303 with 2.2–2.6 KB responses on a Next.js App Router site; probes return 500
Drop folder/root/.mig_cache/ containing xmrig-6.26.0-noble-x64.tar.gz, mig.pid, extracted/xmrig-6.26.0/
Process…/extracted/xmrig-6.26.0/xmrig -o rx.unmineable.com:443 …, parent PID 1, user root
Mining poolrx.unmineable.com:443 (RandomX via unMineable)
Archive SHA-25618198537f741405f569db0e6ecdc11c01f514aa861843b49bfa1ef60fe2877e7 (official release)
Binary SHA-256138fd274c1cad558ece5c22feaac3e906533ee79d72e4a988ee1f5bc43606007
PersistenceSSH key with comment your_email@example.com added to both authorized_keys and authorized_keys2

Forensic Finding: Always do a second, slower sweep. Our initial report said "no persistence found", and it was wrong. Check authorized_keys2 too. Many admins have forgotten it exists, and attackers haven't.

04

Why Monitoring Missed It

We run a Wazuh SIEM across the estate, and it was working on the day of the attack. It logged 8,923 alerts from this server that day, and none of them was this attack. The post-mortem found five blind spots:

  • No application traffic. Wazuh read only the global web server logs, not the per-site logs where the exploit requests actually landed.
  • Exploit responses filtered out. A stock rule drops successful 2xx/3xx requests at level 0, so the 303 exploit responses were invisible even where logs were read.
  • No process or CPU checks. Nothing watched for sustained CPU, a web process spawning a shell, or connections to mining pools.
  • Drop sites unwatched. /tmp and /dev/shm weren't under file integrity monitoring, and scans ran only every 12 hours.
  • Alert fatigue. Around 85 high-severity alerts a day, mostly SSH and mail brute-forcing, buried real signal. SMS paging only fired at the very top level.

On top of that, the server's Wazuh agent had quietly gone offline five days after the attack. This was unrelated to the breach. The agent's config still held the installer's MANAGER_IP placeholder, and an automatic package update restarted it into a broken state. It had been dark for 23 days by the time we found the miner.

Forensic Finding: A SIEM that's running is not the same as a SIEM that can see. Test your detection against a real attack, and alert on agents that stop reporting.

05

Assessing the Data Impact

Root access means the attacker could have read anything on the server. So we looked for evidence of what it actually did:

  • Exploit responses were tiny. All eight successful requests returned 2.2–2.6 KB, far too small to carry a database.
  • Nothing was staged. No files were created anywhere on the server during the attack window apart from two routine emails, so there were no dump files.
  • No other foothold. Crontabs, systemd services and timers, /etc/ld.so.preload, startup files, user accounts (root the only UID 0), sshd_config and bind mounts hiding processes were all clean. Every root login was traced to a known connection.
  • No card data exposure. Payments are taken on the payment provider's hosted page, so card numbers never reach the server.

The evidence points to a server used for its CPU, not its data. But "no evidence" isn't "impossible". Following Microsoft's reporting that some React2Shell campaigns also harvested credentials, every secret on the server was treated as exposed and rotated. That covered database passwords, application secrets, API keys and payment-provider keys, with a live penny transaction to prove payments still worked. The affected client was informed the same night and given the findings to make their own data-protection reporting decision as controller.

06

Remediation & Hardening

Patching the hole took minutes. Closing the conditions that let one bug become a 28-day root compromise took the following day, across every server in the estate.

Detection

  • Wazuh now reads every per-site access log on both hosting servers, pushed centrally so new sites are covered automatically.
  • New custom rules were tested against the real attack log lines: the React2Shell request pattern pages by SMS, and there are rules for Node/Next.js spawning a non-Node process, sustained CPU above 80%, and connections to common mining-pool ports.
  • Real-time file integrity monitoring on /tmp, /var/tmp and /dev/shm, plus malicious-IOC lists wired into the ruleset.
  • Agent configs fixed and Wazuh packages pinned, so an automatic update can't knock monitoring offline again.

Least privilege

  • The breached app and another public-facing app moved off root to a dedicated unprivileged user, bound to localhost behind the reverse proxy.
  • Every .env file and database dump locked down to owner-only (600); a world-writable web folder fixed.
  • Stale database dumps, dead process-manager folders and old development backends archived off the web root; an unused dev site suspended.

Attack surface & access

  • Database and file-sharing ports that had been reachable from the internet are now closed at the firewall.
  • Mail brute-force jail tightened to 3 failures in 24 hours with a 24-hour ban.
  • Root SSH on every server now requires a hardware security key (FIDO2, PIN plus a physical touch). Password login is off and outdated trusted keys have been removed.

The Mitigation Strategy: Patch the bug, then assume the next one is already out there. Least privilege limits what an exploit can reach, detection shortens how long it goes unnoticed, and hardware-backed access means a stolen password or planted key is no longer enough to get back in.

07

Lessons Learned

The honest root cause isn't an exotic exploit. We learned about a critical vulnerability nine months late, and the hard way. The fix was public, and the bots found the server before we upgraded it.

  1. Patch lag is the vulnerability. Mass-exploitation bots move within days of disclosure. Critical framework advisories need acting on in days, not "next time we touch that app".
  2. Deployed code drifts from the repo. Repository alerts alone aren't enough. We're adding dependency alerts on every repo and a weekly audit of the code actually running on each server, feeding straight into Wazuh.
  3. Never run web apps as root. It's the difference between a defaced site and a lost server.
  4. Verify your monitoring against reality. Replay real attack traffic through your rules and alert when an agent goes quiet.
  5. Assume exposure, then prove otherwise. Rotate secrets first and investigate second, and always run the second sweep.

Need a second pair of eyes?

If you think your systems may have been compromised, or you would like an independent view of how exposed they are, get in touch. I reply personally, usually within a working day.