RondoDox Botnet Malware Analysis: Dropper Reverse Engineering, Attribution, and New IOCs

:books: RondoDox Incident Series - Part 2 of 2

Part Title Status
1 I Got Hit by a Botnet-for-Hire: The Full RondoDox Incident :white_check_mark:
2 - You are here RondoDox Botnet Full Malware Analysis, Attribution, and New IOCs :white_check_mark:
3 RondoDox Part 3: How to Actually Counter It (Even When Your VPS Is Already Choking) :white_check_mark:

← Previous: Part 1 the incident story, what happened, how it felt, how I cleaned it up, and where I got a few details wrong the first time I wrote it up.

This post is the other half. Less story this time the binaries, the code, the attribution, and the indicators, reconstructed from static analysis of the actual malware pulled off my own server. Where I’m inferring rather than proving something, I’ve said so directly, same as in Part 1.


Before We Start

Part 1 was about what happened to me. This one is about what the malware actually is reverse engineered, attributed, and documented for anyone else who runs into it.

Short version: this was RondoDox, a tracked commercial DDoS-for-hire botnet that also mines Monero as a side hustle on every box it lands on. Everything below comes from directly examining the binaries recovered from my server, cross-referenced against prior public research from Bitsight TRACE and NICTER.

One thing worth saying up front, since Part 1 admitted a few mistakes: everything in this post is static analysis of files that still exist and can be independently re-checked hashes, disassembly, string tables. That’s a different kind of evidence than “what I remember seeing in htop at 6am,” and it’s a lot more reliable. I’m calling out the one place that distinction still matters below.


Part 1 - The Entry Point, Recapped

Covered in full in Part 1 including the correction that I can’t actually prove my firewall was doing anything at the time. The short version, for reference:

Vulnerability: unauthenticated file upload + a Docker volume mount that exposed the entire host filesystem, in an open-source video generation tool, exposed on a high port.

Three compounding issues, not one:

# Vulnerability What
1 Zero authentication Auth middleware present in the code, but commented out
2 Host filesystem mount Container’s working directory is the host’s, writable, as root
3 Unrestricted file upload File extension checked, content never was

The auth-bypass and file-exposure findings here are what I originally reported and got published as GHSA-hhqf-49vx-fv7x (High severity no CVE has been assigned to it). The specific host-filesystem-write path using that same auth gap plus the volume mount to write arbitrary files, rather than just read exposed ones is something I only fully verified after the incident, when I went back and built a live proof-of-concept against the same root cause.

Why it was invisible to normal defenses: Docker inserts its own iptables rules ahead of whatever the host firewall thinks it’s enforcing. Any container published to 0.0.0.0 is reachable directly from the internet no matter what the host firewall’s rule set says. The exploit never touched my reverse proxy and generated no web server logs RondoDox hit the exposed API directly.

The exploit request’s User-Agent - an actual signature you can hunt for in your own logs:

Mozilla/5.0 ([email protected])

That’s not a spoofed browser string. It’s the operator’s own contact address, sitting in plain text in every request their tooling makes. More on that address in Part 5.


Part 2 - The Attack Chain

1.  RondoDox masscans the hosting provider's IP block → finds the exposed port
2.  GET /docs → Swagger UI reveals every unauthenticated endpoint
3.  POST file upload with payload → lands on HOST filesystem as root
4.  Downloads a shell script from the distribution C2
5.  Script disables defenses, kills competing malware, finds a writable dir
6.  Downloads the main dropper, launches it with a spoofed app-name argument
7.  Dropper connects to a second C2 server
8.  Dropper drops the XMRig miner binary and launches it
9.  XMRig mines Monero to the operator's pool
10. Seven persistence mechanisms deployed, files made immutable
11. chattr, crontab, and iptables binaries deleted to blind the defender
12. Botnet C2 can now issue DDoS commands to the host at any time

Same caveat as Part 1: steps 1–3 match this botnet’s documented behavior everywhere else it’s been observed, and the vulnerability they describe is verified but the specific request that hit my server that night didn’t survive in any log I preserved. Everything from step 4 onward, I can trace directly, starting from a root-level command that runs the dropper. That’s the first artifact-backed moment in the whole chain, and it’s where the malware analysis below actually begins.

Step 12 is the reason this matters beyond one compromised VPS. The mining is passive income. The product being sold is DDoS capacity.


Part 3 - Malware Analysis

3.1 The Miner - /bin/softirq

Property Value
Software XMRig 6.26.0 (official open-source release)
Type ELF 64-bit, statically linked, stripped
Size 8,350,992 bytes (~8.35 MB)
SHA256 b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49
Name Chosen to mimic the kernel thread [ksoftirqd]
Permissions world-writable/executable, plus the immutable + append-only attribute set

The important finding here: the binary itself is unmodified, official XMRig. No embedded IPs, no backdoor, no custom code. All the malicious behavior which pool, which credentials, which flags gets injected purely through the command line at launch. That’s deliberate: it means the binary itself looks completely clean to any hash-based detection system that only flags known-bad samples. XMRig isn’t malware by itself. Only the way it’s launched is.

One honest caveat, carried over from Part 1: I don’t have a machine-captured process listing showing this miner actually running with its full command-line arguments by the time I ran a proper capture, I’d already killed it by hand, working from a glance at my own terminal. The pool addresses and credentials below are consistent with the persistence mechanisms I did capture cleanly (they reference the same launch arguments), so I’m confident in them, but I want to be clear they rest on that one secondhand observation and not on an independently reproducible artifact.

3.2 The Dropper - /etc/rondo/rondo

Property Value
Size 141,720 bytes
Type ELF 64-bit, statically linked, stripped, no embedded build ID
SHA256 062713105b7034e6c764f2148e247a3b61d3340ce6d025ae7c869260d24323ff
Language Likely Rust or Zig no Go runtime table found, ruling out Go
Strings Only 391 printable strings of 5+ characters survive in the raw binary — everything meaningful is encrypted

Every string in this binary IPs, commands, error messages, everything is encrypted. Pulling it apart, cross-referenced against NICTER’s published analysis of the same family, shows a five-stage scheme:

1. XOR with a 64-byte key
2. Bit rotation, left by 5
3. String reversal (two passes)
4. XOR with a second, 32-byte key
5. Caesar cipher substitution

Once decrypted, the capability list becomes clear.

DDoS arsenal - 18 attack vectors:

.udp .sudp .udpraw .syn .tcp .tcp2 .tcpbypass
.handshake .psh .tcplegit .http .icmp .gre
.spoof .raw .stun .dtls

Game server targets - the “product” being sold to DDoS-for-hire customers:

valve, roblox, fortnite, dayz, fivem, raknet,
discord, minecraft, csgo, wireguard, samp

Anti-analysis - kills 20+ tools on sight, most of them the exact tools a defender would reach for first:

htop, pstree, lsof, netstat, wireshark, tshark,
tcpdump, gdb, strace, ltrace, valgrind, ufw

Self-destruct capability, presumably a last resort if it detects active analysis:

rm -rf /* --no-preserve-root
shutdown, poweroff, halt, reboot

Download fallback chain - tried in order until one works, which says a lot about the range of stripped-down IoT and embedded targets this thing expects to land on:

wget → curl → tftp → ftpget → busybox wget → toybox wget → python3

Runtime behavior, confirmed from process and log data: the dropper runs itself with a .persisted suffix argument on reboot, spawns a family of helper processes under a name chosen to blend into a normal ps aux listing, and pulls down shared-library files for its mining math into /tmp/ under randomized, dot-prefixed filenames — the kind of naming that skips right past a casual ls.

3.3 The Fake crontab - A Detail I Almost Missed

While going through the persistence artifacts, I found a small setgid binary sitting near the cron tooling. My first instinct was to write it off as some kind of watchdog or helper process and move on.

It isn’t one. Cross-referencing it against the system’s own cron logs, this binary was the thing actually performing crontab-style list operations — at the exact moments the real /usr/bin/crontab had already been deleted. It’s a drop-in replacement: the attacker deleted the legitimate tool and substituted their own, so they could keep managing persistence entries without needing anything from the original package.

I don’t have a clean explanation for one detail: its file timestamp says it was placed on this system months before the incident, not on the night of the attack. That could be a backdated timestamp, or it could mean this box had something on it earlier than June 13th that never got caught. I’m leaving it as an open question rather than guessing at an answer I can’t back up.

3.4 Persistence - Seven Redundant Layers

Layer 1: systemd service unit
Layer 2: SysV init script + runlevel symlink
Layer 3: /etc/crontab                          (@reboot)
Layer 4: /etc/cron.d/ entry                    (@reboot)
Layer 5: /var/spool/cron/root                  (@reboot)
Layer 6: /var/spool/cron/crontabs/root         (@reboot)
Layer 7: /etc/rc.local                         (startup)

All seven were deployed within roughly a minute of each other, during the attacker’s second visit not the initial break-in. Alongside them: every malware file got chattr +ia (immutable + append-only), and the tools needed to inspect or undo any of it chattr, crontab, iptables were deleted right after use.


Part 4 - Attribution: Confirming This Was RondoDox

This is the part I want to walk through properly instead of just dropping a comparison table, because the process of confirming it was more interesting than the result.

I had a handful of concrete, unique values pulled directly from the binaries and persistence entries: a mining pool address, a username/password pair, a specific process name, and a naming convention on the launch arguments. None of those are things a random opportunistic script would reuse they’re operator-specific. So the question was simple: does anything else, anywhere, show the same values?

Cross-referencing against Bitsight TRACE and NICTER’s published RondoDox research, most of it lined up exactly:

Indicator My server Published research
Miner process name /bin/softirq softirq.x86_64
Pool IP 45.94.31.89:443 45.94.31.89:443
Credentials react / 3cthDeQ5 react / 3cthDeQ5
Launcher name rondo rondo.x86_64
Argument naming convention react.x86_64 <vulnerability>.<architecture>

The one value that didn’t line up cleanly: the XMRig build version on my server (6.26.0) is newer than the version referenced in the research I was comparing against (6.24.0). Worth being upfront about that rather than smoothing it over a version bump between reports is completely normal for an actively maintained campaign updating its payloads over time, but it’s a real discrepancy, not a perfect match, and I’d rather flag it than claim the indicators lined up “exactly” when one of them didn’t.

The naming convention is the piece that actually convinced me this wasn’t a coincidence. react.x86_64 isn’t a random string it decodes as <exploited-vulnerability>.<cpu-architecture>: react referring to the app that got exploited, x86_64 the target’s CPU. That’s internal bookkeeping. It’s how an operator running hundreds of unrelated compromises keeps track of which exploit got them into which box, and it’s exactly the kind of convention researchers use to cluster otherwise-unrelated incidents back to a single campaign. Seeing my own server’s argument match that pattern, against a public report describing infrastructure I’d never looked at before finding this, was the moment attribution stopped feeling like a guess.

Scale, per published tracking:

  • 174 vulnerabilities exploited (148 mapped to known CVEs)
  • 18 CPU architectures targeted
  • 32 known infrastructure IPs
  • Peak observed rate: ~15,000 exploitation attempts/day
  • Active since May 2025

Business model: commercial DDoS-for-hire, with Monero mining running in the background on every box as passive revenue between paying jobs.


Part 5 - New IOCs (Not Previously Published)

These four weren’t in any prior public writeup on RondoDox I could find submitted to threat-intel trackers as part of this research.

Type Value Notes
NEW - C2 IP 204.10.194.134 Malware distribution server, hosts an operator contact page
NEW - Operator email [email protected] Found on the C2 page and embedded directly in the exploit’s User-Agent
NEW - Scanner IP 95.214.52.198 Confirmed RondoDox scanner, probed this server twice before the successful exploit
NEW - Exploit vector The MPT vulnerability / GHSA-hhqf-49vx-fv7x (no CVE assigned) Not previously linked to RondoDox activity

Previously known, confirmed present here:

Type Value
Mining pool 1 45.125.66.100:444 (TLS)
Mining pool 2 45.94.31.89:443 (TLS)
Mining credentials react / 3cthDeQ5
Brute-force IP (unrelated, failed) 185.231.180.156
Cover domain welstanbapparel.com → resolves to 45.125.66.100
Miner SHA256 b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49
Dropper SHA256 062713105b7034e6c764f2148e247a3b61d3340ce6d025ae7c869260d24323ff

A separate, unrelated SSH brute-force attempt against root from 185.231.180.156 ran for about 90 minutes the same night every attempt failed. It’s included here for completeness, but it isn’t part of the RondoDox chain; it’s just background internet noise that happened to overlap in time.


Part 6 - Infrastructure Map

Component IP Notes
C2 server 1 45.125.66.100 Mining pool 1
C2 server 2 45.94.31.89 Mining pool 2
Distribution server 204.10.194.134 New hosts an operator contact page
Scanner 95.214.52.198 New confirmed scanning IP
Cover domain welstanbapparel.com Resolves to C2 server 1

Part 7 - Why RondoDox Succeeded Where Others Failed

My server got probed constantly in the weeks before this this wasn’t the first attempt, just the first successful one:

Attacker Method Result
A known IoT botnet CGI shell injection Blocked
Generic port scanner NSE probe scan Blocked
Kubernetes scanner kubectl scan Blocked
Generic web vuln scanner Broad app scan Backend unreachable
RondoDox Direct hit on the exposed container port, firewall bypass Success

The pattern: everything that failed was aimed at the parts of the server that were actually reachable through my reverse proxy the parts I’d at least thought about securing. RondoDox succeeded because it went straight for a container port that bypassed the firewall layer entirely, whether or not that layer was even doing its job that night. Every other attacker was knocking on the front door. This one found a door I didn’t know I’d left unlocked.


Part 8 - Remediation Applied

Item Status
Miner binary removed :white_check_mark:
Dropper directory removed :white_check_mark:
All 7 persistence mechanisms removed :white_check_mark:
Planted SSH key removed :white_check_mark:
Vulnerable containers removed :white_check_mark:
Exposed ports closed :white_check_mark:
Firewall correctly enforced (Docker’s rule insertion accounted for) :white_check_mark:
C2 IPs blocked at the firewall :white_check_mark:
chattr restored :white_check_mark:
crontab restored :white_check_mark:
SSH keys rotated :white_check_mark:

Cleanup method for files locked with the immutable flag, once chattr itself was gone full walkthrough of hitting this wall and getting past it is in Part 1:

import ctypes, os

libc = ctypes.CDLL('libc.so.6', use_errno=True)
FS_IOC_GETFLAGS = 0x80086601
FS_IOC_SETFLAGS = 0x40086602
FS_IMMUTABLE_FL = 0x10
FS_APPEND_FL    = 0x20

fd = os.open(b'/bin/softirq', os.O_RDONLY)
flags = ctypes.c_int(0)
libc.ioctl(fd, FS_IOC_GETFLAGS, ctypes.byref(flags))
flags.value &= ~(FS_IMMUTABLE_FL | FS_APPEND_FL)
libc.ioctl(fd, FS_IOC_SETFLAGS, ctypes.byref(flags))
os.close(fd)


Part 9 - Submitting These IOCs

If you want to help get these blocked more broadly, this is where they’re going:

  • ThreatFox (threatfox.abuse.ch) - all IPs and hashes
  • MalwareBazaar (bazaar.abuse.ch) - the dropper binary, tagged RondoDox
  • AbuseIPDB - the new scanner IP, distribution server, and the unrelated brute-force IP
  • NICTER - the new C2 IP and the exploit vector
  • Bitsight TRACE - the new infrastructure findings

What We Learned

RondoDox - a commercial DDoS-for-hire botnet, active since mid-2025, that also runs Monero mining as passive revenue on every infected host. Not opportunistic malware an operation with a bookkeeping convention, a contact page, and paying customers.

Living-off-clean-binaries - the miner itself is unmodified, official-looking XMRig. All malicious intent lives in how it’s launched, not in the binary. Hash-based detection alone won’t catch this.

String encryption in droppers - a five-stage XOR/rotation/Caesar scheme is enough to defeat casual inspection, but not enough to survive someone willing to actually reverse the algorithm.

Naming conventions as attribution - launch arguments like react.x86_64 aren’t random. They’re internal bookkeeping (<exploited-vulnerability>.<architecture>) that researchers use to cluster unrelated compromises back to the same campaign.

Tool deletion doesn’t remove the underlying syscall - deleting chattr, crontab, or iptables blinds someone relying on those specific tools. It doesn’t block the kernel API underneath them.

One open question I still can’t answer: whether the timestamp on the fake crontab binary means this box was touched months before the incident I actually caught. I don’t have evidence either way, and I’d rather leave it unresolved than manufacture a tidy explanation.

GHSA-hhqf-49vx-fv7x - unauthenticated file upload plus a host filesystem mount, in the video-generation tool I’d deployed to build a PoC for. No CVE has been assigned to it. If you’re running anything with a similar Docker volume pattern, check it.


References

  • Bitsight TRACE: RondoDox Botnet Malware Analysis
  • Bitsight TRACE: RondoDox Botnet Infrastructure Analysis
  • NICTER: RondoDox Campaign Report
  • FortiGuard Labs: RondoDox Unveiled
  • GitHub Advisory: GHSA-hhqf-49vx-fv7x (no CVE assigned)
  • MalwareBazaar: b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49

← Previous: Part 1 - the incident story, start to finish, corrections included.

That’s the full RondoDox writeup. If you’re running any exposed Docker containers, check what your firewall actually thinks is closed versus what’s really reachable from the internet - that gap is exactly how this one got in.