wp2shell: patch WordPress, then check who got there first
Pre-authentication RCE in WordPress core: no plugin, no login, stock install. Update to 7.0.2 / 6.9.5 / 6.8.6 immediately, then hunt for whoever got there before the patch.
01 The Big PictureFor leadership · no jargon
Okay, so this one is a five-alarm fire. wp2shell is a pre-authentication remote code execution chain in WordPress core, disclosed on Friday 17 July 2026: no vulnerable plugin, no dodgy theme, no login. Anyone who can reach a stock install on an affected version can walk it all the way down, from anonymous visitor to new admin account to uploaded plugin: WordPress to shell, hence the name. Site, database and server are all gone at that point. WordPress enabled forced auto-updates for affected versions (a step it treats as a last resort), and public exploit code was being sprayed at anything reachable within hours.
Scale is why this one hurts: WordPress runs a genuinely silly fraction of the web, and attackers worked the weekend hard. Honeypots saw tens of thousands of attempts and backdoor admin accounts created by the hundred. If an affected site was online and unpatched any time from Friday onwards, assume it was probed, and quite possibly popped. Yikes.
The job is two parts: get onto a fixed version (7.0.2, 6.9.5 or 6.8.6), then check whether someone got there before the patch, because patching does not evict an attacker who already has a shell. One wrinkle before we start: the bug was found by an AI agent in about ten hours for roughly $25 of compute, which is why this one moved so fast, and why the next one will too.
02 Technical BreakdownFor engineers
Right, the nitty-gritty. wp2shell chains two CVEs. The first, CVE-2026-63030, is a route-confusion flaw in the REST batch endpoint, /wp-json/batch/v1, which lets a visitor bundle several API calls into a single request. Think of the batch handler as a bouncer checking a whole group's IDs in one go. It runs two loops: the first validates every subrequest and records matched route handlers in two parallel arrays, the second executes them. If a subrequest fails early (say, a malformed path), the code appends the error to the validation array but continues before appending to the matches array. The two arrays are now off by one, the bouncer has lost track of which ID belongs to which guest. Every later subrequest gets executed using the handler matched for the request after it. Net effect: you can validate one request and execute it as another, which bypasses parameter sanitisation on every batch-enabled endpoint.
The second, CVE-2026-60137, is a SQL injection in WP_Query's author__not_in handling (the code behind the posts endpoint's author_exclude parameter). Pass an array and every element is forced through absint, pass a scalar string and it is interpolated straight into the SQL, untouched. Normally the API's parameter validation only lets the array form through, but the batch desync lets us borrow a different route's validation and skip that check entirely. Two neat tricks make it weaponisable: the batch API rejects GET subrequests, but the method check is itself parameter validation, so a recursively nested batch request desyncs a second time to smuggle the GET in; and a UNION-based injection then lets the attacker return fully forged wp_posts rows.
From a read-only SQLi to code execution is where the chain gets properly absurd, and credit where due, the model worked this bit out faster than the human could untangle it. The short version: forged post rows poison WordPress's in-memory post cache, the oEmbed feature can be prodded into persisting those fake posts as real database rows, a cycle-detection gadget in post-parent handling forces a database write that doesn't clobber the fields the attacker controls; a forged customize_changeset row with user_id 1 makes WordPress briefly assume the administrator's identity; and a fake post with status parse and type request fires the parse_request hook, replaying the original batch request from scratch, this time as admin. One subrequest in that batch creates a new administrator account. Log in, upload a plugin zip, shell. Full details, and they're worth reading, are in Searchlight's writeup (linked in References).
03 Affected
Full RCE chain (both CVEs): WordPress 6.9.0 through 6.9.4, and 7.0.0 through 7.0.1. The 7.1 Beta 1 build is also vulnerable; Beta 2 carries the fixes.
SQL injection only (CVE-2026-60137): WordPress 6.8.0 through 6.8.5. The route-confusion bug doesn't bite on this branch, so it stops at database reads rather than shell. Still worth fixing today, just not five-alarm. Anything older than 6.8 is not affected.
Fixed releases: 6.8.6, 6.9.5 and 7.0.2, all shipped 17 July 2026. Preconditions: none. No plugin, no theme, no account, no user interaction. If an affected version was reachable from the internet at any point from Friday onwards, treat it as having been probed.
04 Detection
Before changing anything, let's see what we're dealing with. First, what are you running?
wp core version 7.0.2, 6.9.5 or 6.8.6 (or later) and you're patched. Anything in 6.9.0-6.9.4 or 7.0.0-7.0.1 is fully exposed; 6.8.0-6.8.5 carries the SQLi. No WP-CLI? The version is in the dashboard footer, or in wp-includes/version.php on the server.
grep -Ei "batch/v1" /var/log/nginx/access.log* | grep -Ei "author_?_?not_?_?in|author_exclude|UNION|SELECT|SLEEP\(|SUBSTRING" The high-signal hunt: batch-endpoint traffic carrying SQLi markers. Swap in your Apache log path as needed. The exploit POSTs nested batch requests, so plain POSTs to /wp-json/batch/v1 or ?rest_route=/batch/v1 from the weekend onwards are worth a look even without SQL keywords.
wp user list --role=administrator --fields=ID,user_login,user_email,user_registeredRogue admin accounts are the chain's calling card; WatchTowr counted over a hundred created by different actors in their honeypots alone. Any account you don't recognise, created since Friday, is a compromised site until proven otherwise.
wp core verify-checksumsCompares core files against WordPress.org's official hashes. Any "doesn't verify" line means a file was tampered with; investigate it before you do anything else.
find /var/www/html -name "*.php" -mtime -7 -print
find /var/www/html/wp-content/plugins -iname "wp2shell*" -print Recently written PHP files, plus public-PoC artefacts (plugin folders or zips named wp2shell-<random>). Treat PoC naming as a lead, not proof; real attackers rename things.
Worth a mention: Searchlight hosts a free external checker at wp2shell.com that probes a live instance for you. It has been up and down under load, so retry if it times out.
05 Remediation
01 Update WordPress
The only actual fix. The forced auto-update may already have landed, check rather than assume, because hosts, firewalls and disabled auto-updates all break that push.
wp core updateLands you on the fixed release for your branch: 7.0.2, 6.9.5 or 6.8.6. Via the dashboard it's Updates, then Update Now. Back up first if you're the cautious sort (be the cautious sort).
02 If you can't patch this minute: block the batch route
A stopgap, not a cure, but it closes the door while you organise the update. Block BOTH forms of the route at your WAF or CDN; the pretty permalink form alone leaves the query-string form wide open:
/wp-json/batch/v1
?rest_route=/batch/v1Block POSTs (or all methods) to both. Cloudflare deployed managed rules covering both CVEs on all plans, including free, for proxied sites, so if you're behind their proxy you already have a filter in place. Still patch: a WAF rule is a filter, not a fix.
No WAF? A must-use plugin does the same job from inside WordPress. Drop this in wp-content/mu-plugins/block-batch.php:
<?php
/* Emergency wp2shell mitigation: block anonymous access to the REST batch route.
Temporary. Remove after updating to 7.0.2 / 6.9.5 / 6.8.6. */
add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
if ( strpos( $request->get_route(), '/batch/' ) !== false && ! is_user_logged_in() ) {
return new WP_Error(
'rest_forbidden',
'Batch endpoint temporarily disabled (wp2shell mitigation).',
array( 'status' => 403 )
);
}
return $result;
}, 0, 3 );mu-plugins load automatically and can't be deactivated from the dashboard, which is exactly what you want from an emergency control. Delete the file once patched.
03 If you find evidence of compromise
Patching does not evict an attacker who already has a shell. If the detection steps above turned up rogue admins, tampered core files or mystery plugins: take the site offline, rotate everything (database password, wp-config.php salts, admin passwords, API keys), remove the rogue accounts and plugins, and restore from a known-clean pre-Friday backup if you have one. Then patch before reconnecting. If this sounds like your weekend plans changing, yes, sorry. When in doubt, get an incident-response professional in.
06 Verification
wp core versionExpect 7.0.2, 6.9.5, 6.8.6 or later. The dashboard footer should agree.
wp core verify-checksumsExpect silence: every core file matching the official hashes.
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://yoursite.example/wp-json/batch/v1Only relevant while the emergency mitigation stands: expect 403 from the WAF rule or mu-plugin. Once you're patched and the mitigation is removed, a 4xx from WordPress itself is normal for an unauthenticated POST with no valid payload.
Final external check: run your site through wp2shell.com and expect a "not vulnerable" verdict.
07 Rollback
Rolling back the security update itself: don't. It reopens a known-exploited pre-auth RCE on the most-attacked CMS on the internet this month. If the update broke a theme or plugin, fix forward, or hold the line with the batch-route block above while you sort it out.
The realistic rollback is the emergency mitigation. Blocking the batch route breaks legitimate features that use it (the block editor and various plugins batch their API calls), so once you're on a fixed release, remove it:
rm wp-content/mu-plugins/block-batch.phpOr disable the WAF rule. Keep the patch; lose the crutch.
08 References
- Searchlight Cyber: the full wp2shell chain, by the discoverer
- wp2shell.com: Searchlight's hosted vulnerability checker
- NVD: CVE-2026-63030
- The Register: in-the-wild exploitation reporting
If I've got anything wrong here, or you've hit an edge case I haven't covered, do say: corrections and questions are always welcome :)
Ship the fix. Prove it’s closed.
If this runbook doesn’t fit your stack, send the CVE — an operator writes a verified path, usually within a business day. Or contribute your own and get co-authored credit.