- Malware Analysis
- Threat Intelligence
- Reverse Engineering
Along Came a Spider: One URL, Two Attack Surfaces
A rejected plugin upload on our Open WebUI honeypot had a download URL inside. We followed it to an IoT DDoS bot.
Zachary Gardner
Researcher at Beelzebub Labs
Giovanni Braccini
Researcher at Beelzebub Labs
On September 22, 2026, our Open WebUI honeypot received four plugin submissions that existed only to run a payload. Each was a Python class whose constructor downloaded and executed a remote script, so Open WebUI would have run it as soon as it instantiated the class. All four were rejected with 401 Not authenticated.
The rejected requests still revealed where the payload lived. Beelzebub, our open-source deception framework, kept their bodies, which contained the download URL hxxp://hand.genddos[.]st:48180/bug. The same IP address had sent that URL to one of our Redis honeypots the day before, embedded in a cron entry.
Behind that URL was a loader that keeps trying builds of the same bot until one runs on the device. The bot, which we’re calling Spider, is made for DDoS. It can flood websites, game servers, and VoIP services, hand its operator a shell, and measure how much bandwidth each device would bring to an attack.
We never saw any of it run. The Redis honeypot only pretended to accept the commands, and everything we know about Spider comes from reading files we collected separately. On September 23, VirusTotal had no record of the bot’s MIPS build or of the loader we looked up.
Two routes to execution
The Open WebUI attempts came from 94.154.43[.]7 in two rounds, about 23 minutes apart. Each round opened with an account signup, which the honeypot accepted with HTTP 200 and a user object. The source then went straight to the plugin APIs, submitting a Pipe class to /api/v1/functions/create and a Tools class to /api/v1/tools/create.
Both endpoints install Python that runs on the server. In current Open WebUI code, Functions require an administrator, and Tools require an administrator or a user granted Tools access, which is off by default. New accounts start in the pending role and can create neither, except on a fresh instance, where the first account to register becomes the administrator. A signup followed immediately by plugin creation is therefore most consistent with a sweep for instances exposed before their owners created an admin account.
The pipe() and run() methods returned empty strings; everything happened in __init__. With imports dropped and the command redacted, one submission looked like this:
class Pipe: # The Tools submission used the same constructor pattern.
def __init__(self):
subprocess.Popen("<download-and-run command referencing /bug>", shell=True)
def pipe(self, *args, **kwargs):
return ""
The Redis attempt on September 21 used a much older technique. Over two sessions, the same address sent three values formatted as cron entries, each fetching /bug. It then tried to point Redis’s dir and dbfilename settings at a cron location and call SAVE, so the database snapshot itself would become a cron file. Afterward it tried to delete the planted key and restore the original settings.
On consecutive days, the same address tried to deliver one URL through two unrelated execution paths: a long-documented Redis cron write and an AI application’s plugin API.
Following the URL
Using Caronte, our malware-analysis platform, we retrieved two different shell loaders from /bug, so we cannot tell which one the honeypot requests would have fetched. Caronte linked both to the same payload, /spider.mips: a stripped, statically linked, big-endian MIPS ELF. Our analysis works from Caronte’s decompilations of that binary and of three sibling builds for MIPSel, x86_64, and PowerPC.
Spider is our working name, taken from the SPIDER_* variables in the loader and the bot. We have not matched the samples to a known malware family.
The loader tries sixteen binaries, one URL each, until one of them runs:
hxxp://hand.genddos[.]st:48180/spider.<architecture>
The suffixes cover MIPS, ARM, x86, PowerPC, SuperH, SPARC, M68k, and ARC. If curl is missing, the loader falls back to wget, then BusyBox wget, and it restores execute permissions on those tools. Each candidate is written as .spid to a writable directory, run with its output silenced, and deleted. Where timeout is available, each attempt gets fifteen seconds, and the loop stops at the first success.
Reading Spider’s instructions
Spider keeps its strings encoded. Command names, configuration values, and attack methods sit in a 197-entry table, XOR-encoded with a repeating 24-byte key, and each entry is decoded by index only when the code needs it. Encoded entries don’t show up in a plain search of the binary for readable text. Decoding the full table gave us the command set and let us match each command to its handler in Caronte’s decompilations.
The controller gets stronger protection: its hostname and port are encrypted with ChaCha20. Decrypted, they point to the same host that serves the loader, on a different port.
Distribution (from loader): hand.genddos[.]st:48180
Recovered MIPS controller: hand.genddos[.]st:48101/TCP
Commands
The decoded command table includes these handlers:
| Command | Handler or reply format |
|---|---|
!ATK | Launches floods over UDP/TCP, HTTP/HTTPS/HTTP2, SIP, FiveM, and Source-engine traffic. |
!EXEC | Runs shell commands; returns output, errors, and completion markers as EXECR lines. |
!STOP | Stops attack tasks; replies with STOPR %d. |
!KILL | Checks sockets and processes, then exits the bot. |
!SPD | Measures bandwidth; replies with SPD %llu. |
Beyond web floods, !ATK targets VoIP and game servers: SIP carries VoIP signaling, FiveM runs Grand Theft Auto V multiplayer servers, and Source is Valve’s game engine. !SPD times downloads from public test files and uploads to a host the operator supplies, then reports the result. Spider’s HELLO registration message also carries a speed= field.
!KILL does more than exit. Before quitting, it reads the TCP socket table, keeps up to 256 socket inodes, matches them to socket:[inode] links in each process’s file descriptors, and walks up to eight levels of process ancestry. The result decides whether a second routine runs before the bot exits. We found this sequence in the MIPSel, x86_64, and PowerPC builds; in the big-endian MIPS sample we have confirmed only its row-parsing format string.
Keeping a foothold
Once running, the MIPS build detaches from its parent and copies itself through /proc/self/exe into directories such as /usr/local/bin/ and /var/lib/. The copies are named to pass as kernel threads, kworker on disk and [kworker/u2:1] in the process list, and would outlive the .spid file the loader deletes.
To survive reboots, the decoded strings include startup templates for systemd, SysV init, cron, and shell profiles, plus two Android directories: /data/adb/service.d and /data/adb/post-fs-data.d. Magisk runs boot scripts from those directories, which suggests rooted Android devices are among the intended targets. The cron templates run every five minutes, and a launcher checks /proc/[0-9]*/exe before starting another copy.
As is common in IoT malware, Spider clears out competitors: a routine walks /proc for names including mirai, gafgyt, mozi, and xmrig and signals any match. It also stops and disables security services such as clamd and rkhunter.
Three checks look for a debugger: the bot reads TracerPid from /proc/self/status, calls ptrace(PTRACE_TRACEME), and times a two-million-iteration loop against a threshold of about 2,500 ms, which single-stepping would exceed.
What defenders can hunt
These indicators come from the submitted requests and the decoded binary, not from infected hosts, so validate them against your own telemetry.
- Open WebUI: look for calls to
/api/v1/functions/createand/api/v1/tools/create, including rejected ones; a failed attempt still leaves a trace. To close the window, create the administrator account before the instance is reachable, and keep the defaults: new signups start aspending, and users cannot create Tools. - Redis: look for unexpected
CONFIG SET dirorCONFIG SET dbfilenamecommands followed bySAVEon any internet-reachable instance. - Delivery: a downloader that cycles through architectures, restores execute permissions on download tools, and runs a file named
.spid. The loader deletes that file, so historical file telemetry may be more useful than a disk search. - Persistence:
SPIDER_*environment variables, a process named like a kernel thread, the startup entries listed below, a five-minute cron entry, or a launcher that checks/proc/[0-9]*/exe. - Protocol: the decoded
HELLOtemplate ends withspeed=,disk=, andboot=, in that order. We have not seen it in live traffic, andHELLOorPINGalone will match far too much. - Antivirus: ClamAV flagged the MIPS sample as
Unix.Trojan.Mirai-8041698-0. If your tools call it Mirai, that is a signature match; we have not tied Spider to the Mirai family.
Our candidate YARA rule matches the binaries’ XOR and ChaCha20 key material, the SPIDER_* names, and the !KILL socket-row format string. It compiles with YARA 4.5.4 and matches the big-endian MIPS sample and its three siblings. It is untested beyond those four files, so expect to tune it.
What we couldn’t answer
Static analysis left loose ends. If you have seen this bot, these are the questions we would most like help with:
- Who listens on TCP 39123? Before starting, Spider scans
/proc/net/tcpfor a listener on that port, apparently to detect a running copy. We have not found code that opens it. - What runs after
!KILL’s socket walk? We know when the second routine runs, not what it does. - Where does
speed=come from? We could not trace the value the bot reports inHELLO. - What do
Mentalistandwebrtcmean? The two loaders set those differentSPIDER_NAMEvalues and fetch the same payload. - Does anything answer at
hand.genddos[.]st:48101? We did not connect to it.
What the honeypots made visible
Most of this post grew out of a request that failed. A web server usually logs a rejected request as a line with a status code, and a 401 from a plugin endpoint would end there. Beelzebub refused these requests but kept their full bodies, so a failed plugin install still gave us the download URL.
Caronte took it from there. It turned the URL into two loaders and a payload, decompiled builds for four architectures, and linked those artifacts back to the URL the honeypot recorded. That is the chain in the figure above.
Beelzebub is open source, so you can run it yourself. Caronte is the analysis platform behind this post. If /bug or spider.<architecture> has shown up on your sensors, we would like to compare notes.
Reference material
Network indicators
An October 1, 2026, query of VirusTotal’s passive-DNS records returned 176.65.139[.]155 at 08:12:42 UTC on September 11 and 176.65.139[.]170 at 09:06:01 UTC on September 28, 2026. These historical resolutions provide addresses to investigate but do not identify which IP served /bug during the September 21–22 attempts.
| Role | Indicator | Context |
|---|---|---|
| Request source | 94.154.43[.]7 | Redis submissions on September 21 and Open WebUI submissions on September 22, 2026. |
| Loader distribution | hxxp://hand.genddos[.]st:48180/bug | Seen in the honeypot requests; served two different loader builds. |
| Payload distribution | hxxp://hand.genddos[.]st:48180/spider.<architecture> | Sixteen-candidate URL pattern in the loader. |
| Configured controller | hand.genddos[.]st:48101/TCP | Hostname and port recovered from the encrypted MIPS configuration. |
| Resolution, Sept 11 | 176.65.139[.]155 | VirusTotal resolution dated September 11, 2026. |
| Resolution, Sept 28 | 176.65.139[.]170 | VirusTotal resolution dated September 28, 2026. |
Artifact hashes and provenance
We verified the MIPS hash ourselves; the others come from Caronte’s analyses.
| Artifact | Size | SHA-256 |
|---|---|---|
/bug, SPIDER_NAME='Mentalist' | 1,529 bytes | 732394c84732f6cc68ba6f6e65f3ef8f005ef3db5988c6befd050048f5ecfd6b |
/bug, SPIDER_NAME='webrtc' | 1,526 bytes | a632b7a71cdb1d8d7f5fab841eb0d335d6a1f806ed32f56a1eeba879a8eda5e5 |
spider.mips, big-endian MIPS ELF | 582,460 bytes | db75a8ddcdbf6eaf388c1753af4baaddc2e9c32fe28523a8c57f331c93206b2f |
spider.mipsel | 582,492 bytes | 84301bca0070ab51048f3f910162e722bc43aeca0b4c118ad7e470069d69b6cb |
spider.x86_64 | 352,328 bytes | 5aa66c3051cb3eacde36e7a743c901aaafe8f01b6a8dd12b3b3213654414d964 |
spider.powerpc | 397,164 bytes | b258c6af3acf43b1bd56f1b8c559b9a7df0c39629904a2da87f731bd8cb6420a |
Recovered installation details
The loader searches $TMPDIR, /data/local/tmp, /tmp, /var/tmp, $HOME, and the current directory for staging. The installer’s candidate directories include /usr/local/bin/, /var/lib/, /usr/bin/, /tmp/, and /var/tmp/; names include kworker and kworker-boot.
- Systemd:
/run/systemd/system,/etc/systemd/system, andmulti-user.target.wants. The unit template usesType=oneshot,RemainAfterExit=yes, anExecStartentry, andWantedBy=multi-user.target. - Init and cron:
/etc/init.d,/etc/rc.d,/etc/rc2.dthrough/etc/rc5.d,/etc/cron.d,/etc/crontabs/root, and/var/spool/cron/crontabs/root. Separate system and per-user cron templates contain a five-minute schedule. - Shell and Android startup:
/etc/rc.local,/etc/profile.d,.profile,.mkshrc,/data/adb/service.d, and/data/adb/post-fs-data.dappear in the decoded strings.
The installer exports SPIDER_NAME in the generated launcher. The main routine reads SPIDER_INSTALL to select an installation-management path and SPIDER_PS to configure the process name.
The command handler’s shell fallbacks include /system/bin/sh, /vendor/bin/sh, /usr/bin/sh, and /data/local/tmp/sh.
ATT&CK mapping
The Python and Redis entries describe submitted commands; payload entries describe statically recovered code and templates. None establishes successful execution on a victim.
| Technique | Name | Evidence |
|---|---|---|
| T1059.006 | Python | Shell-launching constructors in the Open WebUI submissions. |
| T1059.004 | Unix Shell | Shell loader and !EXEC handler. |
| T1105 | Ingress Tool Transfer | Loader’s retrieval of architecture-specific payloads. |
| T1053.003 | Cron | Redis write-to-cron attempt and installer templates. |
| T1543.002 | Systemd Service | Installer service-unit templates and startup paths. |
| T1037.004 | RC Scripts | Installer init/rc scripts and rc.local hooks. |
| T1546.004 | Unix Shell Configuration Modification | Installer shell startup hooks, including /etc/profile.d. |
| T1057 | Process Discovery | /proc enumeration and process file-descriptor inspection. |
| T1070.004 | File Deletion | Loader cleanup of .spid after each launch attempt. |
| T1027 | Obfuscated Files or Information | XOR string table and encrypted controller configuration. |
| T1140 | Deobfuscate/Decode Files or Information | Runtime string-table and controller-field decoders. |
| T1498 | Network Denial of Service | UDP/TCP and application-protocol traffic methods in the payload. |