The .hph extension trick: how WordPress malware survives cleanups by shadowing .php files

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:

  1. The attacker drops a backdoor at wp-content/themes/twentytwenty/inc/loader.php. They also drop a byte-identical copy at loader.hph in the same directory.
  2. You scan for *.php, find loader.php, quarantine it. The loader.hph copy is invisible to your scanner because it doesn’t have the right extension.
  3. The attacker either (a) re-renames loader.hph back to loader.php via a separate webshell that you didn’t find, or (b) drops a one-line .htaccess that maps .hph to 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 .phtml as PHP.
  • .phps — usually configured to display PHP source code highlighted; rarely a direct execution vector but often skipped by malware scanners.
  • .inc and .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 .htaccess that does SetHandler application/x-httpd-php for 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/null

If 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.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.