You clean a WordPress malware infection. You find every .php file with the suspicious signature, quarantine it, restore from backup, harden the site. Three weeks later the same backdoor is back. Same filename, same content, same behavior. You’re sure you got every .php file the first time. So how did it come back?
Look for the .hph files. Or .phtml. Or .php5, .phar, .pht, or .php8. Attackers shadow their .php backdoors with copies under non-standard extensions specifically because most malware-cleanup workflows scan for *.php and miss everything else. Those shadow copies don’t execute on their own (the web server only runs .php by default), but they sit on disk untouched through your cleanup, and a single line in .htaccess brings them back to life.
How the trick works
The mechanism is simple and depends on three things being true:
- The attacker drops a backdoor at
wp-content/themes/twentytwenty/inc/loader.php. They also drop a byte-identical copy atloader.hphin the same directory. - You scan for
*.php, findloader.php, quarantine it. Theloader.hphcopy is invisible to your scanner because it doesn’t have the right extension. - The attacker either (a) re-renames
loader.hphback toloader.phpvia a separate webshell that you didn’t find, or (b) drops a one-line.htaccessthat maps.hphto PHP execution:AddHandler application/x-httpd-php .hph. Either way, the file works again.
I’ve seen the same file shadowed in three or four different non-standard extensions on a single compromised site — the attacker’s defense in depth against your cleanup. Some of the extensions I’ve found in the wild on real WordPress compromises:
.hph— the most common. Single-character typo of.php, looks innocuous in a directory listing..phtml,.pht,.phar,.php3,.php4,.php5,.php7,.php8— all of these are real PHP-handler-mapped extensions on various server configurations. Some Apache/LiteSpeed defaults still execute.phtmlas PHP..phps— usually configured to display PHP source code highlighted; rarely a direct execution vector but often skipped by malware scanners..incand.module— common shadow extensions for Drupal-derived patterns; sometimes mapped to PHP execution by misconfigured includes..png/.jpg/.gif— image files containing PHP, used in tandem with an.htaccessthat doesSetHandler application/x-httpd-phpfor specific filenames. Even uglier because file extensions look completely innocent.
The detection sweep
Every cleanup, including yours from a few weeks ago, should run this. It’s three commands and takes seconds:
# 1. Find every non-standard PHP-ish extension in your sites
find /var/www -type f \
\( -name "*.hph" -o -name "*.phtml" -o -name "*.phar" \
-o -name "*.pht" -o -name "*.php3" -o -name "*.php4" \
-o -name "*.php5" -o -name "*.php7" -o -name "*.php8" \
-o -name "*.phps" \) \
2>/dev/null
# 2. Find any .htaccess that adds a non-standard PHP handler
grep -rlE "AddHandler|SetHandler|AddType.*x-httpd-php" \
--include=".htaccess" /var/www 2>/dev/null
# 3. Find image-extension files that contain <?php — the polyglot trick
find /var/www -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.gif" \) \
-exec grep -lE "<\\?php" {} + 2>/dev/nullIf the first command returns anything in wp-content/themes/ or wp-content/plugins/, look at the file. WordPress core and legitimate plugins almost never use these extensions. Anything that shows up here is suspicious by default.
If the second command returns a .htaccess in a path that isn’t WordPress core, that .htaccess probably exists specifically to enable a shadow extension. Read it. If it has AddHandler ... .hph or SetHandler application/x-httpd-php for an unusual filename, that’s malware infrastructure.
The third command catches the polyglot trick — a file named logo.png that’s actually PHP wrapped in image bytes. Real images don’t contain <?php. Anything that does is a webshell wearing a costume.
Cleanup
Same approach as for normal PHP backdoors: quarantine, don’t delete. mv them to a quarantine directory outside of any web-served path, set the parent directory to mode 700 owned by root, then audit the contents at your leisure. The quarantined copy lets you compare hashes if a similar variant turns up later — useful for confirming whether a future incident is the same campaign or a different one.
Also drop the malicious .htaccess entries. If the entry is inside a legitimate-looking .htaccess that has other rules in it, edit out only the offending lines — but in most cases the malicious .htaccess is in a directory that has no business having an .htaccess at all (e.g. wp-content/uploads/2018/03/), and the entire file can go.
Building the sweep into your monthly maintenance
Add the three commands above to a monthly cron, output anything found into an email or a Slack/Discord webhook. On a clean site they should output nothing every month forever. The day they output something is the day you have a problem you didn’t know about.
Most malware scanners — including the free tier of Wordfence — don’t scan .hph by default. Adding it to your custom scan rules takes thirty seconds. The fact that it’s not on by default everywhere is exactly why attackers keep using the trick.
