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