Reference · WordPress Security

The hack recovery
playbook.

A field-tested reference for cleaning a compromised WordPress site without losing rankings. Malware forensics, ghost-admin detection, robots.txt integrity, and the 410-vs-404 call that decides how fast you recover.

Table of contents

  1. Phase 1 — Contain, don't restore
  2. Phase 2 — Full-stack forensics
  3. Phase 3 — The ghost admin sweep
  4. Phase 4 — Robots.txt integrity check
  5. Phase 5 — 410 the spam, don't 404 it
  6. Phase 6 — Search Console recovery
  7. FAQ
Phase 01

Contain, don’t restore

The first instinct is always the worst one: restore last week’s backup and call it done. If the compromise happened three weeks ago and you don’t know the entry vector, you’ve just restored the vulnerability that let them in.

Do this instead:

  • Take a full, timestamped snapshot of the compromised state — files, database, and server logs. That’s your forensic evidence.
  • Put the site behind a maintenance page at the server level, not a plugin. The plugin was probably compromised.
  • Rotate every credential that can touch the server: hosting panel, SFTP, database, WordPress admins, any API keys stored in wp-config.
  • Pull the last 30 days of access logs. That’s where the entry vector is.

A restore without forensics is a rehearsal for the next compromise.

Phase 02

Full-stack forensics

Malware in WordPress hides in six places. Check all six, in this order:

  1. Core files — diff against a fresh WordPress download of the exact version.
  2. Plugin and theme files — diff against fresh copies from the official repos.
  3. wp-content/uploads — any PHP file in here is malware. No exceptions.
  4. mu-plugins — most-abused directory in modern WordPress compromises. Almost no legitimate reason for a file here unless you put it there.
  5. Database — wp_options for injected scripts, wp_posts for injected content, wp_users for ghost admins (see Phase 3).
  6. .htaccess and web-server config — injected rewrites, PHP handler swaps, and mime-type tricks.
Phase 03

The ghost admin sweep

A ghost admin is a user with admin capabilities that the WordPress dashboard has been made to hide. You can wp-cli list users all day — if the attacker registered a pre_get_users filter or set a database flag, the account won’t appear. It’s the single most common reason “cleaned” sites reinfect.

Bypass the dashboard entirely. Query the source of truth:

SELECT u.ID, u.user_login, u.user_email, u.user_registered, m.meta_value AS capabilities
FROM wp_users u
JOIN wp_usermeta m ON m.user_id = u.ID
WHERE m.meta_key = 'wp_capabilities'
  AND m.meta_value LIKE '%administrator%'
ORDER BY u.user_registered DESC;

Every row is an admin. If a row is not in your dashboard user list, that row is the ghost. Delete via SQL, not the dashboard — the same filter that hides it will refuse to delete it.

Then check for mu-plugins that filter user visibility:

grep -r "pre_get_users\|users_list_table\|user_row_actions" wp-content/mu-plugins/
Phase 04

Robots.txt integrity check

Robots.txt is almost never in a version-controlled state. Most site owners can’t tell you what it should say. That’s exactly why it’s a great attack surface — nobody notices when it changes.

The five-minute check:

  1. Fetch /robots.txt from the live site and diff it against what WordPress generates by default (or what your SEO plugin generates).
  2. Any Allow: line for a path that isn’t obvious content is suspicious. Allow: /wp-content/uploads/2024/ is fine; Allow: /go/ or Allow: /out/ is a red flag.
  3. Check file permissions. Robots.txt should be 644, owned by you, not by the web server user. If the web server can write it, the attacker can too.
  4. Check for a physical robots.txt file overriding a plugin-generated one. Delete the physical file — let the plugin serve it — and confirm the served content is what you expect.
Phase 05

410 the spam, don’t 404 it

After a cleanup with spam URLs in the index, the default WordPress behaviour is to 404 anything it doesn’t recognise. That’s wrong. A 404 tells Google “temporarily missing.” A 410 tells Google “permanently gone, remove from index.” The difference in de-indexing speed is measured in months.

Drop this in .htaccess above the WordPress block, adjusting the pattern to match the spam paths from Phase 4:

# 410 sweep — post-hack spam URL cluster
RewriteEngine On
RewriteRule ^go/  - [G,L]
RewriteRule ^out/  - [G,L]
RewriteRule ^(.+)\.(html?|aspx?|jsp)$ - [G,L]

# Then the standard WordPress block below...

Verify with curl -I https://yoursite.com/go/whatever — you want a HTTP/1.1 410 Gone response, not 404.

Phase 06

Search Console recovery

Google needs to be told what happened. In this order:

  1. Submit a URL removal request for the parent path of the spam cluster (e.g. /go/). This is the fastest hide.
  2. Submit an updated sitemap.xml containing only real URLs. If the sitemap was compromised, regenerate it.
  3. If there was a manual action or a “Site may be hacked” label, file a reconsideration request with a written cleanup report.
  4. Monitor the Pages report weekly. The “Not found (404)” bucket will empty as spam URLs get re-crawled and hit the 410. The “Crawled — currently not indexed” bucket may spike temporarily; that’s recovery, not regression.

FAQ

Should I use 410 or 404 for URLs a hacker created?

410 Gone. Every time. A 404 tells Google 'this might come back' and it will re-crawl for months, keeping the spam URLs in the index. A 410 tells Google 'this is permanently gone, drop it' and de-indexing accelerates by 3–8x. For post-hack cleanup where you might have 1M+ spam URLs indexed, that difference is the difference between recovering rankings in 30 days vs 6 months.

How do I know the malware is actually gone?

Three tests, in order. One — the file integrity scan against a fresh WordPress core + your original plugin/theme versions must return zero mismatches. Two — a full database query for suspicious admin users, hidden options, and injected scheduled tasks (wp_cron) must come back clean. Three — a 7-day monitored window with file-change alerts and no reinfection. Only after all three do I call a site clean.

What's a 'ghost admin' and how does it survive normal cleanup?

A ghost admin is a user account with admin privileges that's been deliberately hidden from the WordPress dashboard using a database-level flag or an mu-plugin that filters user queries. You can wp-cli list users, reinstall core, and rotate all visible passwords — the ghost is still there because it never appeared in your list. The fix is a direct SQL query against wp_users JOIN wp_usermeta filtered on capabilities, not the admin UI.

Can robots.txt itself be the attack vector?

Yes, and it's underreported. If robots.txt is writable by the web server user (which it often is on shared hosting), an attacker can inject an allow: / for a spam directory or, worse, replace the file so Google is invited to crawl injected paths that don't exist as files but resolve through .htaccess rewrites. I've cleaned one case where 1.24 million spam URLs got indexed via this vector. The five-minute integrity check is at the bottom of this page.

Do I need to tell Google I got hacked?

If you had a manual action or a 'Site may be hacked' warning, yes — file a reconsideration request after cleanup with a written report of what was found and fixed. If it was silent (no warning), Search Console's coverage report is your recovery signal — file a URL removal request for the worst spam clusters, submit an updated sitemap, and monitor the 'Discovered - not indexed' bucket for the 410 catches.

How long does full recovery take?

Cleanup — 4 to 48 hours depending on infection depth. Ranking recovery — 2 to 12 weeks depending on how long the spam URLs were indexed and how aggressive the 410 sweep is. Sites cleaned within a week of infection usually recover fully. Sites left infected for 3+ months often need a full content and trust rebuild alongside the technical cleanup.

Further reading — WebCare Studios

Site down? WhatsApp now