Another day, another VPS breach
DEV Community

Another day, another VPS breach

I woke up to two emails that immediately caught my attention. One was from my website monitoring service (I use UptimeRobot, no affiliation) reporting that a client's website was down. The other was from my VPS provider informing me that they had suspended my VPS due to abuse.

I logged into the control panel and immediately noticed a massive CPU spike. The server had gone from its usual 15-20% CPU usage to a sustained 100% for nearly four hours before the provider shut it down under their fair usage policy.

My first clue was xmlrpc.php. It was consuming a significant amount of resources, so I started researching it. I'm not primarily a WordPress/PHP developer, and I was surprised to learn that XML-RPC exposes functionality for remote management of WordPress. I disabled XML-RPC, brought the VPS back online, and thought the problem was solved.

It wasn't.

The next day I woke up to the exact same two emails. This time my VPS provider had already imposed CPU limits on the server. I noticed a few kernel-looking processes consuming CPU, assumed they were related to the throttling, and restarted the VPS. A few hours later, it was offline again. At that point I knew I was dealing with a compromise rather than a performance issue.

Initial Discovery

I began investigating the WordPress installation and immediately found obvious signs of infection. There were numerous malicious PHP files (index.php, cache.php, etc.) buried inside recursively nested directories such as:

image/image/image/image/cache.php

The deeper I looked, the worse it became. The attackers had created:

  • A rogue WordPress administrator account
  • An unauthorized SSH key
  • A root-level user on the VPS
  • An administrator account inside CyberPanel

This wasn't just a compromised website anymore. It was a full VPS compromise. My working theory was that the attackers exploited a vulnerable WordPress component (likely allowing arbitrary PHP upload or remote code execution), established persistence, and pivoted into the operating system.

The libnet-cache Process

Then I found a suspicious process called libnet-cache. At first glance it looked like a legitimate kernel process, but it wasn't. Every time I killed it:

kill -9 <pid>

it immediately reappeared. Something else was respawning it.

I started tracing persistence mechanisms. Systemd? Found one malicious service. Cron? Found multiple malicious cron jobs. I tried deleting them. Permission denied. That made no sense. I was root.

After some research I discovered the files had the immutable (i) attribute set. Normally you remove it with:

chattr -i <file>

Except... that also returned permission denied. Even the chattr binary itself had been tampered with. Fortunately its permissions were recoverable, allowing me to restore it and finally remove the immutable attribute.

Cleanup Steps

From there the cleanup became much more methodical:

  • Removed immutable attributes
  • Deleted malicious services
  • Removed malicious cron jobs
  • Deleted unauthorized users
  • Removed rogue SSH keys
  • Reinstalled WordPress core using WP-CLI
  • Deleted every malicious PHP file from wp-content/uploads
  • Rotated credentials
  • Reviewed the rest of the server for persistence

Root Cause

The root cause appeared to be a compromised WordPress site that allowed the attackers to gain remote code execution, escalate their foothold, establish persistence, and eventually deploy a cryptocurrency miner. My VPS had become someone else's mining rig. One compromised website was all it took to lose an entire VPS.

Useful Commands from the Investigation

Here are some of the most useful commands from the investigation, grouped by purpose.

Initial Investigation

top
htop
ps aux --sort=-%cpu | head -20
ps -ef
pstree -p
lsof -p <PID>
ss -tulpn
netstat -tulpn

Finding Suspicious PHP Files

find /home -type f \( -name "*.php" -o -name "*.phtml" \)
find /home/*/public_html/wp-content/uploads \ 
    -type f \( -name "*.php" -o -name "*.phtml" \)
find /home -type d | grep "image/image"
find /home -type f -mtime -7

WordPress Investigation

wp core verify-checksums
wp core download --force
wp plugin list
wp user list
wp option list
wp cron event list

Finding Persistence

systemctl list-units --type=service
systemctl list-unit-files
systemctl status <service>
crontab -l
cat /etc/crontab
ls -la /etc/cron.*
find /var/spool/cron -type f -exec cat {} \;

Finding Unauthorized Users

cat /etc/passwd
last
lastlog
getent passwd

SSH Investigation

cat ~/.ssh/authorized_keys
find /home -name authorized_keys
grep "ssh" /var/log/secure

Process Investigation

ps auxf
readlink -f /proc/<PID>/exe
ls -l /proc/<PID>/exe
strings /proc/<PID>/exe
lsof -p <PID>

Finding Immutable Files

lsattr <file>
lsattr -R /etc
chattr -i <file>

Finding Recently Modified Files

find / -mtime -2
find / -ctime -2
find / -newermt "2026-07-22"

Key Observation

One command that often surprises people during an incident is:

readlink -f /proc/<PID>/exe

It tells you the actual executable backing a running process. Malware frequently disguises itself with names like kworker, kthreadd, libnet-cache, or systemd, but this command reveals the real binary on disk, making it invaluable during Linux incident response.

Comments

No comments yet. Start the discussion.