The Four-File Backdoor
A malware infection across 67 WordPress sites survived two complete cleanups. Every scan came back clean. Four days later it was back. Here is what was actually hiding, and why almost every remediation misses it.
Two abuse notices and a suspension clock
The account held 67 WordPress installations — client sites, product prototypes, a few dormant projects — all running as a single Linux user on one shared plan.
That last detail is the whole problem. When 67 sites share one system user, every site can write to every other site’s files. There is no boundary to breach. Malware that reaches any one install has, by construction, reached all of them.
The host’s abuse team had issued a malware notice naming one domain, with a 24-hour window before the entire hosting plan would be suspended. Not the reported site. The plan. All 67.
Five hours later, a second notice arrived naming a different domain.
Clean, then not clean
This was not the first attempt at removal. It was the third.
A single line in a config file
PHP has a directive called auto_prepend_file. It names a file to execute before every single PHP request — before the application starts, before any of its own code runs.
The infection had written that directive into four files on every affected site, in two different formats, pointing at two different payloads.
# ... 54 lines of ordinary caching rules above ...
php_value auto_prepend_file "/home/xxxxxxxxxx/public_html/wp-content/517b3d40.php"public_html/.user.iniPrepend directive — payload A · found and removed by earlier cleanupswp-content/.user.iniPrepend directive — payload A · found and removed by earlier cleanupspublic_html/.htaccessPrepend directive — payload B · never openedwp-content/.htaccessPrepend directive — payload B · never openedBoth previous cleanups had found and removed the .user.ini pair and payload A. Neither had ever opened .htaccess.
So after each “successful” cleanup, two files per site still carried a live instruction telling the server to execute malware on every page load. The payload it pointed to had been deleted — but the loader was intact and patient. The moment anything restored a file at that path, execution resumed. That is not reinfection. That is an infection that was never fully removed, quietly rebuilding itself.
The reason this evades cleanup is structural, not clever. WordPress security plugins scan WordPress. They read themes, plugins, uploads and core files. A prepend directive lives outside all of that — it is web server configuration, executed before WordPress exists in memory.
It also defeats the standard containment move. Putting a site into maintenance mode stops WordPress from loading. It does nothing whatsoever to a directive that runs before WordPress loads.
Built to survive a human reading the list
The payload itself was installed as a WordPress must-use plugin — a category that loads automatically and cannot be deactivated from the admin screen. It carried a complete, plausible plugin header:
/** * Plugin Name: Grid Importer Vox * Plugin URI: https://github.com/[plausible-author]/grid-importer-vox * Description: Modular content delivery layer * Version: 1.1.4 * Author: [plausible-name] * License: GPL-2.0-or-later */
An earlier variant on the same account had called itself “Keen Booster Mod — Essential media handling engine”. The names rotate. Anyone scrolling a plugin list looking for something suspicious would slide straight past both.
Three further details made detection harder. File modification dates were forged, so the usual “what changed recently” search returned nothing useful. The loader verified its payload’s checksum before running it and swallowed every error, so nothing appeared in any log. And the host’s own scanner had truncated hundreds of malicious files to zero bytes rather than deleting them — leaving the infection’s file structure intact and its loaders pointing at empty husks.
What actually worked
Three techniques did the real work, and one deliberate constraint kept the process safe.
Read the timestamp that cannot be forged
Malware routinely rewrites a file’s modification time to blend into a directory. It cannot rewrite the change time — that requires root, which a compromised website user does not have. Sorting the account by change time cut straight through the forged dates and produced an accurate history of every file the infection had written.
Diagnose at the layer where it breaks
Several sites returned a server error with zero bytes of output. The instinct is to assume WordPress is broken. But a two-line PHP file returned the same error while a static stylesheet loaded perfectly — which proves PHP itself is failing before any application code runs. That single test is what pointed at server configuration rather than WordPress, and it is what found the .htaccess directive.
Remove by name, never by pattern
Automated classifiers are seductive at this scale and they are not reliable enough to trust. A rule intended to catch obfuscated code will also match WordPress’s own HTML parser, which legitimately uses the same construct. A file-integrity check flags every genuine file newer than the manifest it compares against.
So the final pass used no pattern matching at all. Every file was enumerated first and inspected, then removed from an explicit list of paths. Each site was tested over HTTP before and after its own changes, and any site that came back worse had its files restored automatically, in the same run, before the process moved on.
Nothing was ever deleted. Everything was moved to a timestamped quarantine with a full manifest, so any step could be reversed exactly. During remediation an over-broad rule did misfire — and it was that safety net, not a later discovery, that caught and reverted it within the same session.
How the keys got out
Cleaning the infection is only half of it. The other half is closing whatever let it in.
Sitting in the public web roots were 17 backup archives totalling roughly 25GB — xxxxxxxxx.zip, “All Files.zip”, “old files.zip” — every one downloadable by anyone who guessed the filename, and the filenames were guessable.
Each archive contained a wp-config.php. That file holds the database credentials, which matters, and the WordPress authentication salts, which matters far more. With the salts, an attacker can forge a valid administrator session cookie without ever knowing a password. Changing the admin password does nothing. Only regenerating the salts invalidates it.
The archives were moved out of the web roots and the salts regenerated across every live install — which invalidates every existing session, including any that had been forged.
One check made the remaining work far smaller: every site’s database host was a loopback address, meaning the database was never reachable from outside the server. The leaked database credentials were therefore not directly exploitable, and 67 database password rotations dropped from urgent to routine housekeeping.
Where it finished
| Artefact removed | Before | After |
|---|---|---|
Prepend directives in .user.ini | 66 | 0 |
Prepend directives in .htaccess | 68 | 0 |
| Injector plugin copies | 87 | 0 |
| Malicious database drop-ins | 111 | 0 |
| Files carrying infection markers | 172 | 0 |
| Hidden payload & state files | 529 | 0 |
| Credential-bearing archives in web roots | 17 | 0 |
Every site was then verified from outside the server — status code, page size, page title, and the served HTML checked for spam content and infection markers. A status code alone is not evidence a page is clean; one site on this account had been returning a perfectly healthy 200 while serving a gambling site, because its domain had been repointed at different nameservers entirely.
Four things that carry over to every WordPress engagement
A clean scan is not a clean account
Every scanner has a blind spot, and the persistence lives in it. If an infection returns, the question is not who broke in again — it is what the last cleanup could not see.
Never store backups in the web root
A site archive contains your database credentials and your authentication salts. In a public folder with a guessable name, it is a permanent open door that no malware removal will ever close.
Shared users make one breach total
Sites sharing a single system account cannot be cleaned independently, because they re-infect each other faster than they can be fixed. Separate accounts turn an account-wide incident into a single-site one.
Rotate salts, not just passwords
If a config file has leaked, an attacker may hold a key that forges logins outright. Password changes do not revoke it. Regenerating salts does, immediately.
Client identifiers, domains and infrastructure details removed at the client’s request. Figures are the actual counts from the engagement.
Is your site doing this to you right now?
If you have cleaned a WordPress infection and it came back, the persistence is still there. We hunt it for a fixed price, we quarantine instead of deleting, and you get a written before/after report.