A WordPress reinstall does not clean a hacked site
A WordPress reinstall does not clean a hacked site
The site had been compromised, so we did the thing everyone does.
Full core reinstall. Every plugin deleted and pulled fresh from wordpress.org. Clean files, verified against the official checksums, straight from the source.
The next morning there was a new administrator account.
The reinstall theatre
Reinstalling WordPress feels like a cure because it is thorough and it is visible. You watch hundreds of files get replaced. Something that comprehensive must have worked.
It did not, and the reason is simple once you say it out loud. A reinstall replaces the files a reinstall covers. That is all it does. It is not a disinfectant. It is a file copy.
So the useful question is not “did I reinstall everything.” It is “what does a reinstall never touch.”
What a reinstall never touches
Four places, and an attacker only needs one.
Paid plugins. This was the hole in our case. The site ran commercial
plugins, and commercial plugins do not exist on wordpress.org. There is no
canonical copy to reinstall from and no checksum to verify against. We could
confirm the integrity of every free plugin on the site and say nothing at all
about the paid ones. wp plugin verify-checksums simply has no opinion on code
it has never seen.
The uploads directory. Nobody reinstalls wp-content/uploads, because it is
the customer’s data. A PHP file sitting in there survives everything. It should
never exist, and it is trivial to check for, and almost nobody checks.
mu-plugins. Anything in wp-content/mu-plugins loads automatically on every
request and does not appear in the plugins list in the admin UI. You cannot
deactivate what you cannot see. A reinstall of the plugins directory does not
go near it.
The database. Injected options, malicious cron hooks, and rogue user rows live in MySQL. You can replace every PHP file on the server and the database sits there untouched, waiting.
The recreating admin is the tell
The detail that mattered most was that the admin account kept coming back. Not that it existed, that it returned after deletion.
An account you delete once and never see again is a symptom. An account that reappears is a machine. Something on that server was still executing, and it was watching for its own absence.
The pattern is small enough to hide almost anywhere: check whether a user exists, and if it does not, create it. Drop that in a file that runs on every request and you have persistence that survives password changes, plugin removal, and a full core reinstall. You are not fighting a file at that point. You are fighting a process that rebuilds the file.
Once you understand that, the cleanup order inverts. Deleting the rogue admin first is the least useful thing you can do. Find what is recreating it, kill that, and then delete the account.
The Find
After careful evaluation of all the plugins and files - it was found that there was a plugin folder that otherwise wouldn’t show up on WP CLI that did have the payload to download the script to the mu plugin area and thereby creating the admin account.
The user was using the file manager plugin as well which seems to have opened the way for this injection - once the plugin was removed and we did run another cleanup operation
Finally all the non-required admin logins were removed and the 2 factor authentication was enabled on all admin accounts.
Why this needed to become a tool
The part that stayed with me was not this one site. It was realising the process did not scale.
When several sites go down at once, you cannot afford to reason your way through each one from scratch. You need a fixed set of rules that runs everywhere and tells you where to look, before you have finished reading the first ticket.
That is a different tool from a vulnerability scanner. The public scanners look at a site from the outside and tell you what is exposed. What I needed ran on the box, with filesystem and database access, and answered one question: has this already been compromised, and where is the payload hiding.
There is a catch, and it is the reason most scanners get abandoned. If the tool alarms on things that turn out to be fine, you stop trusting it, and an alarm you have learned to ignore is worse than no alarm. Checksum verification is the perfect example. Run it across a fleet with commercial plugins and every single site lights up, every one of those alerts is meaningless, and within a week nobody reads the report.
So the rule became: a finding should almost always mean something. Not
eval on its own, which appears in legitimate code. eval(base64_decode( as a
pair. Not every file in the uploads directory, only executable ones, which have
no business being there at all. Skip wp-admin and wp-includes entirely, and
skip the security plugins too, because Wordfence and Sucuri ship malware
signatures on purpose and a naive scanner will flag them every time.
That set of rules turned into Bulwark.
The short version
If you take one thing from this: a reinstall proves your files match the originals. It proves nothing about the files that have no original, the data directory, the plugins that load invisibly, or the database.
And if something keeps coming back after you delete it, stop deleting it. Go find the thing doing the recreating.