On october 9th, 2026, at 18:36 UTC, a bot of mine registered with a DDoS botnet’s command server. Two hours later the operator sent it his first real order:
1 | UDP_FLOOD 45.141.202.x 80 20 1024 |
Sixteen commands followed over the next six and a half hours. The bot is a 70 line python script. It has no payload, no exploits, no attack code. It speaks exactly one protocol and writes everything it hears into a log file. Victim addresses below are shortened, they did nothing wrong and don’t need copycat traffic.
This is the story of how a honeypot, a firecracker microVM lab and one embarrassing wrong port got me that seat.
The honeypot that collects its own evidence
I run a small honeypot on a VPS. It’s a single rust binary with an axum HTTP core and a set of personas, each pretending to be a service that scanners already sweep: a docker API on 2375, a kubelet on 10250, WinRM on 5985, an elasticsearch node on 9200, a Next.js app on 3000, plus raw TCP taps for MSSQL, SMB, RDP, VNC and redis.
The design rule is simple: it never executes anything. There is no shell behind any persona, no real data, nothing to steal. It answers plausibly so the other side keeps talking, and it logs.
The part that pays for itself is the URL pipeline. Every HTTP URL found in any request body gets fetched by a sandboxed downloader and hashed. When someone sends me an exploit that drops a binary from somewhere, the binary usually arrives in my artifacts directory without me touching anything.
The delivery: a react flight exploit
On october 6th the Next.js persona caught a POST with a multipart body. The boundary string was WebKitFormBoundaryReactCVE, which is the attacker helpfully naming his own exploit for me. The body smuggles a react server components flight payload that ends in:
1 | process.mainModule.require("child_process").execSync( |
The URL pipeline fetched the dropper: 108,020 bytes, ELF 32-bit i386, served by SimpleHTTP/0.6 Python/3.12.3, because apparently even botnet operators can’t be bothered to configure a webserver. VirusTotal knew the sample as trojan.mirai/ddos with 13 engines. First seen there: seven hours before it hit my honeypot.
Detonate first, assume nothing
I have a detonation lab for exactly this: firecracker microVMs behind the firecracker jailer, no host filesystem shared into the guest, the sample injected offline into a per-run copy of the disk image, and default-deny nftables with a DNS/HTTP sinkhole on the lab gateway. The guest can talk to the sinkhole and nothing else unless I whitelist a target.
First run, the binary did exactly one interesting thing: it dialed 94.154.43.245:9847. That is the same address the exploit POST came from hours earlier. Delivery and command channel on one box, no separation at all.
Second run with a small CnC emulator listening and lab traffic to that address DNAT’d into it: the bot sends a binary handshake, \x00r\x00\x00\x00\x00\x00\x00 plus 96 bytes of high-entropy data. No plaintext in that one. That’s a reversing project, not a blog post.
The second sample and the port i had wrong
The more interesting binary arrived two days later: 1,546,568 bytes, x86_64, delivered from http://w5q.eu/eclipse.x86_64 by the same crew. Unknown to VirusTotal and unknown to MalwareBazaar at delivery time. I love those.
In the lab it got busy: copied itself to /var/tmp/.e and /usr/lib/.e, wrote an /etc/init.d persistence script, set ulimit -u 50, poked /proc/sys/kernel/pid_max, and cleaned every hidden file from /tmp, /var/tmp and /dev/shm except its own .e, .eclipse and .box. Housekeeping. Then it dialed 176.65.139.36:7070.
Here is the embarrassing part. I had an observer pointed at that exact host for five days already, on port 47821, because an earlier sample of theirs spoke the apex3 protocol there. Port 47821 answered nothing, ever. One four minute detonation told me what five days of blind dialing didn’t: wrong port, right host. Never trust a config string over the binary’s actual behavior.
Plaintext protocols pay
Third run, emulator on 7070: the bot connects and sends its registration in plain text. One line. Its architecture.
1 | x86_64 |
Then the channel is just PING from the server every 45 seconds, PONG back, wait for orders. That’s the whole protocol.
So i wrote the fake bot. It sends x86_64, answers PING with PONG, and logs every line it receives. It downloads nothing, it executes nothing, it attacks nothing. I pointed it at the real 176.65.139.36:7070. It connected, registered, and the server started PINGing it. No credentials, no handshake secret, nothing to fake beyond one word.
What the operator does on a friday night
Between 20:20 and 01:09 the bot logged 147 keepalives and 16 real commands:
1 | UDP_FLOOD 45.141.202.x 80 20 1024 |
The syntax is vector, target, port, threads or duration, packet size. The 1.1.1.1 line is the operator testing his own botnet, twice. The UDP targets look like random victims. The HTTPS ones are the interesting part: graph.vshield.pro belongs to a DDoS protection outfit, and l7srv.su looks like layer-7 stresser infrastructure. Botnets attacking the DDoS economy with other people’s infected servers. That stopped being crime and became an industry a while ago, and the logs read like it.
The line i don’t cross
The observer logs. It answers nothing except PONG. Joining a UDP flood for one second, even for research, even with a tiny packet count, would make my server part of the botnet, and that’s the difference between threat intelligence and becoming the threat. The honeypot side is the same discipline: the whole thing runs as a hardened systemd unit with MemoryDenyWriteExecute and no shell anywhere behind it, because a honeypot that gets popped is just a very honest VPS.
The quieter part of the story
The command channel went dark at 01:10 and has only come up twice since, for minutes at a time, PING only, no orders. The observer keeps dialing every 30 seconds and will be there when the operator comes back to work.
Meanwhile the delivery side rotates: the same crew that used w5q.eu on october 8th showed up with 65512.cfd four days later, same binaries, fresh hashes, one of them 28 minutes old on VirusTotal when it arrived. The domains rotate, the addresses don’t, and the samples arrive hours after they’re compiled. That’s the actual pace of this economy, and a one-person lab with a few fake services can watch it from the inside.
If you run anything exposed to the internet, put a fake something in front of it and watch who talks to it. You don’t need my setup. A non-executing listener is an evening of work, and the first thing it hears won’t be noise. It will be someone’s automated routine, and sometimes, if you build the observer carefully, it will be the boss talking.