Your monitoring dashboard shows CPU usage at 98% — but the application is idle and traffic looks completely normal. That combination — pegged CPU, zero explanation — means something on your server is burning resources that aren’t yours to spend.
A client of mine learned this the hard way. An XMRig miner had quietly hashed Monero on their VPS for three weeks before an unusually high cloud bill triggered an alert. By the time I was called in, we spent the entire weekend tracing the infection, scrubbing persistence mechanisms, and rebuilding hardening from scratch. My own server got hit by an SSH brute-force at 2am years ago — after that, security is the first thing I configure on any new box.
The Real-World Problem: Silent CPU Hijacking
Cryptominers occupy an odd niche in the malware world. They don’t destroy data, don’t demand ransom, and work hard to stay invisible. The business model is pure compute theft: your CPU cycles, your electricity bill, their Monero wallet. Every hour they remain undetected is profit for the attacker — and an unexplained charge for you.
Symptoms creep up gradually:
- Unexplained CPU spikes (often 70–100% across all cores)
- Server response times degrading despite normal traffic
- Higher-than-expected cloud compute bills
- Unusual outbound network connections to unknown IPs
- New cron jobs or systemd services you didn’t create
By the time most admins notice, the miner has been running for days or weeks.
Root Cause Analysis: How Did It Get In?
Before you touch a single process, figure out the entry point. Clean up the miner without closing that door, and it’ll be back within hours. Cryptominers exploit the same entry points repeatedly — know the patterns, and the forensic trail becomes much easier to follow.
SSH Brute Force / Credential Stuffing
Exposed SSH with password authentication is the #1 vector. A freshly deployed VPS can see over 1,000 login attempts in the first hour. Automated bots scan entire /8 ranges around the clock. Weak or reused passwords on root and sudo accounts get cracked fast — sometimes in under five minutes on a targeted system. Once in, the attacker downloads and runs the mining payload immediately.
Exploited Web Applications
Unpatched WordPress, PHP apps, or frameworks with known RCE vulnerabilities give attackers direct command execution. Log4Shell (CVE-2021-44228), disclosed in December 2021, became a cryptominer delivery vehicle within 48 hours of public disclosure. PHP deserialization flaws and SSTI vulnerabilities follow the same pattern. Miners get installed through a web shell or direct command injection — often within seconds of exploitation.
Exposed Docker and Kubernetes APIs
Port 2375 without TLS authentication is essentially unauthenticated root access to your host. Attackers connect directly to the Docker API, spin up a privileged container with host filesystem access, and break out to install their payload. A misconfigured Kubernetes dashboard gives the same result through a different path.
Compromised Packages and CI/CD Pipelines
Malicious npm or PyPI packages — and compromised build pipelines — inject mining code silently into your deployments. These are the hardest to catch. The delivery mechanism looks completely legitimate, so standard perimeter defenses don’t flag it.
Detection: Map the Full Infection Before Touching Anything
Killing processes before you understand the full scope almost always backfires. Miners replant themselves from persistence mechanisms you haven’t found yet. Here’s the systematic approach that actually works.
Step 1: Identify Suspicious Processes
# Check top CPU consumers
top -b -n 1 | head -20
# More detailed view with full command paths
ps aux --sort=-%cpu | head -20
# Check for processes running from deleted binaries (a classic hiding technique)
ls -la /proc/*/exe 2>/dev/null | grep deleted
Miners often masquerade as system processes: kworker, kthreadd, sshd, or random-looking strings. Pay close attention to the deleted binary check — if /proc/<PID>/exe points to a (deleted) path, that’s nearly always malicious. The attacker executes the binary, then immediately deletes it from disk, leaving only the running process behind.
Step 2: Trace Outbound Network Connections
# List all connections with the responsible process
ss -tulpn
# XMRig commonly connects to mining pools on ports 3333, 4444, 5555, 14444
ss -tnp | grep -E ':3333|:4444|:5555|:14444'
# Resolve IPs of suspicious connections
for ip in $(ss -tn | grep ESTAB | awk '{print $5}' | cut -d: -f1 | sort -u); do
host "$ip" 2>/dev/null | head -1
done
Step 3: Audit Every Persistence Mechanism
Persistence is where miners show their craft. They plant themselves in multiple locations so that killing the process just triggers a restart from somewhere else.
# Check all crontabs, including per-user
crontab -l 2>/dev/null
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/
for user in $(cut -d: -f1 /etc/passwd); do
crontab -u "$user" -l 2>/dev/null && echo "--- $user ---"
done
# Check systemd services for unfamiliar names
systemctl list-units --type=service --state=running
ls -la /etc/systemd/system/
# Check shell startup files for injected downloaders
find /root /home -name '.bashrc' -o -name '.profile' -o -name '.bash_profile' \
| xargs grep -l 'curl\|wget\|chmod' 2>/dev/null
Step 4: Find the Miner Files
# Search writable locations miners love
ls -la /tmp/ /dev/shm/ /var/tmp/
find /tmp /dev/shm /var/tmp -type f -newer /etc/passwd 2>/dev/null
# Search for XMRig config files
find / -name 'config.json' -newer /etc/passwd 2>/dev/null \
| xargs grep -l 'pool\|wallet' 2>/dev/null
# Check PHP web shells in web root
find /var/www -name '*.php' -newer /etc/passwd 2>/dev/null \
| xargs grep -l 'base64_decode\|eval\|system(' 2>/dev/null
Solutions Compared: Three Ways to Handle a Compromised Server
With the infection mapped, you face a genuine fork in the road. Each option carries a different risk profile — pick the wrong one and you either waste a day or leave the attacker a way back in.
Option A: Live Cleanup (Fast, Higher Risk)
Kill processes, remove files, and purge persistence in-place. Works when the infection is straightforward and you can account for every piece of it. The real risk: miss one persistence mechanism and it’s back the next morning, as if nothing happened.
Option B: Full Server Rebuild (Thorough, Slower)
Provision a fresh server, restore from a known-good backup, or redeploy your application stack from scratch. This is the only path that guarantees a clean state. If the server holds sensitive data or the attack vector isn’t completely clear, this is the right call — full stop.
Option C: Snapshot Rollback
On cloud providers (AWS, GCP, DigitalOcean), roll back to a pre-infection snapshot. Fast and clean — but you need snapshots enabled, and you need to know exactly when the infection started. That timestamp is rarely obvious.
My recommendation: for production servers where the attack vector is unclear, rebuild (Option B). For dev or staging environments where you can trace exactly how the miner got in, live cleanup is acceptable — but verify thoroughly before moving on.
Best Approach: Structured Cleanup and Hardening
Phase 1: Contain — Cut Off the Miner Immediately
# Block outbound connections to common mining pool ports
iptables -A OUTPUT -p tcp --dport 3333 -j DROP
iptables -A OUTPUT -p tcp --dport 4444 -j DROP
iptables -A OUTPUT -p tcp --dport 5555 -j DROP
iptables -A OUTPUT -p tcp --dport 14444 -j DROP
# Block known mining pool domains at the DNS level
cat >> /etc/hosts << 'EOF'
0.0.0.0 pool.minexmr.com
0.0.0.0 xmr.pool.minergate.com
0.0.0.0 xmrpool.eu
0.0.0.0 monerohash.com
EOF
Phase 2: Eradicate — Remove Every Piece
# Kill the miner process
kill -9 <PID>
# Remove systemd-based persistence
systemctl stop <malicious-service>
systemctl disable <malicious-service>
rm /etc/systemd/system/<malicious-service>.service
systemctl daemon-reload
# Clean common miner drop locations
rm -rf /tmp/xmrig /dev/shm/.x /var/tmp/.update*
# Remove malicious crontab entries (edit and delete suspicious lines)
crontab -e
Phase 3: Verify Clean
# Confirm CPU is back to normal
top -b -n 3 | grep '%Cpu'
# Verify no suspicious outbound connections remain
watch -n 2 'ss -tnp | grep -v "127.0.0\|::1"'
# Run rootkit checks
apt install -y chkrootkit rkhunter
chkrootkit
rkhunter --check --sk
Phase 4: Harden Against Reinfection
# Disable SSH password authentication
# This regex handles both commented and uncommented PasswordAuthentication lines
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd
# Rotate all SSH authorized keys — revoke any you don't recognize
cat ~/.ssh/authorized_keys # Audit this carefully
# Install and configure fail2ban for SSH
apt install -y fail2ban
cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
maxretry = 3
bantime = 3600
findtime = 600
EOF
systemctl enable --now fail2ban
# Enable automatic security updates
apt install -y unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades
# Restrict execution from world-writable directories
# remount is temporary — add /etc/fstab entries to survive reboot
mount -o remount,noexec /tmp
mount -o remount,noexec /dev/shm
# Add to /etc/fstab for persistence:
# tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0
# tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0
Phase 5: Set Up CPU Anomaly Alerting
# Alert when CPU exceeds 80% — vmstat gives reliable output across distros
cat > /usr/local/bin/cpu-monitor.sh << 'EOF'
#!/bin/bash
THRESHOLD=80
# vmstat column 15 is idle%; subtract from 100 to get total busy%
CPU=$(vmstat 1 2 | tail -1 | awk '{print 100 - $15}' | cut -d'.' -f1)
if [ "$CPU" -gt "$THRESHOLD" ]; then
echo "HIGH CPU ALERT: ${CPU}% on $(hostname) at $(date)" \
| mail -s "CPU Alert" [email protected]
fi
EOF
chmod +x /usr/local/bin/cpu-monitor.sh
echo "*/5 * * * * root /usr/local/bin/cpu-monitor.sh" > /etc/cron.d/cpu-monitor
# Set up auditd to catch execution from /tmp
apt install -y auditd
auditctl -w /tmp -p x -k tmp-execution
auditctl -w /etc/cron.d/ -p wa -k crontab-changes
# Review audit logs
ausearch -k tmp-execution
ausearch -k crontab-changes
One pattern repeats in every reinfection case I’ve handled: the admin cleaned the miner but never identified the original entry point. Patch the hole first. If you can’t determine how the attacker got in, the only path forward is a full rebuild — treat the old box as fully compromised and don’t bring it back.
Detection speed is what separates a minor incident from a serious one. A miner caught in the first hour costs almost nothing. Three weeks of undetected mining means real money lost, potential data exposure, and yes — a very long weekend.

