---
title: "How to clean a hacked WordPress site (and find the way in)"
date: "2026-06-02"
updated: "2026-09-07"
excerpt: "The first malicious file you find is almost never the way in. How to separate payload from entry point, where injected code hides, and how to stop it returning."
tags: ["WordPress", "Security", "Debugging"]
draft: false
category: "guide"
tldr: "The first malicious file you find is the payload, not the entry point. Back up the compromised state, find when it started, check the usual hiding places (database options, mu-plugins, functions.php, uploads, cron, admin users), verify with checksums, then close the vector by updating everything and rotating every credential."
howToSteps:
  - name: "Back up before you clean anything"
    text: "Take a full backup of the compromised files and database, then take the site offline or into maintenance mode so you aren't fighting reinfection while you work."
  - name: "Establish when it started"
    text: "Find recently modified PHP files and cross-reference the timestamps against your access logs to narrow down the moment of compromise."
  - name: "Search where injected code actually hides"
    text: "Check wp_options for siteurl and home, large autoloaded options, wp-content/mu-plugins, the active theme's functions.php, PHP files inside wp-content/uploads, scheduled cron events, and the administrator user list."
  - name: "Verify with checksums"
    text: "Run wp core verify-checksums and wp plugin verify-checksums --all to find modified core and repository plugin files automatically."
  - name: "Close the vector"
    text: "Update core, plugins, themes, and PHP; remove anything unused; then rotate every credential and replace the wp-config.php salts to invalidate existing sessions, including the attacker's."
faqs:
  - question: "Is the first malicious file I find the way the attacker got in?"
    answer: "Almost never. It's usually the payload (the visible damage), not the vector. Deleting it without finding the entry point means the infection comes back."
  - question: "What's the difference between a payload and a vector?"
    answer: "A payload is what does the visible damage, like a redirect or spam pages. A vector is how it got in, such as an outdated plugin, a reused password, or a backdoor left from an earlier compromise."
  - question: "Where does injected code usually hide on a hacked WordPress site?"
    answer: "Beyond the obvious files: the wp_options database table, must-use plugins in wp-content/mu-plugins, the active theme's functions.php, PHP files placed in wp-content/uploads, scheduled cron events, and unfamiliar administrator accounts."
  - question: "When should I rebuild instead of cleaning?"
    answer: "When the install has been compromised repeatedly, backdoors turn up in unrelated directories, or nobody can account for what half the plugins are for. A fresh WordPress install with clean plugin copies is faster and safer than another cleanup."
---

The first malicious file you find is almost never the entry point. It's
the payload. Delete it and the symptom disappears for a few days, then
comes back, which is how most people end up on their second or third
"cleanup" of the same site.

## Payload and vector are different problems

A **payload** is what does the visible damage: the mobile-only redirect,
the spam pages, the injected script in the footer, the fake plugin in the
admin list.

A **vector** is how it got in: an outdated plugin with a known
vulnerability, a reused admin password, a writable uploads directory
serving PHP, a stolen hosting credential, or a backdoor left behind by an
earlier compromise nobody fully cleaned.

Fixing only the payload guarantees a repeat visit. Almost all of the real
work is finding the vector.

## Before you clean anything

Take a full backup of the compromised state: files and database. You are
about to destroy evidence, and if you later need to work out what was
taken, or when it started, that snapshot is the only record you'll have.

Then take the site offline or put it behind maintenance mode. Cleaning a
live site while it's still serving the payload means fighting reinfection
while you work.

## Establishing when it started

Timestamps narrow everything else down. Find recently modified files:

```
find . -type f -name "*.php" -mtime -30 -printf "%TY-%Tm-%Td %p\n" | sort
```

What you're looking for is a file whose modification time doesn't match
its neighbours. Core files all written on the same date are a WordPress
update. One file in a theme folder touched three months after everything
else is worth opening.

Treat this as a lead, not proof. Modification times can be set by
whoever wrote the file. But attackers are frequently lazy, and it works
more often than it should.

Cross-reference against your access logs for that date. A POST to an
unexpected endpoint, or a burst of requests to one plugin's file, is
usually the moment you're looking for.

## Where injected code actually hides

**The database, not just the filesystem.** Check `wp_options` for
`siteurl` and `home` first. If those point somewhere unfamiliar, that's
your redirect. Then look at large autoloaded options, which load on every
single request and are a comfortable hiding place:

```
wp option list --autoload=on --format=table --fields=option_name,size_bytes \
  | sort -k2 -n -r | head -20
```

Anything unexpectedly large near the top deserves a look.

**Must-use plugins.** `wp-content/mu-plugins/` loads unconditionally, can't
be deactivated from the admin, and doesn't appear in the normal plugin
list. It's a favourite. Many sites legitimately have nothing there at all,
which makes anything you find easy to assess.

**The active theme's `functions.php`**, and any file it includes. Scroll to
the bottom. Appended code after a long run of blank lines is a common
pattern.

**PHP in `wp-content/uploads/`.** Nothing in the uploads directory should
ever execute:

```
find wp-content/uploads -name "*.php" -o -name "*.phtml"
```

Any result here is worth treating as hostile until proven otherwise.

**Scheduled tasks.** `wp cron event list` will show jobs that reinstall a
payload on a timer. If you clean files and the infection returns on a
schedule, this is usually why.

**Admin users.** `wp user list --role=administrator`. Look for accounts you
don't recognise, and for legitimate accounts whose email address has been
changed.

## Let the checksums do the boring part

WP-CLI compares core files against the official release:

```
wp core verify-checksums
wp plugin verify-checksums --all
```

This finds modified core and repository plugin files in seconds and is far
more reliable than reading through directories yourself. It won't cover
premium or custom plugins; compare those against a clean copy from
the vendor.

## Closing the vector

Once the payload is gone, the actual job starts.

Update everything: core, plugins, themes, PHP version. Remove plugins
and themes that aren't in use rather than leaving them deactivated;
deactivated code is still on disk and still reachable.

Then rotate credentials, all of them, not just the one you think was
used: admin passwords, database password, hosting and SFTP, and any API
keys stored in the site. Replace the salts in `wp-config.php` too, which
invalidates every existing login session, including the attacker's.

If the compromise had filesystem access for any length of time, assume
anything writable may have been touched.

## Before you call it done

- Fetch the site with a **mobile user agent** and with a search-engine
  referrer. Plenty of payloads stay dormant for a normal desktop visit and
  only fire for those.
- Check **Google Search Console** for security notices, and request a
  review if the site was flagged.
- Search the rendered HTML of a few pages for `<script` tags you can't
  account for.
- Watch the logs for a week. Reinfection usually shows up quickly.

## When should you stop cleaning and rebuild instead?

Sometimes cleaning is the wrong call. If the install has been compromised
repeatedly, if you're finding backdoors in unrelated directories, or if
nobody can tell you what half the plugins are for, the faster and safer
path is a rebuild: fresh WordPress, fresh copies of each plugin from
source, and only the content and uploads migrated across, with the
uploads scanned before they go anywhere.

That's a harder conversation to have with a client than "I cleaned it".
It's also the one that ends with a site you can stop thinking about.

Once it is clean and updated, the work that keeps it that way is dull and
scheduled. See [a WordPress maintenance checklist that prevents problems](/blog/wordpress-maintenance-checklist).
If you are dealing with an active security compromise right now and need urgent containment, learn how I handle [WordPress emergency site recovery and malware cleanup](/wordpress-site-recovery).
