Part 2 — The Files Were Pretending to Be WordPress

Part 2 of a real-world WordPress VPS compromise investigation

By SepedaTua — CrushEdge.com


In Part 1, I stopped the server and started looking at the filesystem instead of immediately trying to get the websites back online.

The first confirmed webshell was bf6f03.php.

Then I found something more interesting.

The attacker had apparently decided that naming a file shell.php was too obvious.

So they started giving the files names that looked like they belonged to WordPress.

That turned out to be a much better clue than the filenames themselves.


wp- does not mean WordPress

If you’ve managed WordPress for a while, you get used to seeing files like:

wp-config.php
wp-login.php
wp-settings.php
wp-load.php

So when you see:

wp-helper.php
wp-loader.php
wp-env-setup.php

your brain can easily categorize them as “probably WordPress.”

Mine almost did.

Then there was this one:

wp-includes/rest-api/class-wp-rest-advanched.php

The location looked convincing.

The filename even looked convincing.

Except for one small problem:

advanched

That’s not how WordPress names the class.

And when I opened the file, things got considerably worse.


It wasn’t a WordPress REST API class

The file contained functionality for manipulating files on the server.

Among other things, it handled:

  • authentication
  • file uploads
  • chunked uploads
  • file editing
  • renaming
  • chmod
  • deletion

There was even a password embedded in the script:

$password = 'admin';

And the page identified itself as:

Mr.X private File Manager V.2

It also contained an attacker Telegram identifier.

At that point, I wasn’t looking at a modified WordPress core file.

I was looking at a web-based file manager/webshell disguised as WordPress code.

That distinction matters.

A normal WordPress REST API class has a very specific purpose.

This thing could manipulate the filesystem.


The dangerous part wasn’t the password

Seeing:

$password = 'admin';

is obviously bad.

But the password itself wasn’t the most important finding.

The functionality was.

The script could perform operations such as:

upload
write
edit
rename
chmod
delete

That means an attacker who could reach the script potentially had a convenient interface for modifying the website’s filesystem.

And once an attacker can write PHP into a web-accessible directory, things get interesting very quickly.


Then I found wp-env-setup.php

This file was particularly strange.

The filename sounds harmless enough.

The contents were anything but.

Its size was around:

243 KB

and it contained more than:

16,000 lines

of code.

The file began with bytes that looked like image data before PHP appeared.

Then the PHP constructed a very large encoded string piece by piece:

$z = "";

$z .= "UEQ5";
$z .= "d2FI";
$z .= "QUtD";
$z .= "bWxt";
$z .= "SUNo";
...

This isn’t how you normally write WordPress configuration or environment setup code.

The important part came near the end:

$decoded = $c($z);
eval("?>".$decoded);

So the file essentially did:

large encoded payload
        ↓
decode
        ↓
execute

That’s a very useful pattern to remember when hunting compromised PHP.


Don’t just search for eval()

At this point you might be tempted to run:

grep -RIn "eval(" /home

and call it a day.

Don’t.

You’ll get a pile of results.

Some will be legitimate.

Some will be generated code.

Some will be libraries.

Some will be malicious.

The interesting question isn’t:

“Does this file contain eval()?”

It’s:

“Why is this file using eval(), what is it decoding, and where did the file come from?”

Context matters.

In this case, the surrounding behavior made the answer pretty clear.


I preserved the file before removing it

This was one of the more important operational decisions.

Before removing a confirmed malicious file, I kept an evidence copy and recorded its SHA-256 hash.

For example:

cp -a /path/to/wp-env-setup.php \
  /root/evidence/wp-env-setup.php

sha256sum /root/evidence/wp-env-setup.php

Why bother with the hash?

Because later I can say:

“This is the exact file I investigated.”

If the file is accidentally modified later, the hash changes.

It’s a simple way to keep track of evidence without needing a fancy forensic platform.


Then there was wp-helper.php

This one changed the investigation again.

The filename was boring.

The code wasn’t.

The script checked whether a process called:

wp-worker

was running.

It used commands such as:

ps
exec()
shell_exec()

and gathered CPU information using commands including:

nproc
lscpu

It also contained a mining-pool reference:

pool.supportxmr.com:3333

and parameters for a wallet and worker process.

That was enough to move this from:

“Someone installed a webshell.”

to:

“Someone appears to be using the server for cryptomining.”


The PHP wasn’t the miner

This distinction is important.

wp-helper.php wasn’t necessarily doing all the mining itself.

It was acting more like a controller.

Conceptually:

                 wp-helper.php
                       │
             ┌─────────┴─────────┐
             │                   │
       check process        collect CPU info
             │
             ▼
        wp-worker
             │
             ▼
        mining pool

That is a much more useful way to think about malicious PHP.

The PHP script can be the launcher.

The actual payload can be a completely different file.


Then I found wp-worker

This was the point where I stopped thinking of this as a WordPress-only incident.

wp-worker was not another PHP script.

It was a native executable.

The file command identified it as a 64-bit x86-64 ELF executable.

In other words:

PHP
 ↓
shell command
 ↓
Linux executable

Now we’re talking about the underlying operating system.


Why this matters

Suppose an attacker only manages to modify:

wp-content/something.php

That’s already bad.

But suppose that PHP can execute:

shell_exec()

and launch an executable.

Now the boundary has changed.

The attacker isn’t merely modifying WordPress anymore.

They are using WordPress/PHP as a gateway into the Linux environment.

That is why I became much more interested in things such as:

ps aux

and:

find /home -type f

rather than limiting the investigation to WordPress directories.


The miner also explained some of the weird code

Some of the PHP looked unnecessarily complicated.

Then I noticed code collecting information about:

CPU model
number of processors

That suddenly made sense.

If you’re running a cryptominer, CPU information is useful.

The attacker doesn’t care about your fancy WordPress setup.

They care about how much CPU they can steal.

That’s a rather rude way of looking at somebody else’s server.


There was another clue: the mining pool

The code referenced:

pool.supportxmr.com:3333

This was one of the strongest indicators that wp-worker was related to cryptocurrency mining.

It’s also a good example of why strings inside malware can be useful IOC material.

A search such as:

grep -RInE \
'supportxmr|xmrig|wallet|worker' \
/home \
2>/dev/null

can sometimes find related artifacts.

Again, though:

a string match is an indicator, not automatically proof.

A legitimate application could theoretically mention a mining pool.

You need the surrounding context.

Here, we had the surrounding context.


Then I started finding the same tricks elsewhere

Once I knew what I was looking for, the filesystem started making more sense.

There were files with:

  • fake WordPress names
  • suspicious random names
  • image extensions containing PHP
  • deeply nested directories
  • executable-looking payloads
  • encoded PHP
  • shell-command execution

This is why I keep saying that malware hunting isn’t one command.

It’s a process of connecting small clues.

One suspicious filename by itself might mean nothing.

Ten independent indicators pointing in the same direction are a different story.


The image files were particularly clever

Some of the malicious files were hidden among normal-looking WordPress assets.

For example:

wp-includes/images/...
wp-admin/css/...
wp-content/plugins/...
wp-content/themes/...

And some ended with extensions such as:

.png
.gif
.jpg
.ico
.bmp

But file identified them as PHP.

This is a simple trick, but it works surprisingly well against manual inspection.

If I open:

wp-includes/images/logo.png

in a file browser, I probably won’t think twice.

If I run:

file logo.png

and get:

PHP script, ASCII text

the story changes immediately.


Why attackers like WordPress directories

There is another practical reason for this technique.

WordPress contains a huge number of files.

An administrator looking at:

wp-admin/
wp-includes/
wp-content/

doesn’t necessarily know what every file should look like.

An attacker can take advantage of that noise.

A random PHP file in the root directory might stand out.

A random .png several directories deep is easier to overlook.

That’s why checking file type versus file extension is such a useful hunting technique.


Then came the database

At this point I was already convinced that I shouldn’t treat this as a single-site problem.

So I started looking at the database side too.

That produced another interesting set of artifacts.

There were unexpected database tables created around the suspicious period.

Some contained attacker-oriented messages and contact information, including text equivalent to:

CONTACT FOR DATABASE
AND VULNERABILITY

There were also attacker contact identifiers.

That was significant because it gave me another independent evidence source.

I wasn’t relying only on filesystem artifacts anymore.

I now had:

HTTP activity
      +
filesystem artifacts
      +
database artifacts

When several independent sources point toward the same incident window, confidence goes up considerably.


But did the attacker enter through MySQL?

Not necessarily.

This is where I had to resist the temptation to connect every suspicious thing together.

The server also had a MySQL configuration that allowed broad network listening:

bind-address = 0.0.0.0

There were also database accounts permitting connections from external hosts.

That’s dangerous.

But I don’t have enough evidence to say:

“The attacker entered through MySQL.”

So I won’t claim that.

The accurate statement is:

The MySQL configuration created unnecessary attack surface and became one of the things I needed to fix during recovery.

That’s a much more useful conclusion anyway.


The bigger picture was becoming clear

At this point, my mental model of the incident looked something like this:

                    Internet
                       │
                       ▼
                WordPress site
                       │
                       ▼
              Initial compromise
                       │
                       ▼
                PHP webshell
                       │
             ┌─────────┴─────────┐
             ▼                   ▼
       File operations      PHP execution
                                 │
                                 ▼
                          shell commands
                                 │
                                 ▼
                           wp-worker
                                 │
                                 ▼
                          cryptomining

And separately:

              compromised environment
                       │
              ┌────────┴────────┐
              ▼                 ▼
         filesystem          database
              │                 │
              ▼                 ▼
       fake WP files       suspicious tables
       disguised images    attacker messages

The exact relationships between every artifact were not all proven.

But there was more than enough evidence to justify treating the host as compromised.


This is where “just clean WordPress” stopped making sense

I could have spent hours doing this:

delete file
scan
delete another file
scan again
restart PHP
restart Apache
scan again

And maybe I would eventually have removed most of the obvious malware.

But how would I know I didn’t miss something?

That was the problem.

Once an attacker has had enough access to:

  • write PHP
  • execute shell commands
  • deploy an executable
  • modify databases
  • create persistence

I don’t want to spend my night playing whack-a-mole with suspicious files.

I have four kids.

There are better uses for the remaining hours of my day.


The backup became the answer

Fortunately, I had another option.

I had a backup from before the known compromise.

So the recovery strategy became:

Don't trust the current filesystem
             ↓
Use the historical backup
             ↓
Restore the accounts
             ↓
Remove known malicious artifacts
             ↓
Search for the known indicators
             ↓
Fix the MySQL exposure
             ↓
Bring services back carefully

This is where the investigation moved from forensics to recovery.

And there was one more problem I needed to solve before I was comfortable bringing the server back:

MySQL had been deliberately exposed for legitimate remote access.

That part deserves its own article because the original requirement was reasonable—but the way I had implemented it was much broader than it needed to be.


Coming next

Part 3 — I Needed Remote MySQL Access, So I Made It Too Easy

I’ll explain the MySQL configuration I found, why a temporary cloud VPS made normal IP allowlisting inconvenient, why user@'%' is a bad shortcut, and the safer alternatives I should have used instead.

No Comments

Leave a Reply

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