The 15-Minute WordPress VPS Security Check

The quick version I wish I had before the incident

By SepedaTua — CrushEdge.com


If you don’t have time to read the entire incident report, that’s fine.

I understand.

Sometimes you’re looking at a server at 2 AM, coffee is getting cold, Apache is stopped, and the only thing you want to know is:

“Is this server actually clean?”

You probably won’t be able to prove that in 15 minutes.

But you can find a lot of bad signs.

Here is my quick checklist.


1. Check what is listening

ss -lntup

Write down every public-facing service.

If you see something you don’t recognize, investigate it.


2. Check CPU

ps -eo pid,user,%cpu,%mem,etime,args \
--sort=-%cpu |
head -20

A compromised WordPress site doesn’t necessarily use high CPU.

But if something is burning 100% CPU and you don’t know what it is, that’s worth investigating.


3. Find recently modified files

Change the date to your suspected compromise window:

find /home \
-type f \
-newermt '2026-08-20 00:00:00 UTC' \
-printf '%TY-%Tm-%Td %TH:%TM:%TS | %u:%g | %s | %p\n' \
2>/dev/null |
sort

This is one of the fastest ways to reduce a huge filesystem into a manageable list.


4. Find PHP files

find /home \
-type f \
\( -name '*.php' -o -name '*.phtml' -o -name '*.php5' -o -name '*.php7' \) \
-print \
2>/dev/null

Now look for PHP files in strange places.

Examples:

uploads/
images/
cache/
tmp/
wp-includes/images/
wp-admin/images/

Not every one is malicious.

But unexpected PHP deserves attention.


5. Look for obvious obfuscation

grep -RInE \
'base64_decode\s*\(|gzinflate\s*\(|gzdecode\s*\(|str_rot13\s*\(|eval\s*\(|assert\s*\(' \
/home \
--include='*.php' \
--include='*.phtml' \
--include='*.php5' \
--include='*.php7' \
2>/dev/null

Don’t panic when this returns something.

Some legitimate software uses encoding.

Inspect the file.

Context matters.


6. Search for command execution

grep -RInE \
'shell_exec\s*\(|passthru\s*\(|proc_open\s*\(|popen\s*\(|system\s*\(|exec\s*\(' \
/home \
--include='*.php' \
--include='*.phtml' \
--include='*.php5' \
--include='*.php7' \
2>/dev/null

Again:

Finding the function isn’t proof of compromise.

Finding something like:

eval(base64_decode(...));

inside a randomly named PHP file is a very different story.


7. Scan images that aren’t actually images

This is worth putting on a sticky note.

find /home -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \
   -o -iname '*.gif' -o -iname '*.webp' -o -iname '*.ico' \
   -o -iname '*.bmp' -o -iname '*.avif' \) \
-print0 2>/dev/null |
while IFS= read -r -d '' f; do
    t=$(file -b "$f")

    case "$t" in
        *"PHP script"*|*"HTML document"*|*"ASCII text"*|*"Unicode text"*)
            printf '%s | %s\n' "$f" "$t"
            ;;
    esac
done

The filename says:

logo.png

The content might say:

PHP script

The content wins.


8. Check WordPress uploads

find /home -path '*/wp-content/uploads/*' \
-type f \
\( -name '*.php' -o -name '*.phtml' -o -name '*.php5' -o -name '*.php7' \) \
-print \
2>/dev/null

Unexpected results deserve immediate investigation.


9. Check for strange executable files

find /home \
-type f \
-executable \
-printf '%m %u:%g %s %p\n' \
2>/dev/null

Pay particular attention to executable files inside:

public_html
uploads
images
cache
tmp

10. Check cron

crontab -l

Then:

grep -RInE \
'curl|wget|php|python|perl|bash|sh|nc|socat' \
/etc/cron* /var/spool/cron \
2>/dev/null

A malicious cron job can survive your cleanup if you only inspect WordPress.


11. Check SSH keys

find /home /root \
-path '*/.ssh/authorized_keys' \
-type f \
-print \
2>/dev/null

Then inspect them.

You should recognize every key.

If you don’t:

stop and investigate before deleting it.


12. Check MySQL accounts

SELECT User, Host
FROM mysql.user
ORDER BY User, Host;

Then:

SHOW GRANTS FOR 'username'@'host';

Look especially for:

%

and users with:

GRANT ALL PRIVILEGES ON *.*

Those deserve attention.


13. Check WordPress administrators

If WP-CLI is installed:

wp user list \
--role=administrator \
--path=/home/customer/public_html

You should recognize every administrator.

If you find:

admin
administrator2
wordpress_admin
random-looking-account

don’t immediately assume it’s malicious.

Check when it was created and who owns the site.


14. Check the database exposure

ss -lntp | grep ':3306'

If you see:

0.0.0.0:3306

ask yourself why.

For most WordPress installations, MariaDB does not need to be directly accessible from the Internet.


15. Check the logs

Start with the suspected time window.

For example:

grep '20/Aug/2026:03:' \
/var/log/virtualmin/example.com_access_log

Then search for:

grep -Ei \
'POST|upload|wp-admin|wp-login|admin-ajax|xmlrpc|plugin|install|file'

But remember something important from my own investigation:

absence of a log entry doesn’t prove absence of an attack.

Logs can rotate.

The wrong log can be inspected.

The relevant request might have gone through another service.

Or the attacker may have used an already-compromised credential.


The three commands I’d remember

If you only remember three commands from this entire series, make them these.

What is this file?

file /path/to/file

When did it change?

stat /path/to/file

What does it contain?

sed -n '1,240p' /path/to/file

Those three commands solved a surprising amount of my investigation.


And one rule I would never break again

Don’t trust the extension.

Not:

.php = dangerous
.png = safe
.jpg = safe
.gif = safe

Instead:

extension
   ↓
interesting
   ↓
file type
   ↓
content
   ↓
context
   ↓
decision

That’s much closer to how you should think about suspicious files.


If you find something bad

Don’t immediately run:

rm -rf

First:

mkdir -p /root/incident-evidence

cp -a /path/to/file \
/root/incident-evidence/

sha256sum \
/root/incident-evidence/file

Then decide whether you’re:

  • investigating,
  • quarantining,
  • cleaning,
  • or completely restoring the site.

Those are different jobs.


If you already know the server is compromised

Then don’t spend three days trying to make yourself feel better by deleting suspicious files one at a time.

Ask:

Do I have a known-good backup?

If yes:

known-good backup
        ↓
wipe affected environment
        ↓
restore
        ↓
rotate credentials
        ↓
harden
        ↓
scan
        ↓
monitor

That’s usually a much cleaner recovery path.


The checklist I actually want to keep

WORDPRESS VPS INCIDENT CHECKLIST

[ ] Record date/time
[ ] Preserve important evidence
[ ] Check running processes
[ ] Check listening ports
[ ] Check cron
[ ] Check systemd
[ ] Check SSH authorized_keys
[ ] Check recent filesystem changes
[ ] Search suspicious PHP
[ ] Check PHP in uploads
[ ] Check disguised images
[ ] Check executable files
[ ] Check WordPress administrators
[ ] Check plugins/themes
[ ] Check MySQL users
[ ] Check remote MySQL access
[ ] Identify known-good backup
[ ] Restore if appropriate
[ ] Rotate credentials
[ ] Restrict firewall
[ ] Restrict database access
[ ] Scan restored files
[ ] Monitor logs
[ ] Monitor CPU/network
[ ] Create a clean baseline
[ ] Test backups

Print that.

Put it somewhere boring.

Hopefully you’ll never need it.

But if you do, you’ll be very happy that you didn’t have to remember all of this at 3 AM.


Final thought

I started this whole exercise thinking the interesting part would be the malware.

It wasn’t.

The interesting part was learning how many small pieces have to line up for a compromise to become a serious incident:

vulnerable application
        +
valid credentials
        +
exposed service
        +
weak isolation
        +
persistence
        +
insufficient monitoring
        =
much bigger problem

You don’t necessarily need to eliminate every risk.

You need to break that chain wherever you can.

If the attacker gets through one door, make sure there isn’t another ten doors sitting behind it unlocked.

That’s probably the most practical security lesson I took from this entire mess.

And now, hopefully, I can get back to normal work.

I’ve got websites to maintain, code to write, four kids at home, and a Jeep that always seems to think it needs another repair.

The server can wait.

The kids probably won’t.

No Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.