The backdoor that survived a malware cleanup: 14,000 folders and a fake acme-challenge path
A shared hosting account was reinfected in under 30 seconds, five days after a full cleanup. The way back in was hidden where no scan was looking.

The short version: when a WordPress site is reinfected a few days after a cleanup, one entry point was almost always missed. In this case it was four PHP shells hidden in a tree of about 14,000 folders that imitated a Let’s Encrypt validation path. Pattern-based scans skipped it. Reading the access logs at the minute of the reinfection showed exactly where the attacker came back in.
The setup
One shared hosting account with 16 WordPress installs and around 40 other sites, all running under the same system user. That last detail matters: on a shared account, a shell in any one site can write to every other site. A clean site next to an infected one does not stay clean for long.
Timeline
- October 4 and 5: the attacker plants a folder called
well-known/acme-challenge(no leading dot) inside one site, and fills it with folders namedatoh, four levels deep. More than 9,000 folders in one copy, 4,600 in a second copy underwp-content. Four of them hold PHP shells. - October 7: a large cleanup. 408 web shell folders removed across the account, fake administrators deleted, WordPress core reinstalled. The maze stays, because nothing about it matched what we were looking for.
- October 7, 15:44: new fake administrators appear on three sites, a few hours after the cleanup.
- October 12, 09:37:45: one request from an outside IP to a shell inside the maze.
- 09:38:01 to 09:38:27: the server starts calling freshly written shells on about 50 sites, each one with a
?pass=parameter to confirm it works. The whole account is reinfected in under 30 seconds.
What the attacker left behind
- Chains of three folders with random seven-character names, each holding an obfuscated
index.php, a 220 KBtool.phpand aphp.ini, all set to read-only. - Thirty identical
.htaccessfiles insidewp-contentandmu-pluginsthat explicitly allow PHP to run there. - A web file manager in the root of one site, under a random name.
- That same site’s
wp-login.phprenamed, so the owner could not log in. - A PHP mailer for sending spam from the account.
- Sixteen fake administrator accounts across five sites, and more than 200 gambling spam posts on one of them, published in four languages.
Why the first cleanup missed it
The first scan searched for what we had already seen: the seven-character folder chains, known file names, known code signatures. The maze matched none of those. Its folders had one-letter names and the path looked like something a certificate tool would create. It sat quietly for a week and was used once.
The lesson is to search by behaviour, not by name:
- PHP files in folders that should never contain PHP (uploads, upgrade, anything under a
.well-knownlook-alike). - PHP files modified recently anywhere outside core, plugin and theme folders.
- Access log requests to
.phpfiles outsidewp-admin,wp-includesand the active plugins and themes.
The cleanup order that works on a shared account
- Neutralise everything at once. We moved 340 shell folders and both mazes to a quarantine folder outside the web root in a single pass. Cleaning one site at a time lets the others reinfect it.
- Remove the files that re-enable PHP. The 30 malicious
.htaccessfiles were byte-identical, so comparing hashes found every copy. - Block PHP where it never belongs. In
wp-content/uploadsandwp-content/upgradeof every site:<FilesMatch "\.(php|phtml|php[0-9]|phar)$"> Require all denied </FilesMatch>Test it: a PHP file placed there must be refused while images still load.
- Verify WordPress core against the official checksums from
api.wordpress.org. Here all 16 installs were intact, which told us the attacker worked from outside the core. - Remove fake administrators. Ours had names like
admin_followed by ten hex characters, random hex logins, an[email protected]address and registration dates in 1970. Keep a copy of each record before deleting it. - Rotate every credential the attacker could have read: database passwords and salts on every site, the hosting panel password and FTP accounts.
- Scan again after a few hours of real traffic and cron runs. Reinfection shows up there, not in the scan you run right after cleaning.
Questions we get about this
Why do the shells come back after I delete them?
Because at least one access point is still there: a shell you did not find, an administrator account you did not remove, or a password that leaked. Deleting the visible files without closing the way back in only resets the clock.
Is a security plugin enough?
It helps with known signatures. It does not see a shell that runs from a folder the plugin is not scanning, and it cannot protect a site from another site on the same hosting account.
Do I need to change the database password if nobody logged in?
Yes. A shell can read wp-config.php without logging in to anything. If a shell ran on your account, assume the database credentials were copied.