Skip to content
Security

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.

Long aisle of archive shelves filled with bound ledgers, in black and white

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 named a to h, four levels deep. More than 9,000 folders in one copy, 4,600 in a second copy under wp-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 KB tool.php and a php.ini, all set to read-only.
  • Thirty identical .htaccess files inside wp-content and mu-plugins that 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.php renamed, 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-known look-alike).
  • PHP files modified recently anywhere outside core, plugin and theme folders.
  • Access log requests to .php files outside wp-admin, wp-includes and the active plugins and themes.

The cleanup order that works on a shared account

  1. 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.
  2. Remove the files that re-enable PHP. The 30 malicious .htaccess files were byte-identical, so comparing hashes found every copy.
  3. Block PHP where it never belongs. In wp-content/uploads and wp-content/upgrade of 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.

  4. 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.
  5. 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.
  6. Rotate every credential the attacker could have read: database passwords and salts on every site, the hosting panel password and FTP accounts.
  7. 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.

Something broken
on your site?

Tell us what you are seeing. You get a clear, written answer within one business day.

Brief us