Part 7 — What I Would Do Differently Next Time

Epilogue to the WordPress VPS compromise investigation

By SepedaTua — CrushEdge.com


After writing all of this down, I realized there was one more useful question.

Not:

“What did the attacker do?”

But:

“What would I do differently if I could go back to the day before the incident?”

That is probably more useful than another list of malware strings.

Because the investigation showed me something uncomfortable:

Most of the important improvements are boring.

There is no single command that magically makes a WordPress VPS secure.

It’s a collection of small decisions.


I would make the blast radius smaller

This is the biggest lesson.

Imagine the server has:

10 WordPress sites
5 applications
MariaDB
Virtualmin
SSH

If one WordPress site gets compromised and PHP can freely access everything available to that account, the attacker has a starting point.

The question becomes:

How much more can they reach from there?

That’s the blast radius.

I want this:

Site A compromised
      │
      └──> Site A damage

Not this:

Site A compromised
      │
      ├──> Site B
      ├──> Site C
      ├──> database
      ├──> SSH credentials
      └──> entire VPS

Separate things that don’t need to trust each other

This applies especially to multi-site VPS hosting.

If several unrelated websites live on the same machine, I want each Virtualmin account to have as little access to the others as possible.

A compromised:

customer01

shouldn’t automatically be able to read:

customer02
customer03
customer04

That’s basic compartmentalization.


The database deserves the same treatment

A database account shouldn’t automatically have access to every database.

For example:

shop_app
    ↓
shop_database

is preferable to:

shop_app
    ↓
ALL DATABASES

If the application only needs:

SELECT
INSERT
UPDATE
DELETE

give it those.

Not:

ALL PRIVILEGES

just because it’s easier.


I would stop treating % as the normal solution

This was one of the clearest lessons from the MySQL side of the incident.

When you need temporary remote access, the easy answer is:

'app'@'%'

But there are better choices.

My preferred order would be:

  1. SSH tunnel
  2. VPN/private network
  3. Temporary source restriction
  4. Direct public MySQL access only when absolutely necessary

The first option is particularly attractive for disposable VPS instances.


Temporary infrastructure should leave no permanent doors

This is a rule I want to remember.

If I create:

temporary-vps-01

for a job that lasts six hours, I don’t want six months of leftovers.

When the job ends:

SSH key
database user
firewall rule
API token
temporary account

should all be considered for removal.

Then destroy the VPS.

Clean slate.


I would also document why each unusual service exists

A surprisingly useful habit is keeping a small inventory.

Something like:

22   SSH             administration
80   HTTP            websites
443  HTTPS           websites
10000 Webmin         Virtualmin management

If six months later I see:

4444

I immediately know:

“That isn’t on my list.”

Without an inventory, you have to remember what you installed months ago.

My memory is good.

It’s not that good.


I would make malware detection less dependent on filenames

This incident made this painfully obvious.

An attacker doesn’t need to call their file:

malware.php

They can call it:

logo.png

So my mental model is now:

filename
   ↓
claim

while:

file
   ↓
actual content

is evidence.

That’s why:

file suspicious.png

is such a useful little command.


I would keep a baseline

This is something I wish I had done earlier.

After a clean restoration, generate inventories.

For example:

find /home \
-type f \
-printf '%u:%g %m %s %p\n' \
2>/dev/null |
sort > /root/files-baseline.txt

And:

ss -lntup > /root/listening-ports-baseline.txt

And:

systemctl list-unit-files --type=service \
> /root/services-baseline.txt

And:

crontab -l > /root/root-crontab-baseline.txt

Now, six months later, you have something to compare against.


A baseline doesn’t need to be fancy

You don’t need a commercial SIEM.

For a small VPS, even:

files-baseline.txt
services-baseline.txt
ports-baseline.txt
cron-baseline.txt
wordpress-plugins.txt
wordpress-users.txt
mysql-users.txt

stored somewhere safe can be extremely useful.

The important word is:

baseline.

You need something known-good to compare with.


Hashes can make the baseline even better

For important files:

sha256sum \
/home/customer01/public_html/wp-login.php \
/home/customer01/public_html/wp-settings.php \
/home/customer01/public_html/wp-config.php

You can save those hashes.

Later:

sha256sum -c baseline.sha256

can tell you whether they changed.

This is particularly useful for WordPress core.


But don’t blindly hash everything forever

You could hash the entire website:

sha256sum every-file

but then WordPress uploads and cache files constantly change.

That creates noise.

Instead, concentrate on things that should be relatively stable:

wp-admin/
wp-includes/
important configuration
custom application code

The goal is useful detection, not a wall of alerts.


I would also separate evidence from cleanup

This was probably the most important operational lesson.

During the incident:

Evidence
   ↓
Preserve
   ↓
Understand
   ↓
Contain
   ↓
Restore

Don’t start with:

rm -rf

unless you’re absolutely sure you no longer need the evidence.

In my case, once I had captured the relevant evidence and decided to restore from backup, the priority changed.

At that point:

clean recovery

became more important than:

preserve every suspicious file

The backup itself became part of the investigation

This was an unexpected benefit.

I initially thought of the August 16 backup as:

“The thing that will get my websites back.”

It turned out to be much more.

It became a historical snapshot.

For example:

Backup
    │
    ├── file exists
    ├── file absent
    └── directory structure

That lets you establish things like:

“This particular suspicious file was not present in the known-good backup.”

That’s much stronger than:

“I don’t remember creating this file.”


I would keep more than one historical backup

If the attacker had been present for several days, the latest backup could already contain malicious files.

That’s why I like having:

daily
weekly
monthly

historical points where practical.

You don’t necessarily need every backup forever.

But you need enough history to go back before an unknown compromise.


One backup is not enough

This deserves repeating.

If you have:

backup-2026-08-23

and the attacker entered:

2026-08-20

your backup may already be contaminated.

But if you have:

backup-2026-08-23
backup-2026-08-16
backup-2026-08-09

you have options.

Historical backups turn an incident from:

“We’re screwed.”

into:

“Let’s determine which restore point is clean.”

That’s a huge difference.


I would monitor changes to web directories

One of the simplest useful alerts would be:

“A new PHP file appeared in an uploads directory.”

That’s much more actionable than:

“Something suspicious happened somewhere.”

For example, a simple periodic check can report:

New PHP files
Modified WordPress core
New executable files
New files pretending to be images

That gives you a much smaller signal-to-noise ratio.


I wouldn’t try to automatically delete malware

This is another lesson from the investigation.

Automatic deletion sounds great until it deletes:

legitimate plugin
custom application
database migration

because some malware scanner thought it looked strange.

I’d rather have:

ALERT:
Suspicious PHP file detected:
 /path/to/file.php

than:

DELETE:
 /path/to/file.php

during normal monitoring.

Contain first.

Understand second.

Delete third.


I would make one emergency checklist

When you’re tired, you don’t want to remember everything.

So I’d keep this somewhere outside the VPS:

WORDPRESS VPS INCIDENT

[ ] Stop unnecessary public services
[ ] Preserve logs
[ ] Preserve suspicious files if evidence is needed
[ ] Record current time
[ ] Record hostname/IP
[ ] Snapshot if possible
[ ] Check running processes
[ ] Check listening ports
[ ] Check cron
[ ] Check systemd
[ ] Check SSH keys
[ ] Check WordPress administrators
[ ] Check database users
[ ] Identify known-good backup
[ ] Restore
[ ] Rotate credentials
[ ] Restrict firewall
[ ] Restrict MySQL
[ ] Verify WordPress
[ ] Monitor logs
[ ] Monitor CPU/network

It doesn’t need to be sophisticated.

It needs to be available when your brain isn’t.


And I would keep the investigation notes

Don’t rely on memory.

During the incident, write:

2026-08-24 01:20
Found suspicious PHP file.

2026-08-24 01:35
Found fake image containing PHP.

2026-08-24 02:10
Confirmed suspicious executable.

2026-08-24 02:45
Selected backup from August 16.

2026-08-24 03:30
Restoration completed.

Even rough notes are incredibly useful.

Three months later you won’t remember why you decided something.

The notes will.


What I learned about “perfect” investigations

I initially wanted certainty.

I wanted to know:

who
when
how
which vulnerability
which request
which command

Sometimes you simply can’t establish all of that.

Logs may be missing.

Timestamps may be misleading.

Credentials may have been reused.

An attacker may have entered through one service and deployed files through another.

The correct response isn’t to invent certainty.

It’s to label evidence honestly.

For example:

Confirmed

This file contained executable PHP code.

Strongly indicated

These files were likely deployed as part of the same compromise.

Possible

The attacker may have used this access path.

Unknown

The original entry point could not be established from the available logs.

That’s good incident reporting.


I also learned not to overreact to Internet noise

A public WordPress server will see garbage.

Constantly.

You’ll see:

/wp-login.php
/xmlrpc.php
/wp-admin/
/.env
/.git/config
/config.php
/phpinfo.php

You may see scanners looking for:

Laravel
Spring
GraphQL
Docker
Jenkins
WordPress
Drupal
Magento

That doesn’t mean you’ve been compromised.

A 404 is often just a 404.

The important question is:

What happened after the probe?

Did the request succeed?

Did a file appear?

Did a new process start?

Did privileges change?

Did the attacker establish persistence?

That’s where the investigation becomes meaningful.


The attacker doesn’t get the final word

This may sound obvious, but after spending hours looking at malicious files, it’s easy to start thinking of the attacker as having complete control.

They don’t necessarily.

A compromise is a sequence of opportunities.

If they get:

web access

but cannot:

execute PHP

that’s useful.

If they execute PHP but cannot:

write outside uploads

that’s useful.

If they get database credentials but:

3306 isn't reachable

that’s useful.

If they compromise one site but:

other Virtualmin accounts are isolated

that’s useful.

Security is often about making every next step harder.


The final architecture I want

If I had to draw the ideal small-business VPS after this incident, it would look roughly like this:

                         INTERNET
                             │
                   ┌─────────┴─────────┐
                   │                   │
                  80                  443
                   │                   │
                   └─────────┬─────────┘
                             │
                           Web
                             │
                         PHP-FPM
                             │
                       WordPress
                             │
                             ▼
                        MariaDB
                        localhost

Administration:

                    ADMIN LAPTOP
                         │
                         │ SSH
                         ▼
                       VPS

Temporary application:

                TEMPORARY CLOUD VPS
                         │
                         │ SSH tunnel
                         ▼
                       MAIN VPS
                         │
                         ▼
                   MariaDB localhost

And backups:

                         VPS
                          │
                          ▼
                  separate storage
                          │
                ┌─────────┴─────────┐
                │                   │
             recent              historical
             backup                backup

It’s not fancy.

That’s the point.


If I had to give one piece of advice

It would be this:

Don’t wait until after a compromise to decide what “normal” looks like.

Know beforehand:

  • which users exist
  • which websites exist
  • which ports are open
  • which services run
  • which database accounts exist
  • which WordPress administrators exist
  • which plugins are installed
  • where PHP is supposed to execute
  • where backups live

Then an incident becomes much easier to recognize.

Instead of:

“Something feels wrong.”

you can say:

“This changed.”

And that’s a much better place to start.


Closing the incident

Eventually, the server stopped being an incident-response project and became a normal server again.

That’s the point.

Not endless forensic investigation.

Not endlessly staring at logs.

Not living in fear of the next POST /xmlrpc.php.

The goal is to:

Understand
   ↓
Recover
   ↓
Harden
   ↓
Monitor
   ↓
Move on

I still have four kids, a wife, work to do, websites to maintain, and probably some car problem waiting for me in the garage.

I can’t spend every night hunting malware.

So the system needs to be designed to make the next incident:

smaller, easier to detect, and easier to recover from.

That’s the real lesson I took away from this one.


The complete series

If you’ve followed the whole investigation, the progression was:

Part 1 — Something Was Wrong
How the first suspicious files appeared.

Part 2 — The Files That Pretended to Be Images
Why .png, .jpg, and .gif files aren’t necessarily images.

Part 3 — The Webshell and the Encoded Payload
How the malicious PHP behaved and why eval(base64_decode(...)) was such a strong indicator.

Part 4 — The Log Lines That Weren’t There
Why the exact initial entry point couldn’t always be proven from Apache logs.

Part 5 — The IOC Checklist I Wish I Had Before the Incident
Practical commands for finding suspicious PHP, fake images, executables, persistence, WordPress accounts, and exposed services.

Part 6 — What I Changed After the Restore
How I approached recovery, MySQL access, SSH tunneling, firewall rules, permissions, backups, and hardening.

Part 7 — What I Would Do Differently Next Time
The lessons that matter more than the malware itself.

And if there is one command from this entire series that I hope you remember, it’s not a malware scanner.

It’s:

file suspicious-file

Because sometimes the file tells you exactly what it is.

Even when the filename is lying.

No Comments

Leave a Reply

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