← Back to Home

WordPress 7.1 Hijack Cleanup: WPScan 4.1 and WP-CLI

WordPressWP-CLISecurityIncident ResponseHardening

A B2B content site I maintain failed its summer audit in the loudest possible way. Google Search Console emailed the owner a malware warning, and the search result for the homepage grew an extra line pointing at a betting site. I opened the homepage in my desktop browser and saw nothing wrong. Then I switched the user agent to a mobile string, and to Googlebot, and the redirect appeared instantly. This is a cloaked redirect, and in 2026 it is still one of the most common ways a WordPress site gets hijacked: real visitors see a clean page, while search engines and crawlers get served the spam.

Before anything else, a disclosure: this article contains Amazon affiliate links. If you buy through them I earn a small commission, and the price you pay does not change. Every tool below is open source or free tier, the incident is one I actually handled, and the domain and hostname have been redacted. The article answers exactly one question: when the alert lands, what order do you do things in. Get the order wrong and you will destroy the evidence that points at the way in.

⏳ TL;DR

Prerequisites: versions, tools, and a three-minute backup

I ran this on Ubuntu 24.04 LTS with a self-hosted WordPress install, meaning full SSH access rather than a managed host. Versions below are current as of writing:

ComponentVersionNotes
WordPress7.1 ("Mary Lou", released 2026-08-19)Database version db_version 61833; 7.1 is the only actively maintained branch
WP-CLI2.12.xConfirm with `wp --info`; since 2.12 `wp core verify-checksums` accepts `--exclude=`
WPScan4.1 (4.0.0 shipped 2026-05-20)Requires Ruby 3.3 or newer; the free API token allows 25 requests per day
PHP8.2 or newerOfficially supported by WordPress 7.1
HostAny VPS with SSH2GB RAM or more recommended

The first action in an incident is not a scan. It is a backup, and it has to land off the server:

cd /var/www/html
wp db export /root/incident/wp-db-$(date +%F).sql
tar czf /root/incident/wp-files-$(date +%F).tar.gz \
  --exclude='wp-content/cache' --exclude='wp-content/uploads/*' .

Excluding wp-content/uploads is deliberate. That directory is often several gigabytes, and it almost never hides PHP backdoors unless the site is configured to execute uploaded files. The places that matter for evidence are wp-content/plugins, wp-content/themes, wp-content/mu-plugins, and every PHP file in the web root.

If Google has already flagged the site, do not click "Request Review" yet. You get a limited number of attempts, and that is a topic for the last section.

Step 1: Capture proof of a crawler-only redirect

You cannot diagnose a cloaked redirect by looking at it. Request the same path with different user agents and save both the headers and the first few hundred bytes:

UA_MOBILE="Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36"
UA_BOT="Googlebot/2.1 (+http://www.google.com/bot.html)"

curl -s -A "$UA_MOBILE" -D /root/incident/h-mobile.txt https://example.com/ -o /root/incident/b-mobile.html
curl -s -A "$UA_BOT"    -D /root/incident/h-bot.txt    https://example.com/ -o /root/incident/b-bot.html
curl -s                 -D /root/incident/h-plain.txt  https://example.com/ -o /root/incident/b-plain.html

diff <(head -c 400 /root/incident/b-plain.html) <(head -c 400 /root/incident/b-bot.html)

If the plain response is clean and the Googlebot response contains a location.href assignment or a third-party domain, you are looking at a cloak. Preserve two more things while you are here: a hash of wp-config.php, and a timestamped list of every PHP file on the site.

find /var/www/html -type f -name "*.php" -newermt "-45 days" \
  -printf "%TY-%Tm-%Td %TH:%TM  %p\n" | sort > /root/incident/recent-php.txt
wc -l /root/incident/recent-php.txt

The first time I ran that command it returned 37 lines. Thirty-four were routine plugin auto-updates, and the remaining three were the way in. That file became the map for everything that followed.

Step 2: Check core and plugin integrity with WP-CLI

verify-checksums compares local files byte for byte against the official checksums on api.wordpress.org. It is the single best value command in an incident:

wp core verify-checksums

Modified files are listed directly:

Warning: File doesn't verify against checksum: wp-includes/class-wp-http.php
Warning: File doesn't verify against checksum: wp-admin/includes/plugin.php
Error: WordPress installation doesn't verify against checksums.

Do the same for plugins and themes, and always pass --all:

wp plugin verify-checksums --all
wp theme verify-checksums --all

On the machine I tested, core reported two modified files and plugins reported one. Here is the counterintuitive part: a passing checksum does not mean the site is clean. It only proves the official files were not touched. It says nothing about PHP in the uploads directory, custom plugins, content injected into the database, or a freshly created administrator account. It is the first step of entry-point hunting, never the conclusion.

Step 3: Find the vulnerable plugin with WPScan 4.1

WPScan 4.0, released 2026-05-20, changed behavior in a way that trips people up: **the default scan no longer enumerates plugins.** The old wpscan --url example.com would happily hammer plugin detection and burn your API quota. In 4.x the default only checks the WordPress version, the active theme, and baseline security findings. You have to ask for anything more with -e.

Install it (Ruby 3.3 or newer; 4.x also supports Ruby 4.0):

gem install wpscan                    # native install
# or the lowest-friction container route, no local Ruby needed
docker pull wpscanteam/wpscan:latest

Register at wpscan.com/api for a free token, capped at 25 requests per day, then scan with application-password authentication. This is the feature that makes 4.x worth upgrading for: it queries the REST API for the authoritative list of every installed plugin, including inactive ones, instead of guessing from page fingerprints.

export WPSCAN_API_TOKEN="your-token"

wpscan --url https://example.com \
  --api-token "$WPSCAN_API_TOKEN" \
  --wp-auth admin:'abcd efgh ijkl mnop qrst uvwx' \
  -e vp,vt,cb \
  --plugins-detection passive \
  --format jsonl | jq .

Generate the application password under Users, then Profile, then Application Passwords. It looks like abcd efgh ijkl mnop qrst uvwx. The flag -e vp reports only plugins with known vulnerabilities, -e vt covers themes, and -e cb looks for configuration backups. One warning worth repeating: if WPScan finds a .wp-config.php.bak sitting in the web root, that is a critical finding on its own, because anyone who downloads it gets your database credentials.

The output on this site flagged a page-builder plugin that had not been updated in three years. Its unauthenticated arbitrary file upload vulnerability was disclosed in November 2023 and patched in February 2024. The site had been leaving the back door unlocked since early 2024. That is why I am not comfortable leaving "keep your plugins updated" as an exercise for the reader.

Step 4: Remove the backdoors, in this order

Confirm the entry point before touching anything. Then clean in the sequence below, re-running the three curl requests from Step 1 after each stage.

First, overwrite core with official files. Far more reliable than hand-comparing:

wp core download --force --version=7.1 --locale=en_US

**Second, inspect mu-plugins.** This is the most popular hiding place, because must-use plugins load unconditionally and are invisible in the normal admin plugin list:

ls -la wp-content/mu-plugins/
wp plugin list --status=must-use

**Third, audit administrator accounts.** Mine had gained a wp_support_admin whose registration date matched the day the PHP files changed:

wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp user delete 42 --reassign=1

Fourth, rotate the authentication keys. This invalidates every existing session and is the most direct way to take the attacker's keys away:

wp config shuffle-salts

Fifth, clear injected cron events and payloads:

wp cron event list --fields=hook,next_run_relative
wp cron event delete wp_evil_sync
wp db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%

Finally, read through wp-config.php for any constant beyond the database four, especially an auto_prepend_file line or an include pointing into the uploads directory. That pattern is the most persistent kind of reinfection.

💣 Four real errors and how I fixed them

Error 1: This does not seem to be a WordPress installation

Error: This does not seem to be a WordPress installation.
The used path is: /root
Pass --path=`path/to/wordpress` or run `wp core download`.

**Cause**: WP-CLI cannot find wp-load.php in the current directory. Running commands as root from /root during an incident is the classic way to hit this, and moving wp-config.php one level up causes it too.

Fix: pass the path explicitly and confirm the install is recognized:

wp --path=/var/www/html core is-installed && echo OK

Error 2: No WPScan API Token given

[!] No WPScan API Token given, as a result vulnerability data has not been output.
[!] You can get a free API token with 25 daily requests by registering at https://wpscan.com/register

**Cause**: three possibilities. The token was never passed, the export happened in a different shell session, or a sudo prefix dropped the environment variable. sudo wpscan does not inherit the WPSCAN_API_TOKEN from your shell.

Fix: passing it explicitly is the most reliable, since it is immune to shell state:

wpscan --url https://example.com --api-token "your-token" -e vp
# or keep the environment with sudo -E
sudo -E wpscan --url https://example.com -e vp

Error 3: Scan Aborted, the url supplied does not seem to be running WordPress

[!] Scan Aborted: The url supplied 'https://example.com/' does not seem to be running WordPress.

Cause: one of two extremes. Either a CDN or WAF intercepted the scanner and returned a challenge page, or the cloak logic treated the WPScan user agent as a crawler and served it the redirect page, so WPScan concluded this was not WordPress at all. The second case is what happened here, and it is genuinely confusing.

Fix: establish which version of the site is the real one, then work around the disguise:

curl -sI https://example.com/ | head -3          # is a WAF challenge being returned?
wpscan --url https://example.com -e vp \
  --random-user-agent --disable-tls-checks --follow-redirect

If a WAF is genuinely in the way, whitelisting the scanner IP is much safer than disabling the WAF. Reserve --disable-tls-checks for test environments with self-signed certificates.

Error 4: Could not update the wp-config.php file

Error: Could not update the wp-config.php file.

**Cause**: wp config shuffle-salts needs to write wp-config.php, and in production that file is usually root:www-data at 640 or 600. Running WP-CLI as an ordinary user cannot write it, and a DISALLOW_FILE_MODS constant in the file will refuse the change outright.

Fix: run as the file owner, or elevate briefly and lock the permissions back down immediately:

sudo -u www-data wp config shuffle-salts --path=/var/www/html
# manual route: back up, change, then lock it again
cp wp-config.php /root/incident/wp-config.php.bak-of-incident
chmod 640 wp-config.php && sudo -u www-data wp config shuffle-salts
chmod 600 wp-config.php

Be aware that shuffle-salts logs every user out. Run it off-peak and tell your colleagues first.

Step 5: Harden so it does not happen the same way twice

Cleaning stops the bleeding; hardening stops the recurrence. Ordered by return on effort, these are the changes that close the specific hole this attack came through.

1) Enforce 2FA by role. WordPress core still ships no built-in two-factor authentication, so this needs a plugin. The free options that matter in 2026 are WP 2FA by Melapress (setup wizard, per-role enforcement, grace periods), Two-Factor (maintained by WordPress core contributors, deliberately minimal, TOTP plus email and U2F), Wordfence Login Security (login protection only), and Solid Security (formerly iThemes Security, part of a broader hardening suite). Install exactly one. Stacking two 2FA plugins causes login filter conflicts, which is the most common self-inflicted lockout in this scenario. Administrators should get no grace period.

**2) Remove the admin file editor.** Add this to wp-config.php:

define('DISALLOW_FILE_EDIT', true);
// stricter: also block plugin and theme installs/updates, run them through WP-CLI instead
// define('DISALLOW_FILE_MODS', true);

**3) Tighten file permissions** (directories 755, files 644, wp-config.php 600):

find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod 600 /var/www/html/wp-config.php

4) Trim entry points you do not use. If XML-RPC is not in use, deny it at the web server layer. Keep application passwords only where they are needed and revoke them when the job is done. Moving the login URL only stops automated credential stuffing, so never treat it as the main defense.

**5) Update strategy.** Back up, update a small batch, then verify. wp plugin update --all is convenient, but on a compromised site always clean before you update. Reverse that order and you may overwrite files an attacker had already modified, burying the evidence under a clean-looking version number.

Getting Google to lift the warning

Once the site is clean, do not immediately hit "Request Review". Google limits how many times a site can be re-reviewed, and a failed first attempt pushes the next window further out. Do these three things first:

1. Confirm the flag has cleared in the Search Console Security Issues panel.

2. Re-run the multi-user-agent curl check on the server and confirm the cloak is gone.

3. Write a short remediation note: what changed, when, and which files were removed. You will want it during review.

Only then submit for review. On this site, the warning cleared three days after submission. That number depends heavily on site history and the type of flag, so I would not treat it as your expected timeline.

Gear for the incident desk (affiliate links)

Incident windows tend to land at 2am, and how pleasant it is to read logs, compare user agents, and study diffs depends a lot on the desk itself. These two are what I actually used during this incident, and both are Amazon affiliate links:

Dell UltraSharp U2723QE 4K USB-C monitor — IPS Black panel, 90W USB-C single-cable setup, built-in USB 3.2 hub and RJ45, so one cable covers display, network, and power. Wide enough for four terminals on the left and a side-by-side diff on the right. Pricing moves with promotions; roughly $500 to $650 as of writing, so check the product page. Real caveats: it ships bright by default, white uniformity varies unit to unit, and spinning up a mechanical drive through the hub can brown out without enough power.

👉 Check the Dell U2723QE on Amazon >>

BenQ ScreenBar Halo 2 monitor light bar — three-zone backlight, stepless color temperature, and auto-off when you leave, which keeps ambient light low during late-night log reading without using desk space. Official pricing is around $179, with frequent promotions. Real caveats: the auto-dimming mode locks color temperature near 4000K with no override, and older monitors whose USB port only supplies 5V/1A will make it flicker, so it needs its own 5V/3A supply.

👉 Check the BenQ ScreenBar Halo 2 on Amazon >>

All prices above are the public list figures as of writing. Amazon changes prices constantly, so please confirm before ordering.

FAQ

Q: Does WordPress 7.1 include 2FA?

A: No. Core still has no built-in two-factor authentication, so it requires a plugin. WP 2FA, Two-Factor, Wordfence Login Security, and Solid Security are the common free choices. Install one to avoid login filter conflicts.

Q: Is the free WPScan tier enough?

A: Yes for incident work. The free tier allows 25 requests per day and a single scan with plugin enumeration consumes several. When quota is tight, -e vp is the cheapest meaningful scan. Paid tiers only matter once you manage a large fleet of sites.

Q: Do I have to rotate the salts after cleanup?

A: You should. An attacker may already have the authentication keys from wp-config.php and could forge login cookies with them. wp config shuffle-salts invalidates every session, which is the most direct way to take those keys back. The tradeoff is that everyone has to log in again.

Q: How long does it take to recover from a Google malware flag?

A: It depends on the flag type and site history, with no fixed number. This one cleared three days after submission. The important part is cleaning thoroughly before you submit, because repeated requests extend the review interval.

Q: If core checksums pass, is the site clean?

A: No. wp core verify-checksums only proves the official core files were untouched. It cannot see PHP in the uploads directory, custom plugins, injected database content, or a newly created administrator account. Read it alongside the WPScan scan, the user audit, and the mu-plugins check.

Wrap-up

The real lesson from this incident was not "keep your plugins updated". It was about order. The first thing I did after the alert was not delete files, it was preserve three pieces of evidence: the user-agent comparison, the file modification times, and the database export. Those three anomalous lines in recent-php.txt identified the abandoned plugin within 30 minutes. Had I run wp core download --force first, the trail would have been gone.

If you are working through a similar cleanup, two follow-ups are worth your time. Rebuilding internal links and SEO metadata across 600 posts with WP-CLI covers the routine automation side, and WordPress 7.1 with Redis object caching covers the performance layer, which is usually where session invalidation after hardening starts causing surprises. For teams automating ops further, letting AI agents safely run a content site with the MCP Adapter is the natural next read: the more automation you add, the earlier you have to manage the exposure it creates.

👉 Join MiniMax Token Plan: AI coding acceleration for businesses

👉 Join Xiaomi MiMo Platform: Leading AI model platform with cost-effective inference

👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation

📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews

🔗 Recommended Tools

These are carefully selected tools. Using our affiliate links supports us to keep producing quality content:

☁️ DigitalOcean Cloud ⚡ Vultr VPS ⭐ MiniMax Token Plan 🤖 QoderWork CN (Refer & Earn) ☁️ Aliyun AI Products 📚 WordPress Books 🔍 WordPress SEO Books 🌐 Web Hosting Books 🐳 Docker Books 🐧 Linux Books 🐍 Python Books 💰 Affiliate Marketing 💵 Passive Income Books 🖥️ Server Books ☁️ Cloud Computing Books 🚀 DevOps Books 🤖 Xiaomi MiMo Platform
← Back to Home