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.