Blog · 2026-05-29
1,001 IPs, 64 countries, one operation: mapping a botnet by its back end
A single attacking IP tells you little. The back end it pulls its payload from, and the client fingerprint it presents, are the parts operators reuse. Correlating both across the sensor network collapses internet noise into discrete operations: one cluster of 1,001 IPs across 306 networks and 64 countries, tied to eight shared staging servers and a single TLS and HTTP fingerprint that appears nowh
A single attacking IP does not tell you much on its own. It is one compromised box out of a sea of them, and by the time it reaches your logs it has usually been cleaned, reassigned, or rotated out. The thing that identifies an operation is the part the operator reuses, and the part they reuse most is the back end: the server the payload gets pulled from once the exploit lands.
We can chase that across the whole sensor network because we keep the payloads and not only the connections. Every dropper that comes through carries the URL of its next stage somewhere, sometimes in the clear after a wget, sometimes inside a base64 blob in a PHP shell_exec. If you pull that URL out of every dropper, link each attacking IP to the servers it fetched from, and then connect any two servers that share delivering IPs, the internet's background noise starts to separate into discrete operations.
one operation, a thousand nodes
Run that over the last hundred days and the biggest cluster that falls out looks like this.
![]()
That is 1,001 source IPs bound to eight staging servers, the orange hubs. The IPs sit in 306 different autonomous systems across 64 countries, which is exactly why you would never connect them by looking at addresses. What ties them together is the back end. The eight servers behave as one pool, because the same delivering IPs pull from many of them. The two busiest, 31.57.216.121 and 46.151.182.82, share 51 delivering IPs between them, 178.16.55.224 and 31.57.216.121 share 45, and the two servers hosted on Korea Telecom (125.135.169.171 and 14.46.136.77, both AS4766) share 38. The IPs that fetch from more than one server are the glue, and they are why this reads as one campaign rather than eight unrelated ones.
![]()
The exploit underneath it is old and dull. It is Apache path traversal, CVE-2021-41773 and its %%32%65 double-encoded cousin, hitting /cgi-bin/.../bin/sh on ports 80 and 443. The payload is a self-replicating shell stage the operators tag apache.selfrep, so every server it lands on becomes another node that scans and re-delivers. It has run at a steady 70 to 125 distinct IPs a week for the whole window, with a small lift in early May.
![]()
the fingerprints agree
The shared back end is one way to draw the boundary. The TLS handshake is a second, and it lands on the same population. Of the IPs in this cluster that negotiate TLS, 655 present the exact same JA4 fingerprint, t13i170900_5b57614c22b0_78e6aca7449b, and that value appears on no other IP anywhere else in the dataset over the same period. The HTTP fingerprint tells the same story: 687 of them share one JA4H, po11nn0700_c5a94e7539c9, again unique to this set. Two independent signatures, the staging pool and the client build, draw the same outline, which is about as good as honeypot data gets for saying a thousand scattered addresses are running one tool.
The diamonds in the graph are those fingerprints, and the cyan dots are the IPs that carry them. A smaller green island hangs off to one side. About a tenth of these nodes also turn up brute-forcing SSH, and 102 of them do it with a single shared SSH client fingerprint (HASSH 19532158b559096b89b1a5f7d17175b2). That HASSH is the softer of the three links, since it also shows up on roughly 120 addresses outside this cluster, so it is better read as a common brute-force client that this campaign happens to use than as a signature unique to it.
the small operations separate just as cleanly
The same method pulls the small fish apart instead of lumping them together.
![]()
The graph breaks into four islands. One is a multi-architecture Mirai variant whose binaries are named manji.arm7, manji.mips, manji.x86 and xd.x86, served from two hosts in the same 94.156.152.0 block. Another serves nullnet_load.mips and memory_load.mips from a separate pair. A third pulls heilong.mips, which is Chinese for black dragon, from a host paired with a DuckDNS name, 69sexy.duckdns.org, the kind of free dynamic-DNS C2 that small operators lean on. The fourth quietly fetches a plain install.sh from file.gpu5.com. None of them connect to each other or to the big cluster, because none of them share delivering IPs or fingerprints. That is the same test running in the other direction: when operations really are separate, the graph leaves them separate.
why this works, and what it is good for
This only works because the sensors keep the request body and the handshake. A NetFlow record or a firewall log gives you the connection but not the wget URL inside it, so it can tell you an IP knocked without telling you which back end it served or which client knocked. With the payload and the fingerprint in hand you get two join keys, and both are things botnet operators are worse at rotating than front-end IPs, because standing up fresh staging and rebuilding your client are more effort than burning an address.
Two honest caveats are worth stating. The first is that the exploits and malware families here are all known, so this is attribution and structure rather than a new bug. The second is that these staging servers are very likely compromised boxes themselves rather than the operator's own rented infrastructure, because the apache.selfrep design turns every victim into a re-deliverer, so the eight hubs are best read as the most-reused nodes in a worm rather than a clean command tier.
If you want the indicators, they are concrete: the eight staging servers above, the apache.selfrep payload tag, the client fingerprints JA4 t13i170900_5b57614c22b0_78e6aca7449b and JA4H po11nn0700_c5a94e7539c9, and CVE-2021-41773 traffic to /cgi-bin/...%2e.../bin/sh on 80 and 443. The patch for that bug shipped in 2021. The fact that it still anchors a thousand-node operation in 2026 is the part worth sitting with.
If you want to reproduce the map, the dataset is queryable. Pull the staging URL out of each dropper body, including the base64 PHP ones, build the links from delivering IP to server, group on the shared JA4, and connect servers that share delivering IPs. The clusters fall out on their own. The figures here cover the full dataset window of roughly a hundred days; the links in this post open the lookup at its ninety-day maximum, so the live counts you land on will run a little below the numbers above.