Skip to content

SecurityPart of: Hacked websites

WordPress hacked? Clean it and keep it clean, step by step

WordPress hacked? Contain it, preserve the evidence, find the way in, reinstall clean, verify what Google sees, harden and request a review, with the wp-cli commands for each step.

getReport teamUpdated 27 Sept 202613 min read

If your WordPress site has been hacked, work in this order: take it offline and change every password, copy the infected site for evidence, find how the attacker got in, then replace core, plugins and themes with clean copies instead of hunting for bad code. Only then verify what Google sees, harden the site and request a review. A hacked WordPress site is rarely destroyed; it is borrowed, kept working so you do not notice, and used to rank spam, redirect visitors or send email. The order decides whether the malware is back in a week. If you are not yet sure it is hacked, start with is my website hacked? and come back here with the evidence.

Quick answer

  1. Contain: maintenance mode, every password and key changed, unknown admin users removed.
  2. Preserve: a full copy of files and database as they are now, plus 30 days of access logs.
  3. Find the entry: outdated core, plugin or theme; a closed or abandoned plugin; a nulled theme; a weak password; a neighbour on the same server.
  4. Remove the malware by reinstalling: fresh core, plugins and themes from their source; keep only uploads (scanned); review wp-config.php and .htaccess; query the database for injected scripts.
  5. Verify with the hacked site checker: the Googlebot and Google-referrer fetches must match the visitor fetch.
  6. Harden, request review in Search Console, and re-check in a week.

Why order matters

Two things go wrong in almost every failed WordPress malware removal. The first is cleaning before finding the entry: the injected files go, the hole stays, and the same script writes them back overnight, often through a cron entry or a backdoor in the uploads folder. The second is restoring a backup that is itself infected, because the injection sat there for weeks before anyone noticed.

Both are avoided by doing the boring steps first: preserving the evidence so you can find the entry, and reinstalling from sources you trust rather than editing the infected copy. Reinstalling feels drastic and is faster than reading every PHP file on the server.

How getReport checks it

The checker fetches the page three times: with getReport's normal user agent, with Googlebot's user agent, and as desktop Chrome with a Referer of https://www.google.com/. Each copy is reduced to a fingerprint: the visible text (hidden elements removed), the outbound links, spam vocabulary, runs of Japanese, Chinese, Korean or Cyrillic text on a page whose lang says otherwise, and any inline script that reads document.referrer and redirects. The three fingerprints are compared.

Fetch comparison table of a hacked WordPress fixture: three columns for the normal visitor, Googlebot and a visitor from Google, with the words-not-on-visitor-page share, hidden links, spam vocabulary and referrer redirect script highlighted in the two right-hand columns
The two right-hand columns show what Google receives; anything highlighted there and not in the visitor column is what the owner never sees.

Cloaking is reported when at least 30% of the most frequent words in the Googlebot or Google-referrer copy are missing from the visitor copy and at least 20 new words appeared, or when foreign-script text appears only in the bot copy. Some sites vary content by user agent on purpose, so the finding shows the new words as evidence, not a verdict.

The redirect check compares where each fetch ends. It does not fetch with a mobile user agent, so a redirect that fires only on phones needs the manual check in step 5. The Japanese keyword hack and pharma and casino injections guides cover the two common shapes. The ordinary security findings point at the likely entry:

Step by step

1. Contain

Take the site out of circulation first, so the attacker's code cannot keep serving spam or stealing form input while you work: a maintenance page from the host panel, or a rule that lets only your IP through.

Then change every password that touches the site, in this order: hosting panel, SFTP or SSH, the database user (update wp-config.php to match), every WordPress administrator, and the API keys stored in plugins (payment gateways, mail services, CDN). Then look for administrators you did not create:

Shell
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered

Delete unknown ones and reassign their content (wp user delete 17 --reassign=1). Rotate the salts in wp-config.php with wp config shuffle-salts to log everyone out.

2. Preserve the evidence

Before deleting anything, copy the web root and a database dump to a folder that is not web-accessible, plus the access and error logs for the last 30 days. You need them in step 3, and if customer data may have been read, for the disclosure you may be legally required to make.

Shell
mkdir -p ~/evidence-$(date +%F)
wp db export ~/evidence-$(date +%F)/db.sql
tar czf ~/evidence-$(date +%F)/files.tgz -C /var/www/example.com .
cp /var/log/apache2/access.log* ~/evidence-$(date +%F)/

3. Find the entry

Most WordPress compromises come through one of five doors:

  • An outdated or vulnerable plugin, theme or core. Plugins carry most published WordPress vulnerabilities; finding vulnerable and abandoned plugins shows how to match versions against the databases. A plugin closed on wordpress.org for a security issue is the first suspect.
  • A nulled theme or plugin. Premium software from a "free" download site very often ships with a backdoor.
  • Weak or reused passwords. A login called admin with a short password is brute-forced within days.
  • A neighbour on shared hosting. If your files are clean and the entry is unexplained, ask the host.
  • A compromised computer. Saved SFTP passwords on an infected laptop hand over every site at once.

The access log narrows it down. Look for POST requests to files that should never receive them, and for the first request to a file you did not create:

Shell
# POSTs to PHP files other than the normal entry points
grep -E 'POST .*\.php' access.log | grep -v -E 'wp-login\.php|admin-ajax\.php|wp-cron\.php|xmlrpc\.php' | head -50

# When did a suspicious file first appear in the log?
grep 'wp-content/uploads/2024/07/class-cache.php' access.log | head -3

A spike of POSTs to wp-login.php or xmlrpc.php, then a successful login, points at a guessed password.

4. Remove the malware by reinstalling

Do not hunt for injections in 10,000 PHP files. Replace them with known-good copies and keep only what cannot be reinstalled.

  1. See the scale. WP-CLI checks core and plugin files against the checksums wordpress.org publishes. Every line it prints is a modified or added file; note them, they show where the attacker worked. Premium plugins have no published checksums and are reported as unverifiable.
Shell
wp core verify-checksums
wp plugin verify-checksums --all
  1. Fresh core. wp core download --force --skip-content overwrites wp-admin, wp-includes and the root files. It does not remove extra files, so run wp core verify-checksums again and delete anything it reports as "should not exist".

  2. Fresh plugins and themes. Reinstall each from wordpress.org (wp plugin install <slug> --force) or from the vendor's download. Delete plugins and themes you do not use; an inactive plugin is still executable code.

  3. Uploads. Keep wp-content/uploads after scanning it. There is no legitimate PHP file in uploads:

Shell
find wp-content/uploads -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.php5' \) -print
# Recently modified PHP anywhere, excluding the freshly reinstalled folders
find . -name '*.php' -mtime -14 -not -path './wp-admin/*' -not -path './wp-includes/*' -print
  1. wp-config.php, .htaccess and mu-plugins. Attackers add an include of a file in uploads to wp-config.php, and rewrite rules to .htaccess that send Googlebot elsewhere. Anything that mentions a user agent, a referrer or an unknown .php file goes. Files in wp-content/mu-plugins run on every request and never appear in the plugin list.

  2. The database. Injections live in posts, options and cron:

SQL
-- Scripts or iframes inside post content
SELECT ID, post_title, post_type FROM wp_posts
WHERE post_content LIKE '%<script%' OR post_content LIKE '%<iframe%' OR post_content LIKE '%eval(%';

-- Options that carry scripts: widgets, theme settings
SELECT option_name FROM wp_options
WHERE option_value LIKE '%<script%' OR option_value LIKE '%base64_decode%' OR option_value LIKE '%eval(%';

-- The two addresses that a redirect hack rewrites
SELECT option_name, option_value FROM wp_options WHERE option_name IN ('siteurl', 'home');

Then list scheduled jobs with wp cron event list --fields=hook,next_run_relative,recurrence, and delete hooks with random names or no plugin you can map them to (wp cron event delete <hook>).

5. Verify

Run the hacked site checker on the home page and three deep pages: an old post, a category page and a page Google listed with spam. The "Words not on the visitor page" share should be low in both right-hand columns, and hidden links, spam vocabulary, text in another script and the referrer redirect script should read 0 or none.

For the phone-only redirect the checker does not test:

Shell
curl -sI -A 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1' -e 'https://www.google.com/' https://example.com/ | grep -i -E '^(HTTP|location)'

Then Search Console → Security issues, and a site:example.com search for the spam terms the report listed.

6. Harden

  • Updates on: core minor releases automatically, plugins and themes weekly, no closed or abandoned plugin.
  • Two-factor authentication for every administrator and a unique login name; WordPress login security has the setup.
  • Least privilege: editors do not need administrator, and contractors' accounts go when the work ends.
  • File permissions: directories 755, files 644, wp-config.php 600 or 640.
  • File editing off, so a stolen admin login cannot write PHP through the dashboard: define('DISALLOW_FILE_EDIT', true); in wp-config.php.
  • No PHP execution in uploads:
Apache
# wp-content/uploads/.htaccess (Apache 2.4)
<FilesMatch "\.(php|phtml|php5|phar)$">
    Require all denied
</FilesMatch>
nginx
# inside the server block, before the PHP location
location ~* /wp-content/uploads/.*\.(php|phtml|php5|phar)$ {
    deny all;
}
  • A web application firewall in front, from the host or a CDN.
  • Backups off the server, restored once to prove they work; the WordPress backup guide has the plan.

7. Request review and clean the index

In Search Console, Security issues → Request review, with two sentences on what was found and what was done. If Chrome shows a red warning, the same review clears Safe Browsing; flagged by Google Safe Browsing has the details and review times. Spam URLs that no longer exist should answer 404 or 410, not redirect to the home page. Use Removals for the worst ones while the recrawl catches up.

8. Monitor for re-infection

Re-run the hacked site checker on the same four pages after a week and after a month. In the access log, watch for POSTs to unexpected PHP files and requests for files you deleted. The log file analyser reads the log in your browser: a crawler still requesting the deleted spam folder is normal for a while, a 200 for it is not.

When to pay someone

Do the cleanup yourself when the site is a brochure or a blog and you have a clean backup or the patience to reinstall. Pay a specialist when the site is a shop or holds customer data, when the infection returns after a full reinstall, or when nobody available can read PHP.

Verify

  • The hacked site checker on four pages: "Visitors and Googlebot see the same page text", "No spam links and no links served only to bots", "No redirect that fires only for Googlebot or Google visitors".
  • wp core verify-checksums and wp plugin verify-checksums --all report no modified files, and find wp-content/uploads -name '*.php' prints nothing.
  • wp user list --role=administrator lists only people you know; wp cron event list shows only hooks you can name.
  • Search Console → Security issues reads "No issues detected" after the review.

Common mistakes

  • Cleaning without finding the entry. The files come back within days.
  • Restoring the newest backup. It is often infected too. Restore an older one only if the log dates the compromise.
  • Deleting the evidence first. Then you cannot find out how they got in, or what was exposed.
  • Changing the WordPress passwords only. The database password, SFTP login and API keys are what the attacker usually kept.
  • Redirecting removed spam URLs to the home page. Google treats it as a soft 404 and keeps them around longer.

Questions people ask

How do I remove malware from WordPress without a plugin?

Reinstall rather than scan. Download fresh WordPress core with WP-CLI, reinstall every plugin and theme from wordpress.org or the vendor, delete any PHP file in the uploads folder, and review wp-config.php, .htaccess and mu-plugins by hand. Then search the database for injected scripts and unknown cron jobs. Checksums from wordpress.org confirm the result.

Why does my WordPress site keep getting hacked?

Because the way in is still open, or a backdoor survived the cleanup. Common causes are a vulnerable plugin that was not updated or replaced, a password that was not changed (often SFTP or the database), a PHP file left in uploads, a malicious cron job, or another infected site on the same hosting account. Find the entry in the access log before cleaning again.

Should I restore a backup or clean a hacked WordPress site?

Restore only if you can date the break-in from the logs and have a backup from before it, then update everything before going live. Otherwise clean by reinstalling, because recent backups usually contain the same backdoor. Either way, change every password and close the entry first, or the restored site is reinfected the same way.

Do WordPress security plugins find all malware?

No. A scanner plugin runs inside the same WordPress install the attacker controls, so modified code can hide files from it, and it only knows the malware patterns in its signature list. Use it as one signal. Checksums compared from WP-CLI, a server-side scan from your host and an outside check of what Google sees catch what it misses.

Check your site before and after Check