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 | sh as 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 | sh as 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 noexec on 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 no in /etc/sshd_config
  • adduser deploy && usermod -aG docker deploy
  • Install fail2ban
  • Audit /root/.bash_history. If you see tools you do not recognize, assume compromise.

Indicators of compromise

TypeValue
Miner binaryHelloMrMeeseeks
Miner binarypls_pak_choi
Miner binaryplazooza
Mining pool15.235.234.220:3333
Monero wallet49V3ssTZLU4fJh9SaM4aZYb2Yr9qj7MqRDPKLyrc3EpSeRPn8kxb9oLGgN3GyXcuNPGhwnWaJANXQcFJq8ARmLB58dXP9Ux
Malware sourceopenfang.sh
Unknown toolgithub.com/qwibitai/nanoclaw
thanks for reading
copy linkshare ↗
Read next
$ prompt --strict
Article · 4 min

Notes on prompting like an engineer

Specs over vibes. Tests over hopes.

CV.pdf
Resume / CV

What I've worked on

Six years across IT, GIS, and AI. Updated quarterly.

Start with an AI Opportunity Audit