Recovery · 13 min read

WordPress malware removal: a step-by-step recovery guide.

An eight-step playbook for cleaning a hacked WordPress site — the same triage we run when a paying client emails at 2 a.m. Triage, eradication, root cause, and the hardening that keeps it from happening twice.

If you're reading this, you've probably already confirmed that your WordPress site is compromised. (If you're still on "I think something is wrong," start with our companion piece on how to tell if your WordPress site is hacked.) This guide walks through every step of cleaning the site — the same playbook our team runs on real client incidents.

Plan for two to four hours of focused work for a typical small-business site. A larger e-commerce site can take a full day. The most important rule: do these steps in order. Skipping ahead is how cleanups fail and the malware comes back the following week.

When to call a professional instead.

If your site processes payments, stores customer PII, runs HIPAA-regulated content, or is your primary revenue channel, hire someone. The cost of a botched DIY cleanup (re-infection, data exfiltration during cleanup, a Google Safe Browsing flag that takes weeks to clear) usually exceeds the cost of professional remediation. Talk to us if you're not sure.

Step 01 ~10 minutes

Snapshot the compromised state — before you change anything.

The first thing every attacker hopes you'll do is panic-delete files. That destroys forensic evidence, breaks your ability to identify the entry vector, and frequently makes the cleanup harder. Resist.

Instead, capture two artifacts:

  • Filesystem snapshot: a tar of the entire WordPress directory. Via SSH: tar -czf compromised-site-2026-04-27.tar.gz /var/www/yoursite.
  • Database dump: mysqldump -u root -p yourdb > compromised-db-2026-04-27.sql.

Store both off the server, ideally on encrypted local storage. Label them clearly as compromised so they don't accidentally get restored later.

Why this matters: Without a snapshot, you can't compare clean against compromised, you can't identify the attacker's tools, and you can't prove to a registrar or host that a specific file was malicious. We've helped clients recover stolen domains because they had this evidence preserved.

Step 02 ~5 minutes

Take the site offline — safely.

A compromised site that's serving real traffic is doing damage to visitors and to your domain reputation every minute. But "offline" doesn't mean "deleted." Two clean ways to take it down:

  • Maintenance page via a static index.html: rename WordPress's index.php to index.php.bak and drop a static HTML file in its place. Visitors get a clean message, attackers can no longer hit your WordPress endpoints.
  • Cloudflare "Under Attack" mode + IP allowlist: if you can't touch files easily, point your domain through Cloudflare and restrict access to your own IP. Slightly slower to configure, much faster to revert.

Don't do this: don't simply password-protect wp-admin. Many WordPress malware payloads run on every page load, not just admin pages — visitors are still being served the malicious content.

Step 03 ~30–60 minutes

Identify every infected file.

This is the slowest and most important step. The goal is a complete inventory of what is malicious, what is modified-but-recoverable, and what is clean. Five techniques, in order:

  1. Run WP-CLI checksum verification: wp core verify-checksums and wp plugin verify-checksums --all. Anything that fails is either modified or extra.
  2. Find recently modified files: find . -type f -mtime -30 -not -path "./wp-content/cache/*" -not -path "./wp-content/uploads/cache/*". A site that's not under active development should have very few results in the last 30 days.
  3. Find PHP in places it shouldn't be: find wp-content/uploads -name "*.php" -o -name "*.phtml" -o -name "*.phar". None of these belong in uploads/. All are presumed malicious.
  4. Grep for common payload signatures: eval(base64_decode, FilesMan, WSO, str_rot13(, gzinflate(base64_decode, preg_replace.*\/e. Use grep -rE across the whole tree.
  5. Diff against clean copies: download the exact same WordPress version and the exact same plugin versions (from the wordpress.org SVN, not the live plugin page) and run diff -r against your install.

Build a list as you go. For each file, note: path, why it's flagged, and whether you'll delete or restore.

Step 04 ~30 minutes

Replace WordPress core, plugins, and themes wholesale.

Don't try to surgically clean modified core files. It's faster, safer, and more thorough to replace them entirely with known-good copies.

  1. Download the exact WordPress version your site runs (wordpress.org/download/releases/).
  2. Delete wp-admin/ and wp-includes/ from the live site. Replace with the freshly downloaded versions.
  3. For each plugin: delete the plugin folder entirely, reinstall the latest clean version from wordpress.org.
  4. For your active theme: if it's from a public marketplace, reinstall fresh. If it's a custom theme, manually compare each file against your last clean Git commit (you do have one in version control, right?).

The only files you should leave alone in this step are: wp-config.php (we'll handle it in step 5), the database, and the wp-content/uploads/ directory (after you've cleaned PHP from it in step 3).

Why this matters: Manual cleanup of injected code is a game of whack-a-mole. Modern WordPress malware often plants 30–50 backdoors across core files. Wholesale replacement guarantees you got them all in those directories.

Step 05 ~30 minutes

Clean the database.

Files are easy. The database is where most cleanups go wrong. Six things to check, in order:

  • wp_users: delete any admin you don't recognize. Cross-reference against wp_usermeta for entries with wp_capabilities set to administrator — sometimes attackers add caps without making the user visible in wp_users.
  • wp_options: inspect the siteurl and home options first. Then look at any option containing base64, eval, or unusually long auto-loaded values: SELECT option_name, length(option_value) FROM wp_options WHERE autoload='yes' ORDER BY length(option_value) DESC LIMIT 20;
  • wp_posts: hunt for injected SEO spam. Look for posts in languages you don't publish in, posts with hidden CSS (style="display:none"), and post statuses you didn't create. The post_modified column is also useful for finding recently-touched posts.
  • wp_postmeta: check for serialized payloads in keys you don't recognize.
  • Scheduled events: SELECT option_value FROM wp_options WHERE option_name='cron';. Look for hooks with random names or callbacks pointing at suspicious functions.
  • wp-config.php: open it and read every line. Look for auto_prepend_file directives, base64-encoded blobs, and any require or include pointing at unfamiliar paths.

Re-generate WordPress salts (https://api.wordpress.org/secret-key/1.1/salt/) and paste them into wp-config.php. This invalidates every existing session immediately.

Step 06 ~30 minutes

Find the root cause — or the malware will be back.

This is the step DIY cleanups skip, and it's why ~40% of self-cleaned sites get re-infected within 90 days. You have to know how the attacker got in, otherwise the same vulnerability is still sitting there waiting.

The investigation order:

  1. Check access logs around the time of compromise. Look at the find -mtime result from step 3 — that gives you a date. Cross-reference your web server access log for unusual POST requests to plugin admin-ajax endpoints, REST routes, or xmlrpc.php in the days before the file was modified.
  2. Match request paths to known plugin CVEs. If you see a flurry of POSTs to /wp-admin/admin-ajax.php?action=elementor_save_data from a single IP and Elementor was out of date — there's your vector. The Wordfence and Patchstack vulnerability databases are searchable by plugin slug.
  3. Check brute-force activity against wp-login.php and xmlrpc.php. Hundreds of failed POSTs followed by one successful one means a credential was guessed (or leaked elsewhere).
  4. Check FTP/SFTP logs. If a credential was leaked, the entry won't show up in HTTP logs at all.

Why this matters: Without a confirmed root cause, you don't know if the attacker is still in the system. The hosting account itself, your admin laptop, or a CI/CD pipeline could all still be compromised — and a clean site behind a compromised SSH key gets re-popped within days.

Step 07 ~20 minutes

Rotate every credential the site touches.

Assume every secret on the server is in attacker hands. The full rotation list:

  • All WordPress admin passwords
  • WordPress salt keys (already done in step 5)
  • Database password (update wp-config.php after)
  • SFTP/SSH credentials, including key-based auth — regenerate the keys
  • Hosting control panel password (cPanel, Plesk, host dashboard)
  • API keys for Stripe, Mailchimp, SendGrid, anything that lives in wp_options
  • Two-factor authentication seeds for any admin (TOTP secrets may have been exfiltrated from wp_usermeta)

If your site connects to a Git repository, also revoke any deploy keys and personal access tokens that lived on the compromised server.

Step 08 ~45 minutes

Harden, monitor, and request a review.

You now have a clean site sitting on the same configuration that just got compromised. Don't bring it back online without:

  1. Running our WordPress hardening guide end-to-end. Every step. The recovery effort you just put in is wasted if the same vector is still open.
  2. Installing a file-integrity monitor. The free SecureAuditWP plugin handles this — alerts you within minutes if any monitored file is modified.
  3. Submitting a Google Safe Browsing review via Search Console (Security & Manual Actions → Security issues → Request Review). Cleanup before review submission, not after — Google will re-flag a site that fails verification.
  4. Notifying affected users if any customer data was on the site. This is legally required in many jurisdictions (GDPR, CCPA, similar) within 72 hours of discovery.
  5. Documenting what happened. Date of compromise, suspected vector, files affected, action taken, date of recovery. Future-you will need this — incidents have a way of recurring, and the next person on call will thank you.

Bring the site back online. Watch the access logs and integrity alerts closely for the next 30 days. The attacker often comes back to verify their access — that's the moment to confirm the fix held.

When restoring from backup is the better choice.

Skipping all of the above and restoring from a known-clean backup is sometimes the right call. The decision tree:

  • Restore from backup if: you have a verified clean backup from before the compromise, no significant content has been added since, and you can confidently identify the root cause to fix it on the restored copy.
  • Clean in place if: you don't have a recent backup, the site has live content (orders, comments, posts) you can't lose, or the compromise predates your oldest backup.

The trap is restoring from a backup that's also compromised. Always test the restore on a staging environment first and run all the detection steps from our 12-signs guide against it before going live.

How long does a real WordPress malware cleanup take?

Realistic estimates from our internal data, by site complexity:

  • Brochure site, 5–20 pages, no commerce: 2–4 hours.
  • Content site, blog with 200+ posts, no commerce: 4–6 hours.
  • WooCommerce site under 1,000 SKUs: 6–10 hours, mostly because of the larger database.
  • Multisite or 1,000+ SKU commerce: 12+ hours, often spread over two days.

If your DIY cleanup is taking dramatically longer than this, something is wrong — usually you're missing the root cause and the malware is re-installing itself between cleanup passes. Stop, re-read step 6, or get help.

Don't want to do this twice.

SecureAuditWP runs file-integrity monitoring, plugin vulnerability checks, and weekly audits — so the next compromise gets caught at hour one, not month three.