wp-malware-removal · v1.0.0 · 2026-07-08 · sha256 7e653317749704e5

wp-malware-removal v1.0.0B

Immutable. This exact content never changes and is served at /api/v1/blob/7e653317749704e5.

---
name: wp-malware-removal
description: >
  Forensic-grade WordPress malware detection and removal that catches the stealth infections a typical
  scan overlooks. Runs over ANY connected WordPress MCP gateway (SproutOS recommended) or SSH+WP-CLI.
  Detects and cleans across 38 malware families: web-shells and file-managers, auto-login/cookie
  backdoors, database and wp_options injection, malicious redirects, cron backdoors, mu-plugins and
  drop-in abuse, .htaccess / auto_prepend_file persistence, web-root shadow folders, hidden admins,
  rogue Application Passwords, remote-loaded JS skimmers, and SEO-spam cloaking (gambling/pharma). Sees
  the site the way Google and an attacker do — not just as a browser. Whole-server (not just
  wp-content), reads every database table, quarantine-first with live rollback to keep the site up,
  LLM-verified so it does not act on false positives, and names the entry vector. Use when a WordPress
  site is hacked, has a suspected web-shell, injected admins, redirect malware, or gambling/pharma spam
  in Google or link previews — or when the user asks to clean a hacked WordPress site or find how it was
  breached. For a read-only "how hackable am I?" check use wp-security-audit; to lock the doors after,
  use wp-hardening.
license: AGPL-3.0-or-later
metadata:
  author: Aditya Sharma (adityaarsharma.com)
  version: 1.0.0
  homepage: https://github.com/adityaarsharma/wordpress-malware-removal
---

# WordPress Malware Removal (Remove)

You are operating a forensic-grade WordPress malware detection and removal engine. Find as much as
possible — including the stealth infections a typical scan overlooks — verify each finding so you never
act on a false positive, remove it **quarantine-first** so the site stays up, and produce a forensic
report that names the entry vector.

**Core aims:** *Minimize false positives (read before you act). Nothing irreversible without a restore
point. Keep the site up. Catch the stealth infections a typical scan overlooks. Kill self-healing
malware at every reinjection node at once, or it regenerates.*

This is one of three sibling skills — **wp-security-audit** (read-only posture), this **wp-malware-removal**
(clean a hack), **wp-hardening** (close the door). They share the gateway contract in
`references/gateway.md`.

---

## What this catches that in-WordPress scanners miss (the edge)

Commercial scanners run **inside** WordPress and scan `wp-content` as themselves. Real modern attacks
defeat that — in 2023, 58,848 infected sites had a security plugin installed *before* infection, and 14%
of the time the malware tampered with that plugin to hide ([Patchstack 2025](https://patchstack.com/whitepaper/state-of-wordpress-security-in-2025/)):

- **Webroot shadow folders** — malware in physical folders shadowing real URLs (`/docs/`, `/pricing/`)
  in the web root, not `wp-content`. *You scan the whole server.*
- **Tiny single-file backdoors** — a 500-byte auto-login backdoor (`wp_set_auth_cookie` on the first
  admin) has no "web-shell" fingerprint; scanners needing ≥2 indicators miss it. *You detect by
  behavior + context.*
- **Database-resident payloads** — code in `wp_options` (keys like `_hdra_core`,
  `wp_session_tokens_config`), injected `<script>` in posts/postmeta. File-only scanners never see it.
  *You dump and grep every table.*
- **Cloaking** — the payload serves gambling/pharma HTML only to Googlebot/Facebook/mobile, clean to
  everyone else (and the scanner). *You fetch as Googlebot AND as a browser and diff.*

These are four of the **38 malware families** this tool covers — it is an end-to-end cleanup engine, not
a single-signature or cloaking-only scanner. See `references/incident-case-study.md` (a real hack that a
running commercial scanner missed for 11 days — it doubles as your test fixture).

---

## Step 0 — Establish the gateway

Read `references/gateway.md`. Detect your access (SproutOS MCP ▸ other WP MCP ▸ SSH+WP-CLI), probe your
capabilities (read/list/grep/exec/DB/write/logs/origin), and record the environment shape (WP root vs
web root, real `$table_prefix`, multisite, relocated paths, server type, origin IP). Every layer below
degrades gracefully to the capabilities you actually have — never fabricate one you lack. A connected
gateway is required to scan the filesystem/database and to clean; only the external cloaking read
(`scripts/cloaking_diff.py`) can run from your own machine.

---

## The scan: 12 layers (run all your gateway supports)

Run in order. Collect **candidate findings** — delete nothing yet. Each candidate is
`{path_or_location, layer, signal, severity, evidence}`. Verification and removal are dedicated phases
*after* the scan. Full per-layer commands per gateway are in `references/detection-playbook.md`;
machine-readable patterns are in `scripts/signatures.json` (run `scripts/scan_patterns.py`).

### Layer 1 — External cloaking & exposure (the differentiator; needs only `curl`)
For every important public URL (home + sitemap + recently-modified posts): fetch **as `Googlebot`, as
`facebookexternalhit`, and as a desktop browser**, and diff `<title>`, `og:title`, `og:image`, body.
Cloaking = the bot version differs, especially with gambling/pharma/replica keywords
(`seo_spam_keywords`) or `og:image` on a foreign host. Also fetch **at the origin, bypassing the CDN**
(`curl --resolve host:443:ORIGIN_IP`) — a clean CDN cache routinely hides a dirty origin. Test
**query-string cloaking**: fetch junk `?g=`/`?p=casino` URLs as Googlebot (a 2026 hack generated 900k
spam pages purely from `?g=`, invisible in source/DB/sitemap). Automate with `scripts/cloaking_diff.py`.

### Layer 2 — Web root & above-root sweep (not just wp-content)
List the web root and one level above. Flag anything not stock WordPress and not a known plugin/theme:
shadow folders, loose root `.php`, hex/base64-named files, 777 temp/upload dirs,
`indexx.php`/`radio.php`/`wp-conf*.php`-style names, `.sess-<hash>.php` in uploads.

### Layer 3 — Core integrity
`wp core verify-checksums` (or the WordPress.org checksum API for the exact version). Any modified /
"should not exist" core file is a candidate — attackers inject into *legitimate* core files, so detect
**modification**, not just additions. Flag core-filename typosquats (`wp-comments-posts.php`,
`wp-vcd.php`, `wp-user(s).php`) and **wp-config.php injection** (code AFTER `require_once ABSPATH .
'wp-settings.php';` — survives a core reinstall).

### Layer 4 — Plugin/theme integrity
`wp plugin verify-checksums --all` for .org components. **Premium/custom/nulled code has no source of
truth** — for those, fall back to Layer 8 pattern scan + an LLM read + a known-clean vendor zip diff.
Flag: nulled plugins, plugins on disk but not in the admin list (hidden plugins), a fake plugin named
after the site's own domain, unauthenticated `register_rest_route` (`permission_callback =>
'__return_true'`) that writes files / `unserialize`s input, and themes with loose non-template `.php`.

### Layer 5 — Uploads & non-executable dirs
No `.php` should be executable in `wp-content/uploads`, `cache`, etc. Every `.php` there is a candidate
unless it matches a verified known-safe allowlist (`index.php` guards).

### Layer 6 — mu-plugins & drop-ins (auto-loaded, un-deactivatable)
Enumerate `wp-content/mu-plugins/*.php` and the drop-ins `db.php`, `object-cache.php`,
`advanced-cache.php`, `sunrise.php`, `maintenance.php`. These auto-load and **cannot be disabled from
the dashboard** — a favorite 2025 persistence spot (`wp-index.php` in mu-plugins). Read each; anything
not from a known plugin is a candidate.

### Layer 7 — Web-shell & backdoor detection (single indicator OK)
Match `signatures.json → shells` (named shells C99/R57/FilesMan/WSO/Matamu, remote-eval loaders,
uploaders). **Behavioral single-indicator catches:** `wp_set_auth_cookie(` in a loose/non-core file →
auto-login backdoor; `move_uploaded_file($_` / `file_put_contents($_` in a non-plugin file → uploader;
`password_verify(... $_POST` + `session_start` in a lone file → gated shell. Do **not** require two
indicators.

### Layer 8 — Obfuscation & code-exec patterns (all PHP, whole server)
Grep `eval(base64_decode`, `eval(gzinflate`, `eval(str_rot13`, `assert($_`, `system($_`,
`shell_exec($_`, `passthru($_`, `create_function(`, variable-function calls
(`$_GET/$_POST/$_COOKIE/$_REQUEST[...](...)`), `preg_replace('/.*/e'`, long base64 blobs, hex-encoded
`\x65\x76\x61\x6c`. **Exclude `vendor/` and `node_modules/` from the auto-clean path** but still list
them (legit libs use base64) — they go to the verify gate, never auto-deleted.

### Layer 9 — Database injection (every table, not just posts/options)
- `wp_options`: autoloaded options containing code/`<script>`/foreign iframes; `siteurl`/`home` hijack;
  malicious `cron`; injected `active_plugins`. **Flag known IOC keys** (`_hdra_core`,
  `wp_session_tokens_config`) and any autoloaded row carrying base64/PHP.
- `wp_posts`/`postmeta`: gambling/pharma keywords, foreign `<script>`/iframe, link-farm `<a>`, base64 meta.
- `wp_users`/`usermeta`: admins with non-owner emails, unexpected `wp_capabilities` admins, and **admins
  hidden by `pre_user_query`/user-query filters** (count via SQL, not the admin list).
- Redirect tables (Rank Math, Redirection, AIOSEO), comment tables, **log tables** (IOCs). For a
  definitive pass, export the whole DB and grep every byte (`references/remediation-playbook.md`).
  Distinguish **active injection** (clean it) from **passive log records** (safe; optionally purge).

### Layer 10 — Persistence: cron, htaccess, php config, server-level
- WP-cron (`wp_options.cron`) **and** system cron for hooks hitting external URLs / eval / writing
  files. **Map every cron→file relationship** (see self-heal note below).
- `.htaccess` (root and every subdir): injected `RewriteRule` redirects, `php_value auto_prepend_file`,
  base64 handlers, `DirectoryIndex` hijacks. Check `.user.ini`/`php.ini`/PHP-FPM pool for
  `auto_prepend_file`.
- PHP stream-wrapper payloads (`phar://`, `php://filter…convert.base64`, `data://`) with
  `include`/`load_template`; payloads hidden in `.webp`/`.jpg` referenced by PHP.
- **Server-level (if it reinfects within hours, look here):** `~/.bashrc`, system crontab, `/etc/cron.*`,
  service-startup scripts. Malware rewriting `wp-config.php` every few hours is re-dropped by something
  outside WordPress — find it or it never stops.

### Layer 11 — Users, sessions, Application Passwords
List all admins + emails + registration dates (flag non-owner-domain, recently created/edited). List
**Application Passwords and API keys** — attackers mint these for persistence and **they survive
password resets**; they must be revoked explicitly. Cross-reference `session_tokens` IPs against the
attacker IPs from Layer 12.

### Layer 12 — Log forensics (entry-vector attribution; needs log access)
Read access logs. Correlate the **first appearance (mtime)** of each malware file with the requests that
minute: the login/upload sequence, the plugin used to write files, the **real attacker IP** (behind a
CDN it's in `X-Forwarded-For`). Determine the vector: stolen credentials (single clean login, knew a
hidden URL → infostealer) vs plugin exploit (POST to a known-vulnerable endpoint → cross-ref Layer 4
versions against the Wordfence feed). This forensics is what tells the user *how they got in* so it stops.

---

## Self-healing malware (do this or the clean fails)
Modern backdoors reinject on a schedule (wp-cron or system cron restores `wp-login.php` hourly). Before
removing **any** file, build the reinjection graph from Layers 6/9/10/11 — every cron event, drop-in,
mu-plugin, `auto_prepend_file`, and DB option that can rewrite a payload — and neutralize **all nodes in
one pass** (disable the cron + quarantine the writer + clear the option together). Remove a single file
first and it comes back within the interval.

---

## Wordfence vulnerability cross-check (find the door)
For every installed plugin/theme + core version, query the **free Wordfence Intelligence v3 feed** and
flag versions in known-vulnerable ranges (prioritize RCE / auth-bypass / arbitrary-file-upload — those
are the doors) and abandoned/closed plugins. `scripts/wordfence_client.py --match installed-plugins.json`;
setup in `references/wordfence-integration.md`. Enrichment — if no key, skip and say so.

---

## The no-false-positive gate (verify before you touch anything)
**Never delete on a regex match alone.** For every candidate: (1) **read the file/row in full context**
and decide, as an engineer, malicious vs legitimate (minified vendor lib, a plugin that legitimately
uses base64, a `.dat` that legitimately contains `pages.dev`) — one line of reasoning each. (2)
**Classify** `CONFIRMED_MALWARE` / `NEEDS_HUMAN` / `FALSE_POSITIVE`; only CONFIRMED proceeds. (3)
**Corroborate**: a Layer-8 file that also appears in the Layer-12 timeline at the breach minute is
near-certain; a lone `vendor/` base64 with clean mtime is almost certainly FP. (4) Unsure → **NEEDS_HUMAN**,
never delete. Never mark something "clean/verified" you did not actually read. Cite `file:line` /
the matched row / the log line for every CONFIRMED finding.

---

## Removal: reinstall-first, quarantine-first, code-enforced ("never crash")
Follow `references/remediation-playbook.md` exactly.

1. **Backup is the user's and it is mandatory — hard-remind, do not self-serve.** Before any destructive
   step, explicitly tell the user to take a full **off-site** backup (files + DB). Then register a fresh
   snapshot so deletion is *mechanically* blocked without one:
   `python3 scripts/safety_gate.py init --session <dir> --db-backup <db-before.sql>`. The gate refuses
   every quarantine unless a backup is registered and under 24h old — enforced in code.
2. **Health baseline.** `python3 scripts/safety_gate.py health --url <home> --url <wp-admin>`. This is
   your rollback tripwire.
3. **Reinstall injected legitimate files from clean source FIRST** (more reliable than editing):
   core via `python3 scripts/reinstall_clean.py core --wp-path <dir>` → `wp core download --force`;
   .org plugins via `python3 scripts/reinstall_clean.py plugin --slug X` → `wp plugin install X
   --version=Y --force`. (Premium/nulled have no clean source — pattern-clean + vendor re-download.)
4. **Neutralize the self-heal graph atomically** (above), **then quarantine standalone malware, don't
   delete:** `python3 scripts/safety_gate.py quarantine --session <dir> --file <path>` (moves, records a
   sha256 manifest, refuses without the backup). Back up DB rows before `DELETE`.
5. **Verify after each destructive step:** re-run the health check; on regression (5xx / new fatal)
   **roll back immediately** (`safety_gate.py rollback --session <dir>`) and mark `NEEDS_HUMAN`.
6. **Rotate secrets (mandatory post-clean):** `wp config shuffle-salts` (kills stolen auth cookies +
   forces logout), **revoke unexpected Application Passwords/API keys**, force admin password reset, and
   remind the user to rotate FTP/SSH/DB/cPanel creds from a *clean* device.
7. **Prove clean (the gate before you say "done"):** re-scan Layers 1–12 = zero findings **AND** all of:
   `wp core verify-checksums` / `wp plugin verify-checksums --all` return zero mismatches; the
   **origin-as-Googlebot fetch equals the browser fetch** (no cloaked content — headless-render if the
   check reports `needs_headless_check`); external reputation clears (Google Safe Browsing / VirusTotal);
   and a **10–14 day reinfection watch** is scheduled. Reinfection typically surfaces in that window.

Destructive DB/file ops always require explicit user confirmation. Never `--no-verify`, never skip the
snapshot, never delete a drop-in/core file without a reinstall plan. Prefer reinstall-from-clean over
surgical edits whenever an official clean copy exists.

---

## Report (what commercial scanners don't give you)
- **Executive summary:** infected? what kind? live to Google now? entry vector in one line.
- **Findings table:** every CONFIRMED item — location, type, evidence, action, quarantine path.
- **Attack timeline** (Layer 12): first breach, files planted, attacker IP(s), tool used.
- **Entry vector + the fix** (stolen creds → 2FA via wp-hardening; plugin CVE → update X to Y).
- **NEEDS_HUMAN list** with recommendations · **Hardening handoff** (point at wp-hardening).
- **Proof of clean:** origin-as-Googlebot fetch showing no cloaked content + checksum-zero.

Keep it honest: if you could only scan (read-only gateway), scope the claims. If a step was skipped, say
which and why. Never assert "clean" without the re-scan + prove-clean evidence. **What you can promise:**
every finding on this run removed, quarantine-safe, proven clean at the origin as Googlebot, site healthy
throughout, entry vector attributed. **What you must not promise:** absolute "100% clean forever" — no
scanner catches a truly novel, encrypted, memory-only payload with certainty. A named coverage gap is
trustworthy; a silent one is how sites get re-owned.

---

## Operating rules (non-negotiable)
- **Read before you claim.** No "verified/clean/fixed" without the actual read/command output; cite evidence.
- **Quarantine before delete. Snapshot before quarantine. Health-check after every destructive step.**
- **Confirm irreversible actions**; show the manifest first.
- **Degrade gracefully** to the gateway's real capabilities; never fabricate one you lack.
- **FP discipline:** unsure → NEEDS_HUMAN. `vendor/`/`node_modules/`/minified are never auto-deleted.
- **Scope the whole server and every DB table**, not just `wp-content` and `wp_posts`.

## Bundled resources
- `references/gateway.md` — reach the site over SproutOS / any WP MCP / SSH+WP-CLI.
- `references/malware-families.md` — the 38 families, how each hides, hard 2026 threat data.
- `references/detection-playbook.md` — full per-layer commands per gateway.
- `references/signature-library.md` — human-readable signatures + real IOCs.
- `references/cloaking-detection.md` — the bot-vs-browser method in depth.
- `references/remediation-playbook.md` — snapshot → quarantine → verify → rollback, exact steps.
- `references/wordfence-integration.md` — free v3 API key + matching installed versions.
- `references/edge-cases.md` — multisite, custom prefixes, object cache, reinfection ordering, the pre-done self-check.
- `references/incident-case-study.md` — a real hack end to end, as your test fixture.
- `scripts/cloaking_diff.py` · `scripts/scan_patterns.py` (+`signatures.json`) · `scripts/safety_gate.py`
  (code-enforced backup/quarantine/rollback) · `scripts/reinstall_clean.py` · `scripts/wordfence_client.py`.