What I would keep on the server owner’s desk after going through all of this
By SepedaTua — CrushEdge.com
After going through this incident, I wanted one thing left behind:
a short playbook I could actually use next time.
Not 50 pages of theory.
Not a giant enterprise security framework.
Just:
“I think my WordPress VPS has been hacked. What do I do now?”
So this is the version I would save.
Phase 1 — Don’t panic and don’t start deleting
The first instinct is usually:
rm suspicious-file.php
Don’t.
Not yet.
If this is a real compromise, those files may be useful evidence.
Instead, record:
date -u
hostname
uptime
Then preserve suspicious files somewhere outside the web root if possible.
For example:
mkdir -p /root/incident-evidence
cp -a /path/to/suspicious.php /root/incident-evidence/
sha256sum /root/incident-evidence/suspicious.php
The SHA-256 hash gives you a simple identity for the evidence.
Phase 2 — Find out what is actually running
Start with:
ps auxww
Then look at the busiest processes:
ps -eo pid,user,%cpu,%mem,etime,args \
--sort=-%cpu |
head -30
If you find an unfamiliar process consuming huge CPU, investigate it.
Don’t immediately kill it if you’re still collecting evidence.
Phase 3 — Check listening services
Run:
ss -lntup
Make a list.
For every listening port ask:
“Why is this here?”
If the answer is:
“I don’t know.”
that’s your next investigation target.
Phase 4 — Check persistence
Cron
crontab -l
Then:
find /var/spool/cron /etc/cron.d \
-type f \
-print \
2>/dev/null
Search:
grep -RInE \
'curl|wget|bash|sh|php|python|perl|nc|socat' \
/var/spool/cron /etc/cron* \
2>/dev/null
systemd
find /etc/systemd/system \
-type f \
-printf '%TY-%Tm-%Td %TH:%TM:%TS | %p\n' \
2>/dev/null |
sort
And:
systemctl list-unit-files --type=service
Phase 5 — Check SSH
Find authorized keys:
find /home /root \
-path '*/.ssh/authorized_keys' \
-type f \
-print \
2>/dev/null
Inspect them.
Then check SSH configuration:
sshd -T | grep -Ei \
'permitrootlogin|passwordauthentication|pubkeyauthentication'
Don’t change SSH configuration while disconnected from the machine.
Keep your existing session open and test a second session first.
Phase 6 — Check the websites
For each website:
find /home/customer01/public_html \
-type f \
-newermt '2026-08-01' \
-printf '%TY-%Tm-%Td %TH:%TM:%TS | %s | %p\n' \
2>/dev/null |
sort
Adjust the date to the suspected incident window.
You’re looking for unusual bursts and files that don’t belong.
Phase 7 — Search PHP
Start with:
grep -RInE \
'base64_decode\s*\(|gzinflate\s*\(|gzdecode\s*\(|str_rot13\s*\(|eval\s*\(|shell_exec\s*\(|passthru\s*\(|proc_open\s*\(|popen\s*\(' \
/home/customer01/public_html \
--include='*.php' \
--include='*.phtml' \
--include='*.php5' \
--include='*.php7' \
2>/dev/null
Then investigate the results.
Don’t blindly delete everything.
Phase 8 — Check fake images
This was one of my favorite checks from the investigation:
find /home/customer01/public_html -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
A result doesn’t automatically mean malware.
But it gives you something worth investigating.
Phase 9 — Check PHP inside uploads
find /home/customer01/public_html/wp-content/uploads \
-type f \
\( -name '*.php' -o -name '*.phtml' -o -name '*.php5' -o -name '*.php7' \) \
-print \
2>/dev/null
Unexpected PHP in uploads is a serious red flag on a typical WordPress installation.
Phase 10 — Check executables
find /home \
-type f \
-executable \
-printf '%m %u:%g %s %p\n' \
2>/dev/null
And specifically look for ELF binaries:
find /home/customer01/public_html \
-type f \
-print0 2>/dev/null |
while IFS= read -r -d '' f; do
t=$(file -b "$f")
case "$t" in
*"ELF"*)
printf '%s | %s\n' "$f" "$t"
;;
esac
done
A random ELF binary inside a WordPress installation deserves immediate attention.
Phase 11 — Check for miners
Search for obvious indicators:
grep -RInE \
'xmrig|stratum\+tcp|stratum\+ssl|cryptonight|randomx|monero|wallet' \
/home \
2>/dev/null
Then inspect suspicious processes:
ps -eo pid,user,%cpu,%mem,etime,args \
--sort=-%cpu |
head -30
And network connections:
ss -tpn
Phase 12 — Check WordPress administrators
If WP-CLI is available:
wp user list \
--role=administrator \
--path=/home/customer01/public_html
Look for accounts that shouldn’t exist.
Also check application passwords and other authentication mechanisms where applicable.
Phase 13 — Check plugins and themes
wp plugin list \
--path=/home/customer01/public_html
wp theme list \
--path=/home/customer01/public_html
Remove software that isn’t needed.
Update software that is needed.
And if a plugin has a history of vulnerabilities, don’t keep it simply because:
“I’ve always used it.”
Phase 14 — Check MySQL
Inside MariaDB/MySQL:
SELECT User, Host
FROM mysql.user
ORDER BY User, Host;
Then inspect remote accounts:
SHOW GRANTS FOR 'some_user'@'%';
Pay special attention to:
%
and administrative privileges.
Phase 15 — Close unnecessary MySQL exposure
Check:
ss -lntp | grep ':3306'
If MariaDB is listening publicly, ask:
“Does it really need to?”
If not, keep it private.
For temporary remote applications, my preferred approach is:
Temporary VPS
│
│ SSH tunnel
▼
Main VPS
│
▼
127.0.0.1:3306
rather than:
Internet
│
▼
0.0.0.0:3306
Phase 16 — Rotate credentials
After a confirmed compromise, I would rotate:
root password
SSH keys
Virtualmin credentials
WordPress administrators
database passwords
API tokens
SMTP credentials
cloud/API credentials
backup credentials
Especially anything that was stored on the compromised server.
Phase 17 — Decide whether to clean or restore
This is the decision point.
If you have:
known-good backup
+
confirmed compromise
I generally prefer:
wipe
↓
restore
↓
verify
↓
harden
rather than:
delete malware #1
↓
search
↓
delete malware #2
↓
search
↓
discover malware #3
↓
repeat forever
Cleaning can be appropriate.
But restoration gives you a much clearer baseline when a trustworthy backup exists.
Phase 18 — Don’t restore blindly
Before selecting a backup, ask:
“When do I know the server was clean?”
If the answer is:
“I’m not sure.”
then compare multiple historical backups.
For example:
August 23
August 22
August 21
August 20
August 19
August 18
August 17
August 16
You may discover that suspicious files first appear between two restore points.
That’s valuable information.
Phase 19 — After restoration, scan again
Don’t assume:
“Backup = clean.”
Run the same checks again.
For example:
find /home/customer01/public_html \
-type f \
-name '*.php' \
-printf '%TY-%Tm-%Td %TH:%TM:%TS | %s | %p\n' \
2>/dev/null |
sort
Then:
find /home/customer01/public_html \
-type f \
-executable \
-print \
2>/dev/null
Then the fake-image scan.
Then the webshell pattern scan.
Then check:
ss -lntup
and:
systemctl list-unit-files --type=service
Phase 20 — Monitor after going live
This is where I would spend the next few hours.
Watch:
tail -f /var/log/...
depending on your distribution and Virtualmin logging setup.
Watch CPU:
top
Watch connections:
ss -tpn
And watch for unexpected filesystem changes.
The first few hours after restoration are important.
The final “normal server” test
Before I consider the incident closed, I want to be able to answer yes to these:
[✓] I know which websites are installed.
[✓] I know which users own them.
[✓] I know which WordPress administrators exist.
[✓] I know which plugins are installed.
[✓] I know which services are running.
[✓] I know which ports are exposed.
[✓] I know which MySQL users exist.
[✓] I know which MySQL users can connect remotely.
[✓] I know which SSH keys are authorized.
[✓] I know which cron jobs exist.
[✓] I know where my backups are.
[✓] I have tested a restore.
If I can’t answer one of those, that’s the next thing I investigate.
And that’s where I would officially close the incident
Not when:
Apache starts
Not when:
WordPress homepage loads
And not when:
malware scanner says "clean"
I’d close it when:
known-good restore
+
credential rotation
+
reduced attack surface
+
verified accounts
+
verified services
+
verified backups
+
monitoring
are all in place.
That’s a much more meaningful definition of “recovered.”
One final note
I originally started this investigation because some files looked wrong.
A few fake images.
A suspicious PHP file.
Some encoded code.
A webshell.
Then the investigation expanded into logs, timestamps, backups, MySQL, persistence, and network exposure.
That’s normal.
A real compromise rarely stays neatly inside one folder.
But you also don’t need to become a professional forensic investigator to respond intelligently.
Start with:
file
find
grep
stat
ps
ss
Add good backups and sensible isolation.
Those boring tools can take you surprisingly far.
And hopefully, the next time I see something like:
something-random.png
I won’t have to spend two sleepless nights wondering whether it is actually an image.
I’ll just run:
file something-random.png
and let the file answer for itself.
No Comments