
Someone broke into our server in November. Here is how I threw them out and rebuilt it in under 5 hours.
React2Shell is still being exploited in November 2026. Its patch is almost a year old, and the attacker who hit us has a Monero wallet that still showed 24 mining workers on 15 November. Below you will find fresh indicators of compromise from our incident, which I could not find published anywhere else: see "Indicators of compromise" further down.
Between 8 and 14 November 2026, that attacker got into our production server through a public Next.js bug called React2Shell (CVE-2025-55182). They stole what they could read, and on 14 November they dropped a cryptominer.
On the same day the server was stopped, I rebuilt it from zero. By the end of 14 November (UTC) every one of our apps was back online on a fresh server, every site on HTTPS, every stolen key being replaced, and the box now watching itself.
This post is the timeline, what the attack looks like, and exactly what I changed. If you self-host Next.js, the part about the logs is the one to read.
The attack
React2Shell is a remote code execution bug in React Server Components, the code Next.js uses to run server-side React. It was fixed upstream in Next.js 15.2.6. The attacker used it against one of our Next.js sites.
Timeline, from the web server logs and file timestamps on the old disk:
- Thursday 12 November, 11:35:43 UTC (5:35 pm our time): one source sends the exploit to our site, then probes for .git and .env files. One second later a credential stealer script is written to the temp folder and run. It reads environment files, SSH keys and tokens and tries to upload them to a command server. Its own error file shows those uploads failed.
- Saturday 14 November, 11:12 UTC (5:12 pm our time): a second source sends ten POST requests to a made-up path, /adfa. An XMRig miner is dropped and starts mining Monero.
- 14 November, 11:35 UTC: the server is stopped by our hosting provider, 23 minutes after the miner landed.
The first break-in came after office hours, and its uploads failed, so nothing looked wrong. Friday is our weekend; nobody looked at the server. I only found the 12 November break-in later, when I traced the second one back.
What the logs hid
This is the part worth knowing if you run Next.js yourself.
The output of the attacker's commands came back inside the error "digest" field of the response. Every exploit request looked like a normal HTTP 200 in the access log. Counting failed logins or 4xx errors shows nothing. The log looks clean while the attack is happening.
So a log watcher alone will not catch this. You need something that watches what the server does, not only what it answers.
How I traced it
I mounted the old disk read-only and walked every step: the scan, the exploit, the stealer, the miner. Then I wrote a scanner for every trace this attack leaves on disk and ran it against the old disk. Every known trace was found, zero missed. The same scanner came back clean on the new server before any app went on it.
I treated every secret on the old server as stolen. Not "probably fine". Stolen.
What I rebuilt, 14 November
From a blank server to every app live took 4 hours 20 minutes: the fresh install booted at 16:45 UTC, the first app was back 47 minutes later, the last one at 21:04. Our sites were down about 9 and a half hours in total, because I traced the attack and saved the evidence before rebuilding.
I did not clean the old server. I installed a fresh one and brought the apps over one by one, under new rules.
One locked user per app. The old setup ran the apps under one Linux user. The new one gives every app its own user: no shell, no login, no sudo. Each app runs as a hardened systemd service: no new privileges, read-only system, no access to home folders, private temp, no capabilities, a memory cap. The app cannot write its own code, so an attacker cannot plant something that survives a restart. Each app also has its own database login that owns only its own database.
Secrets out of the code. Environment files no longer sit in the app's code folder. They live in a root-managed place, and each app can read only its own file. A daily encrypted backup of them goes off the box.
Every stolen key replaced. Database passwords, session and signing secrets, payment webhook secrets, Google sign-in secrets, mail app passwords, Apple sign-in keys, cloud and backup keys, deploy keys. Old keys revoked or disabled at the provider, not just swapped on our side. Every user of two of our apps had to log in again because I replaced their session secret. That is the point.
App ports closed to the internet. Apps listen on the box only. From outside, only the web server answers. It runs fresh configs (the old ones were treated as untrusted), with rate limits, security headers, and a guard that drops this exploit's request shape before it reaches any app.
Every Next.js app patched, twice over. React2Shell is fixed on every app. And on 15 November two more critical, unauthenticated remote code execution bugs landed in Next.js's image optimizer (/_next/image): GHSA-2xp9-vwfh-vxw4 and GHSA-2464-fhr4-w9xw. I swept all our Next apps the same day. Apps on current lines moved to the fixed releases (15.5.29, 16.4.6). Older apps where no fix exists got the image optimizer switched off, one config line that closes the hole. Every build passed before it shipped.
No deploys from the internet onto the box. Self-hosted CI runners are gone from production. Deploys come in through one locked-down deploy path, using per-repo read-only keys that cannot read each other's repos.
Fewer servers. The site that was hit needs no server code, so it is now plain static files served by nginx. No Node process, nothing to exploit.
The box watches back now
I was not watching the server every minute, and no one can. Security should not depend on whether I am awake, at work or looking at a terminal. So the server now watches itself.
Falco and auditd run on the server, watching what processes do and what files and accounts change. Both were tested by firing harmless fake events before I trusted them. They start in observe mode: every alarm is logged and checked before any rule is allowed to act on its own, because a false alarm on a production box is an outage.
zetta-sentinel, our own watchdog, is being finished now. It is written in Rust and sits on top of Falco and auditd. It looks for things that never happen on a healthy server: an app starting a shell, running something from a temp folder, talking to a host it has no reason to talk to, burning CPU with no traffic, touching files it has no business near. Alerts go out by email. It falls back to its own checks if Falco or auditd ever stops, and alerts when that happens. It is installed on the box; it goes live in observe mode first, one rule at a time.
zetta-patchbot is next: on a separate machine, never on the server it protects, it will bump Next.js and React when a fix ships, build, test and open a pull request. A radar on public advisory feeds tells me about a new bug the day it appears.
Making a stolen secret worth nothing
There is a hard truth here: no secret an app reads is unstealable, because the app has to read it to use it. So the goal is a break-in that steals nothing reusable. Four properties:
- Isolated: one app cannot read another's. Done.
- Not stored as plain text on disk. Next.
- Bound to the server: a key that only works from our box is dead anywhere else. Next, provider by provider.
- Short-lived: a key that expires before anyone can use it. Later.
Who I reported it to
Legal routes only. I am not going after anyone. Each report carried exact times, request lines and file hashes, and asked the network to take the host down and preserve its logs.
- Google Cloud (abuse form): 34.40.134.70, the address that sent the 12 November exploit.
- M247 (abuse email): 217.138.216.212, the address that dropped the miner on 14 November.
- Cloudzy (abuse email): 153.75.95.223, the upload server the stealer tried to send our keys to.
- AbuseIPDB: both attacking addresses, 34.40.134.70 and 217.138.216.212, reported as Hacking and Web App Attack.
- VirusTotal: I checked both file hashes. The XMRig miner was already flagged by 42 vendors. The stealer was unknown there.
- MoneroOcean (the mining pool, through its Discord): the wallet, with a request to ban it and hold its unpaid balance. Sent 15 November; the pool said it will take care of it.
- MalwareBazaar: skipped. It takes sample files, and none were left to send.
What you can do too: if an address attacks you, report it to its host's abuse contact. If you find a miner wallet, report it to its pool. Both cost the attacker something.
What I am sharing
I built a small guard for people who self-host Next.js: it checks your installed versions against advisories and ships web server rules and log patterns for this exploit shape. It is not public yet. The sentinel and the patch bot will be open-sourced once they have run on our real server and proved themselves.
Indicators of compromise (fresh, not published anywhere else)
As of 15 November 2026 I found no public record of these values. If you run Next.js, search your logs and disks for them. Times are UTC.
Where the attacks came from
- 34.40.134.70 sent the 12 November exploit and dropped the credential stealer.
- 217.138.216.212 sent the 14 November exploit and dropped the miner. It is a known scanner on public threat feeds, run from M247 (Berlin).
- Request pattern: POST /adfa, ten requests in about five seconds. Earlier probing with the same path started on 8 November.
- User agent: Mozilla/5.0 (compatible; EnvScanner/2.0)
The credential stealer (12 November)
- Dropped as a hidden script in the temp folder, named /tmp/.<hex>.sh (ours was a nine-character hex name, about 94 KB). It collects environment files, SSH keys, passwords and cloud credentials, and writes them to a hidden loot folder next to it.
- Upload address hard-coded in the script: http://153.75.95.223:8080. That address is in the network of Cloudzy (RIPE object AE-CLOUDZY-19910923, routed by AS14956). Some threat feeds still label it with an older owner; the registry says Cloudzy.
- Stealer SHA-256: 2ccff5c692bbc10b07bb2ef7923cdee599d8841d2bb4a6c3f1c62f0ef35e38ea
The miner (14 November)
- XMRig 6.22.0, copied to a hidden folder at ~/.cache/.sys/xmrig one second after the archive xmrig.tar.gz landed in the temp folder, and started at once.
- Pool: gulf.moneroocean.stream:10128
- Wallet: 8B6PDz5F9K6cAi7AzpV4HNiu588sinxeDUM6AAJhpZdw3qi2N2A8gwa3mD5kphJDkAALarNRuJNi9Fj2zzYGuyR2JEPx1b3
- Miner binary SHA-256: fd11982f252c060a1372e81d5be57589647052b56281a5c54975ca22164f7726
- Archive SHA-256: cb49ae0793b257fd95892cb5eb777b9fe0d61a4015c7926cb68a3d353ed6accd
Why this is a warning
On 15 November the pool's public numbers showed that wallet mining at about 128 kH/s from 24 named workers, with its last share on 14 November. The hash total fits a campaign that began around 8 November. Who runs those workers is unverified, and one machine can run several, but some may be unpatched Next.js servers whose owners have not noticed. If you find any of the values above, assume a full compromise, not just a miner. I reported the source addresses to their networks and the wallet to its pool.
Full list, machine-readable: GitHub Gist.
If you self-host Next.js
- Upgrade now. React2Shell is fixed in 15.2.6 and later. The two image optimizer bugs are fixed in 15.5.28 / 16.4.1 and later; on lines with no fix, set images.unoptimized: true.
- Do not trust a clean access log. This attack returns HTTP 200.
- Give every app its own locked user. No self-hosted CI runners on the production box.
- Keep app ports off the internet. Only the web server should answer.
- Block outbound traffic from app users by default. A miner, a reverse shell and a data upload all need to get out.
- Keep secrets out of the code folder, and bind each key to your server where the provider allows it.
- If a site needs no server code, do not run one.
We got hit, I traced every step, I rebuilt in under 5 hours, and the server watches back now. Questions welcome.
More from Insights
2025-05-15

Why Learning to Code is Essential in the Age of AI (And How ZettaByte Empowers You)
As AI reshapes tech, coding remains a critical skill. Discover why human ingenuity thrives alongside AI and how ZettaByte prepares learners for the future.💡💻
2025-03-23

From Idea to Implementation: Understanding the Software Development Life Cycle (SDLC)
Discover the Software Development Life Cycle (SDLC) and how it transforms ideas into functional software. Learn about planning, design, development, testing, and deployment to ensure project success. 🚀
2025-03-20

How Customer Data & Machine Learning Can Enhance Customer Experience
Discover how combining customer data with machine learning can revolutionize customer experience. Learn actionable strategies to deliver personalized services, predict customer needs, and boost satisfaction.