Notes from the Field: Inside the Mirai-Based Second-Stage Payload | SecurityHQ
Inside the Mirai-Based Second-Stage Payload Seen in PHP2Shell Exploitation
Details of PHP2Shell (CVE-2026-109618 and CVE-2026-109619) leaked on 20 October 2026, PHP released fixes on 21 October, and exploitation attempts against PHP web servers were reported within hours. In some of those requests, the injected command pulled down a second-stage payload called nfs, saved it to /tmp and ran it. This post is our technical analysis of that payload: the x86-64 build of Potassium, a Mirai-based botnet that turns the compromised web server into a remotely controlled DDoS machine.
Potassium was first reported in March 2026, and its start-up, single-copy lock, attack parser and rival killer still follow the leaked Mirai source almost line for line. What sets this build apart is everything added on top: a ChaCha20-encrypted configuration, a C2 route through the Ethereum blockchain, fresh session keys for every connection, host profiling and 24 attack methods.
Since the payload is delivered only after an attacker has gained command execution, finding it on a web server means the server was compromised, most likely through PHP2Shell. Function names beginning mal_ are our own labels from the analysis, and where a detail comes from third-party reporting rather than our own disassembly, we say so in the text.
The Payload at a Glance
| Second-stage payload | nfs, the x86-64 build of Potassium, a Mirai-based DDoS botnet, run from /tmp as ./nfs php |
|---|---|
| File type | Linux ELF 64-bit, statically linked against musl libc, stripped, 179,632 bytes |
| Severity | High Command execution on the web server, whose bandwidth is then used to attack others; everything the web account can read is at risk |
| Confidence | High Behaviour confirmed in the disassembly; the ChaCha20 key, the botlesscucks C2 hostnames and this exact hash all match published Potassium and PHP2Shell indicators |
| First stage | PHP2Shell exploit (CVE-2026-109618 / CVE-2026-109619), whose injected command downloads hxxp[://]92[.]118[.]112[.]100/nfs |
| Command and control | thunder[.]godofthunder[.]st on 14 TCP ports between 50217 and 52645; an Ethereum ENS route is built in but reported as switched off in this build |
| SHA-256 | 0c5486f06620e26e316f4b33f318b0d646d5e1133789331aa2fed012360d1486 |
| Analysis date | 23 October 2026 |
How the Attack Unfolds, Stage by Stage
PHP2Shell is only the first stage. It gives the attacker command execution on the server, and the command they run fetches the nfs payload, which is the second stage and the focus of this analysis. The payload wakes up quietly, makes sure it is the only copy, finds its controller and then waits for the third stage: the operator’s orders.

Stage 1: PHP2Shell Exploitation Delivers the Second Stage
PHP2Shell is the name given to CVE-2026-109618 and CVE-2026-109619, two memory-safety bugs in the PHP engine, discovered by the security firm Calif. The first is triggered when PHP runs out of memory midway through setting up a request; the second is a use-after-free, where state left behind by one request is reused by the next request on the same worker. Both carry a CVSS v4.0 score of 9.2.
In practice, one crafted request with no login is enough to run commands on the server, and it fires before the website’s own code is even reached. PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed are all exposed. The fixed releases are 8.5.12, 8.4.27, 8.3.36 and 8.2.35; the end-of-life 7.4, 8.0 and 8.1 branches have only source backports or vendor patches.
Amazon has reported one attempt on its honeypots as early as 17 October, four days before disclosure, and once the advisory was out, reported attempts climbed into the hundreds of thousands within two hours. In the requests linked to this sample, the injected first-stage command downloaded the second-stage payload from hxxp[://]92[.]118[.]112[.]100/nfs, wrote it to /tmp/nfs and executed it as ./nfs php, running as the web-server user.
Stage 2: The Payload Starts Up and Slips into the Background
The second-stage payload is a 179,632-byte x86-64 ELF, statically linked against musl libc and stripped. It is not packed, but with everything compiled in there is no import table to give its behaviour away. A sister ARM build targets routers, IP cameras and DVRs.
The first thing mal_main (0x4001F2) does is decrypt its configuration, covered in the next section. It then ignores SIGHUP, SIGTSTP, SIGTTIN and SIGTTOU, so closing or pausing the terminal does not stop it, ignores SIGCHLD so finished children are reaped automatically, and moves to /.
mal_init_libc(a1: 0, a2: 0); mal_decrypt_string_table(); mal_init_state(a1: v5, a2: v6, a3: v4); mal_random_seed_from_urandom(); mal_sigemptyset_wrap(a1: v103); sigaddset(); // signal(SIGCHLD=17, SIG_IGN=1) — reap children automatically (daemon pattern) signal(); // signal(SIGHUP=1, SIG_IGN) — ignore terminal hangup signal(); signal(); // signal(SIGTSTP=20, SIG_IGN) — ignore ^Z stop signal(); // signal(SIGTTIN=21, SIG_IGN) — ignore background-tty-read signal(); // signal(SIGTTOU=22, SIG_IGN) — ignore background-tty-write sigprocmask(); mal_chdir(a1: "/"); // chdir('/') — detach from launch cwd (anti-forensics / daemon) mal_external_ip = mal_get_external_ip_8888(a1: v9, a2: v10, a3: v7, a4: v8); mal_memzero(a1: (__int64)v100, a2: 32); if ( a1 <= 1 || (int)mal_safe_strlen(a1: a2[1]) > 31 ) { v12 = *(const void **)(mal_strings_table + 3824); if ( v12 != nullptr && (int)(v13 = *(unsigned int *)(mal_strings_table + 3832)) > 0 ) qmemcpy(v100, v12, v13); else v100[0] = 108; } else { mal_safe_strcpy(a1: (__int64)v100, a2: a2[1]); v11 = mal_safe_strlen(a1: a2[1]); mal_memzero(a1: a2[1], a2: v11); } sub_40C3E9(a1: v100); mal_parse_argv_env(a1: a2); mal_env_apply(); v14 = mal_geteuid(); v15 = v14 == 0; if ( v14 != 0 && (int)mal_socket(a1: 2, a2: 3, a3: 6) >= 0 ) { close(); LODWORD(v15) = 1; } sub_40CB53(a1: v15); mal_killswitch_listen(a1: v18, a2: v19, a3: v16, a4: v17); sub_40CA72(); sub_40DB6E(); v20 = sub_40DB98();
Next, it saves its first command-line argument, php here, as the bot ID. As in Mirai, this is a campaign tag that tells the operator which infection route brought the machine in. After copying it, the bot zeroes the argument so the tag no longer shows in the process list.
It then checks its rights. If it is not root, it tries to open a raw socket and, if that works, treats itself as privileged anyway. About half its attack methods build packets by hand, so this decides what the server can launch: running as www-data, the raw-packet floods fail but plain UDP and TCP floods still work. It also learns the server’s own IP with an old Mirai trick in mal_get_external_ip_8888 (0x40D3E8), “connecting” a UDP socket to 8.8.8.8:53 and asking the kernel which local address it picked. No packet is sent, so only system-call auditing sees this step.
Stage 2: 473 Encrypted Strings and a Port That Works Like a Lock
Every useful string in the bot, from C2 hostnames to blockchain details and file paths, is stored encrypted and decrypted only at start-up, which is why a plain strings dump shows almost nothing. This build carries 473 of them, against 222 in the May builds. Where Mirai used a simple XOR, this payload uses ChaCha20, but the 32-byte key and 12-byte nonce are hardcoded and identical across every known build, so anyone with a sample can decrypt the table without running it. To keep crypto scanners quiet, the authors XOR-ed the cipher’s “expand 32-byte k” constant with 0xA5.
// mal_decrypt_string_table — one-shot startup string-vault decryptor.
//
// Allocates ~7.6 KB at mal_strings_table (fallback: .bss unk_62BDE0) and calls
// mal_decrypt_one_string ~473 times with increasing slot indices to deobfuscate
// every string the implant needs at runtime.
//
// The underlying stream cipher is standard ChaCha20 (see mal_chacha20_xor_stream
// @0x40D6C6). The sigma 'expand 32-byte k' is stored at 0x426F20 XOR'd with
// 0xA5. Key, nonce and counter are hardcoded and identical for every call:
// key @ 0x62BA00 = 5a3f2e7b8c9d0a6e3b4f7c8a1d5c2b0d6f1a0c3e2d8e4a091c7e5a4d3c2b6a08
// nonce @ 0x62B9E8 = 4a1b00ff2d7c3a1200010305
// counter = 1 (literal)
//
// Because counter resets every call, slots 0..7 (which share the same source
// pointer &unk_426F30 with len 24) all decrypt to the same string
// 'thunder.godofthunder.st' — the primary C2 fallback hostname that the
// EtherHiding path later overwrites.
//
// DECRYPTED HIGHLIGHTS:
// Slot 0..7 : 'thunder.godofthunder.st' (hardcoded fallback C2 host)
// Slot 8..9 : '2sexy4u.eth', 'sushiconsumer.eth' (ENS domains for EtherHiding)
// Slot 10..16 : 7 public Ethereum JSON-RPC endpoints
// Slot 17..19 : HTTP paths '/', '/eth', '/v1/rpc/public'
// Slot 20 : 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e (ENS Registry)
// Slot 21 : 0xF29100983E058B709F3D539b0c765937B804AC15 (ENS Public Resolver)
// Slot 22 : 0x0178b8bf — selector resolver(bytes32)
// Slot 23 : 0x59d1d43c — selector text(bytes32,string)
// Slot 24..55 : 'ip','ip2',...,'ip32' (ENS text-record keys)
// Slot 275/276 : 'hackattackkaboom.botlesscucks.st', 'hik.gettingheadfromduckysmom.st'
fig 2:The hardcoded ChaCha20 key and nonce, with the first decrypted slots
mal_decrypt_string_table (0x40DBA0) allocates 476 slots and calls mal_decrypt_one_string (0x40D8FA) for each. Since the counter restarts for every string, identical text encrypts to identical bytes, so the first eight slots, all holding the main C2 hostname, share one blob. When memory allocation fails, it quietly switches to small static buffers, a habit that points to low-memory targets such as routers and DVRs. We recovered the whole table statically: 473 of the 476 slots are populated, and 465 strings are unique once repeats such as the eight identical C2 slots are removed.
| Category | What the configuration holds |
|---|---|
| C2 hostnames | thunder[.]godofthunder[.]st in all eight DNS seed slots; backups hik[.]gettingheadfromduckysmom[.]st and hackattackkaboom[.]botlesscucks[.]st, the latter also serving updates |
| Blockchain lookup | ENS names 2sexy4u[.]eth and sushiconsumer[.]eth, seven public Ethereum RPC servers, record keys ip to ip32 |
| Game-server traffic | FiveM client requests (GET /info.json, POST /client, getEndpoints), the CitizenFX/1 User-Agent and five browser User-Agents |
| Device targeting | CPU architecture names, DVR and camera firmware folders, Android system paths |
Only One Copy Is Allowed to Run
mal_killswitch_listen (0x412D78) opens TCP port 1234 on the server’s own IP or on 127.0.0.1 and holds it as a lock. If the bind fails, an older copy already holds the port, so the newcomer dials 127.0.0.1:1234 itself. The running copy reads any connection on that port as a signal to shut down, kills its worker and exits, and the newcomer takes over the port. So the newest copy always wins, and every re-infection quietly upgrades the bot. No reboot persistence was found; this lock is how the bot holds its place.
int v61; // [rsp+44h] [rbp-B4h]
_QWORD v62[21]; // [rsp+50h] [rbp-A8h] BYREF
if ( dword_62EFD0 > 4 )
LABEL_2:
sub_4001D0(a1: 1);
v56 = 2;
mal_killswitch_socket_fd = -1;
v4 = mal_external_ip;
if ( mal_bind_localhost_flag != 0 )
v4 = mal_htonl(a1: 2130706433);
v58 = v4;
v5 = 26;
v57 = mal_htons(a1: 1234, a2, a3, a4);
while ( 1 )
{
mal_killswitch_socket_fd = mal_socket(a1: 2, a2: 1, a3: 0);
v9 = (unsigned int)mal_killswitch_socket_fd;
if ( mal_killswitch_socket_fd == -1 )
goto LABEL_40;
setsockopt();
v13 = read(a1: mal_killswitch_socket_fd, a2: 3u, a3: 0, a4: v10, a5: v11, a6: v12, a7: v52);
BYTE1(v13) |= 8u;
read(a1: mal_killswitch_socket_fd, a2: 4u, a3: v13, a4: v14, a5: v15, a6: v16, a7: v53);
bind();
if ( v20 != 0 )
{
v22 = (_DWORD *)sub_417C3C(a1: (unsigned int)mal_killswitch_socket_fd, a2: v19, a3: v17, a4: v18);
if ( *v22 == 99 && mal_bind_localhost_flag != 0 )
mal_bind_localhost_flag = 0;
close();
mal_killswitch_socket_fd = -1;
v59 = 2;
v60 = mal_htons(a1: 1234, a2: v25, a3: v23, a4: v24);
fig 3:The port-1234 lock and the takeover connection to 127.0.0.1:1234.
This helps defenders twice over. Any process from /tmp or /dev/shm listening on TCP 1234 deserves immediate attention, and once the evidence is preserved, a plain TCP connection to that port, on 127.0.0.1 and on the server’s own IP, is enough to make the bot stop itself. This only works for this family and build, and evidence should be collected first, as set out in the mitigation steps below.
Stage 2: Two Routes to the C2, One of Them on the Blockchain
The bot has two ways of finding its controller. The first is DNS. mal_resolve_c2_dns_seeds (0x413189) walks eight hostname slots, all of which hold thunder[.]godofthunder[.]st in this build, and resolves them into a pool of up to 32 C2 addresses. It uses its own DNS code against 8.8.8.8, 1.1.1.1 and 1.0.0.1, so these lookups never show up in your internal DNS logs. Earlier builds were reported to swap the halves of each answer (A.B.C.D becomes C.D.A.B), so block on hostnames and observed C2 IPs, not DNS answers alone. In public sandbox runs, this sample talked to 134[.]199[.]154[.]64 on port 50792.
The second route runs through the Ethereum blockchain and is much harder to take down. The C2 address sits in the text records ip to ip32 of two Ethereum Name Service (ENS) names, 2sexy4u[.]eth and sushiconsumer[.]eth, which the bot reads through seven public Ethereum RPC servers. Think of it as a DNS TXT record on the blockchain, a technique in the same family as EtherHiding: there is no domain to seize, the lookups go to legitimate providers, and editing one record moves the whole botnet.
Our IDA notes in Figure 2 show that an address from this route is meant to overwrite the hardcoded hostname, so whenever the route is live it takes priority over DNS. In this build it is reported as switched off. When we queried the records on 23 October, only 2sexy4u[.]eth returned a value, 143[.]220[.]202[.]110, and every other record was empty. That address belongs to an unrelated, legitimate organisation, and whoever controls an ENS name can point it at any IP, so monitor the address but do not treat its owner as malicious.

A Handshake That Will Not Talk to a Debugger
To connect, mal_pick_random_port (0x413AAA) pairs a random IP from the pool with one of 14 fixed ports between 50217 and 52645. Earlier builds used a different port set, so port blocking alone will not hold for long.
// mal_pick_random_port — select a random destination port from the 14-WORD array @0x4287E0.
//
// Copies the 14 WORDs (28 bytes) onto the local stack, picks index
// (mal_prng_next() % 14), and returns htons(array[index]).
//
// Array (host-order 16-bit):
// 50217 50341 50488 50613 50792 50904 51127
// 51289 51456 51603 51871 52134 52398 52645
__int64 mal_pick_random_port()
{
_DWORD *v0; // rsi
__int64 v1; // rcx
_DWORD *v2; // rdi
unsigned int v3; // eax
__int64 v4; // rsi
_WORD v6[18]; // [rsp+4h] [rbp-24h] BYREF
v0 = &mal_c2_port_pool_14w;
v1 = 7;
v2 = v6;
while ( v1 != 0 )
{
*v2++ = *v0++;
--v1;
}
v3 = mal_prng_next();
return mal_htons(a1: (unsigned __int16)v6[v3 % 0xEuLL], a2: v4, a3: v3 % 0xEuLL, a4: 14);
}
The handshake in mal_c2_handshake (0x4132D0) then does three things. First, mal_anti_debug_tracerpid (0x412511) checks the TracerPid field in the bot’s own /proc status file; if gdb, strace or a ptrace-based sandbox is attached, the bot quietly drops the connection and retries later.
The placement is deliberate: under a debugger the bot stays alive and keeps retrying, yet never completes a conversation with the C2, so a sandbox logs a few connection attempts and nothing more. Second, it counts CPU cores from /proc/cpuinfo and total memory from /proc/meminfo. The operator gets these at registration to rank bots by firepower, and a data-centre web server is worth far more than a home router.
_BYTE v59[492]; // [rsp+174h] [rbp-464h] BYREF
char v60[260]; // [rsp+360h] [rbp-278h] BYREF
char v61; // [rsp+464h] [rbp-174h] BYREF
if ( (unsigned int)mal_anti_debug_tracerpid() == 0 )
goto LABEL_69;
mal_rng_jitter(a1: v2);
v3 = 0;
v50 = strlen(s: a2);
v4 = mal_fopen(filename: *(const char **)(mal_strings_table + 3552));
if ( v4 == 0 )
goto LABEL_8;
while ( mal_fgets(a1: v60, a2: 256, a3: v4) != 0 )
{
v5 = *(int *)(mal_strings_table + 3656);
if ( (int)v5 > 0 )
v3 += strncmp(_l: v60, _r: *(const char **)(mal_strings_table + 3648), n: v5) == 0;
}
fclose();
if ( (unsigned int)(v3 - 1) > 0xFE )
{
LABEL_8:
LOBYTE(v3) = 1;
v6 = sub_4175F0(a1: 84);
if ( (unsigned __int64)(v6 - 1) <= 0xFE )
LOBYTE(v3) = v6;
}
v7 = mal_fopen(filename: *(const char **)(mal_strings_table + 3568));
if ( v7 != 0 )
Third, mal_derive_keys_hkdf (0x41277F) uses HKDF to derive three keys from the handshake material: a receive key for traffic from the C2, a send key for traffic to it, and a MAC key that catches tampering. The labels wrx-2, wtx-2 and mac-2 sit in plain text and make a reliable fingerprint. Because every connection gets new keys, a captured session is of no use for decrypting or replaying another.
// mal_derive_keys_hkdf — derive directional AEAD keys from a shared session secret.
//
// Uses three HKDF-style info labels at (0x4287C0..0x4287CC):
// 'wrx-2\x01' — receive (RX) direction key
// 'wtx-2\x01' — transmit (TX) direction key
// 'mac-2\x01' — MAC / authentication key
//
// The three labels are the family's highest-fidelity YARA fingerprint and
// should match across sibling samples. Each is 5 ASCII bytes + 0x01 counter.
//
// Mixes the two 32-byte nonces (a4, a5) and the shared secret (a1 + a2/a3)
// through sub_41193F (keyed hash update) to produce 32-byte directional
// keys. The 15-byte fixed secret at 0x4287B0 may be used as a per-build
// fingerprint folded into the init.
__int64 __fastcall mal_derive_keys_hkdf(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5)
fig 6:Per-session key derivation with the wrx-2, wtx-2 and mac-2 labels.
Alongside a legacy format that XORs every byte with 0xAD, mal_rx_framed_packet (0x41215B) reads framed messages: a 21-byte header (sync byte, length, sequence number, 16-byte nonce) ahead of an encrypted, authenticated payload. Out-of-order frames are rejected, and every sixth pass of the main loop sends a keep-alive.
Stage 3: Operator Tasking Turns the Server into an Attack Machine
The third stage exists only at runtime: whatever the operator sends down the encrypted session. An attack order is a short binary message parsed by mal_parse_cmd_packet (0x400F7D): the duration in seconds, the method, the targets and extra settings such as packet size or destination port. Targets are IP and netmask pairs, so one entry can hit an entire subnet. The layout is Mirai’s with a small change in how options are written, and two codes, sent with a zero duration, are handled straight away: 0xFF stops every running attack and 0xFE starts a helper process.
// mal_parse_cmd_packet — parse an inbound C2 command packet after AEAD open.
//
// Packet layout:
// [0..3] uint32 seq_or_sig (big-endian, decoded via mal_ntohl)
// [4] byte opcode
// [5] byte peer_count N
// [6..] N * {uint32 ip; byte flag} (5 bytes each)
// [+..] byte payload_count M (if any)
// [+..] M * {byte key_tag; cstring (null-terminated)} (variable length)
//
// Fast paths handled directly here:
// opcode 0xFF + seq==0 -> mal_cmd_FF_kill_children (SIGKILL all tracked workers)
// opcode 0xFE + seq==0 -> mal_cmd_FE_report_status (spawn helper)
//
// Otherwise builds a 24-byte-per-entry peer array and a 16-byte-per-entry
// payload array (each payload is {byte, strdup(cstring)}) and hands off to
// mal_cmd_dispatch(seq, opcode, N, peers, M, payloads).
//
// Any parse error (overflow, zero-size record, truncated string) frees
// intermediate allocations and returns silently.
void __fastcall mal_parse_cmd_packet(__int64 a1, unsigned int a2)
fig 7:Layout of an attack order. The first 4-byte field carries the attack duration, as in Mirai, and the stop-all (0xFF) and helper (0xFE) codes apply when it is zero.
mal_cmd_dispatch (0x400D2B) launches the attack with Mirai’s double fork. The bot forks a child and goes straight back to listening. The child forks once more and runs the handler for the requested opcode from a 24-entry table (0x00 to 0x17), while the grandchild sleeps for the duration and then kills its parent with SIGKILL. In this manner, every attack ends on time even if the C2 goes silent.
unsigned __int64 v32; // [rsp+8h] [rbp-50h]
const char *v33; // [rsp+10h] [rbp-48h]
__int64 v34; // [rsp+18h] [rbp-40h]
fork();
if ( v10 != -1 )
{
if ( v10 <= 0 )
{
fork();
if ( v11 == -1 )
goto LABEL_7;
if ( v11 == 0 )
{
v13 = mal_getpid(a1: v12);
mal_track_child_pid(a1: v13);
sleep();
v14 = mal_getppid();
mal_kill(a1: v14, a2: 9);
goto LABEL_7;
}
v31 = v11;
v15 = a5;
v16 = mal_getpid(a1: v12);
mal_track_child_pid(a1: v16);
mal_track_child_pid(a1: v31);
if ( a3 != 0 && a4 != nullptr && (v17 = (void *)mal_calloc(a1: a3, a2: 24), v18 = v17, v17 != nullptr) )
{
qmemcpy(v17, a4, 24LL * a3);
if ( a5 == 0 )
{
v19 = 0;
LABEL_24:
v26 = mal_cmd_dispatch_table;
for ( i = 0; ; ++i )
{
if ( i >= (unsigned __int8)mal_cmd_table_count )
goto LABEL_29;
v26 += 16;
if ( a2 == *(v26 - 16) )
break;
}
mal_cmd_dispatch_table[2 * i + 1](a1: a3, a2: v18, a3: v15, a4: v19);
LABEL_29:
mal_free(a1: v18);
Public reporting names the 24 methods, which we group below by how they work. About half need raw sockets, meaning root or CAP_NET_RAW.
| Group | Attack methods | Raw sockets? |
|---|---|---|
| UDP floods | udp, udpplain, udphex, ragnarok, udpbypass |
No |
| Raw-packet floods | syn, ack, pshack, sack, tcpmix, tcpfml, katakomba, tcpcn, udpcn |
Yes |
| Other protocols | icmp, gre, openvpn |
Yes |
| Connection and web | socket, http |
No |
| Game servers | game, fivem, javaclient (Minecraft) |
No |
| Mixed and unused | trapaholic (mixed); tlshttp (present but does nothing) |
Partly |
The FiveM strings mirror the requests a real FiveM (GTA V multiplayer) client sends to a game server: the CitizenFX/1 User-Agent, GET /info.json, POST /client with method=getEndpoints, and the getinfo query. Paired with the dedicated fivem and game methods, the floods pass for genuine player traffic and get past game-server protection.
A Remote Shell, Updates and a Rival Killer
Beyond DDoS, public analysis of this build describes a remote shell: operator commands run through /bin/sh -c, and the output travels back over the same encrypted session. A list of shell paths in the configuration, Android ones included, backs this up. The same analysis describes an update path that fetches a replacement binary over HTTP from hackattackkaboom[.]botlesscucks[.]st and installs it only after checking that it is a genuine ELF file. Between the two, the operator can run anything the web account is allowed to run and swap the bot whenever required.
Hacked servers are often fought over by several botnets, so this bot clears the field. mal_kill_competing_bots (0x40CB5A), Mirai’s “killer”, reads /proc/net/tcp for connections on a given port, matches their socket inodes to processes through /proc/<pid>/fd and kills the owners with SIGKILL. In Mirai, the same routine went after the telnet, SSH and HTTP ports, shutting out legitimate administrators along with rival bots. If this build points it at port 22 or 80, sshd or the web server itself could be killed too.
// mal_kill_competing_bots — Mirai-style anti-rival process killer.
//
// Opens the path at mal_strings_table[216] (= '/proc/net/tcp') and parses
// each line to collect connection inodes matching the current session's
// local port tuple (via sub_40D2BA).
//
// Then walks /proc:
// for each pid dir in opendir(/proc):
// build /proc/<pid>/fd and iterate its entries:
// readlink <entry> -> look for 'socket:[<inode>]' entries
// if the inode matches one we collected -> kill(pid, 9)
//
// Classic Mirai/Gafgyt behavior: remove other bots or interactive admin
// sessions sharing the port range so this instance has exclusive C2 reach.
// Returns 1 if it killed anything (and sleeps briefly), 0 otherwise.
__int64 __fastcall mal_kill_competing_bots(unsigned __int16 a1)
fig 9:The rival killer matches sockets on a port to their processes and kills them.
Indicators of Compromise to Search Across Your Estate
All network indicators are defanged so this page is safe to share. Remove the brackets before using them in any detection rule.
File Hashes
| File | Type | Value |
|---|---|---|
| nfs (second-stage payload) | SHA-256 | 0c5486f06620e26e316f4b33f318b0d646d5e1133789331aa2fed012360d1486 |
| nfs (second-stage payload) | SHA-1 | c472dab4ff7c16782a8037f988d4daa6d4bdb343 |
| nfs (second-stage payload) | MD5 | b61a098fc62a2c2696e196dca78da71a |
| Related build | SHA-256 | e2529d4afa39ae4db2eb05d5176317c906289abf09660c75bdeec43e54c55aa3 |
| Related build | SHA-256 | 267a3c8ed021a0a52c29bf076617a0dfd5cd5285202b8d0bd9ad0712110ccae8 |
Network
| Type | Value | Notes |
|---|---|---|
| Download URL | hxxp[://]92[.]118[.]112[.]100/nfs |
Bot download location |
| Exploit source IP | 45[.]198[.]224[.]169 |
Source of PHP2Shell exploit attempts |
| User-Agent | php2shell-scan/109618 |
PHP2Shell scanner; not proof of infection on its own |
| C2 domain | thunder[.]godofthunder[.]st |
Main C2 hostname |
| C2 domain | hik[.]gettingheadfromduckysmom[.]st |
Backup C2 hostname |
| C2 and update domain | hackattackkaboom[.]botlesscucks[.]st |
Backup C2 and update server |
| C2 IP | 134[.]199[.]154[.]64 |
Live C2 on port 50792 in sandbox runs |
| C2 IP | 117[.]55[.]202[.]115 |
Potassium C2 server |
| C2 ports | 50217, 50341, 50488, 50613, 50792, 50904, 51127, 51289, 51456, 51603, 51871, 52134, 52398, 52645 | Outbound TCP; changes between builds |
| ENS names | 2sexy4u[.]eth, sushiconsumer[.]eth |
Blockchain route for the C2 address |
| ENS record value | 143[.]220[.]202[.]110 |
Value on 23 October 2026; legitimate third-party address, do not block; monitor only |
Files, Processes and Strings on the Host
| Type | Value |
|---|---|
| File path | /tmp/nfs |
| Process | Started as ./nfs php; the php argument is zeroed after start-up, so ps may show ./nfs with an empty argument |
| Parent process | php-fpm, apache2 or httpd starting a shell or downloader that writes to /tmp |
| Listening socket | TCP 1234 on 127.0.0.1 or the host IP, held by a process running from /tmp or /dev/shm |
| Binary strings | HKDF labels wrx-2, wtx-2 and mac-2, stored in plain text |
| Configured paths | /dev/shm/.cache.so, /sbin/crimetables, /sbin/crime-multi (listed in the configuration; worth hunting for) |
Mitigations and Next Steps
If any of these indicators turn up in your environment, treat that server as compromised and work through the following in order. The sequence matters.
-
01
Within the First Hour, Contain It
Isolate the server. Block the hostnames, IPs and 14 C2 ports above at your firewall and DNS, and stop web servers from reaching public DNS resolvers directly, since the bot bypasses your own. Before stopping the bot, save the running file from
/proc/<pid>/exewith its command line, memory maps and open sockets, then kill the process or connect to its TCP port 1234. -
02
Within 24 Hours, Work Out How Far It Went
Search web-server and PHP logs back to at least 17 October for PHP2Shell requests, and for any shell or download tool spawned by
php-fpm,apache2orhttpd. Hunt for the hashes and network indicators across every server, and review firewall logs for outbound traffic to ports 50217 to 52645, sudden outbound bursts and web servers talking to Ethereum RPC endpoints. Monitor that traffic rather than blocking the public RPC providers, which have legitimate users. -
03
Patch PHP, Rebuild and Rotate Secrets
Upgrade to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or a vendor backport for 7.4, 8.0 or 8.1, and restart PHP-FPM and the web-server workers so the patched engine is actually loaded. Since the operator may have had shell access, rebuild from a clean image unless that can be ruled out, and rotate every password, key and token the web account could read. Where that covers personal, payment or health records, check your regulatory notification obligations.
-
04
Harden So It Does Not Happen Again
Restrict web servers to the outbound destinations they actually require, mount
/tmpand/dev/shmwithnoexec, and run PHP as an unprivileged user withoutCAP_NET_RAW. Raise an alert whenever a web-server process spawns a shell or executes anything from a temporary directory, and add the indicators above to your EDR and SIEM watchlists as soon as possible. Our article Beyond CVEs: Detecting Remote Code Execution Through Application Process Behaviour includes ready-made hunts for Microsoft Sentinel, IBM QRadar and Datadog.
References
PHP – Advance notice of the PHP2Shell security release (20 October 2026)
PHP security advisory GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
PHP security advisory GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
SecurityWeek – PHP2Shell: Mass Exploitation Looms for Leaked Critical PHP Flaws
Dark Reading – PHP2Shell Bugs Signal Red Alert for Security Industry
Huntress – Rapid Response: php2shell
Datadog Security Labs – PHP2Shell: detection, analysis, and remediation