A practical guide for site owners, IT leads and agencies: what wp2shell is, how the attack chain works, and the exact sequence to close it and verify you were not already hit.
Most WordPress security incidents we get called into start with a bad plugin. An abandoned slider, a form builder three years behind on updates, a theme bought once and never touched again. That is the usual story, and it makes the conversation easy: remove the weak component, patch, harden, move on.
wp2shell is not that story. This one is in WordPress Core itself. No plugins required. No login required. No unusual configuration required. A stock install, reachable from the internet, running an affected version, is enough.
If you run WordPress on a version in the affected range and it has been publicly reachable since mid July 2026, the useful question is no longer "am I vulnerable". It is "was I already visited". This post covers both.
The short version
| Item | Detail |
|---|---|
| Name | wp2shell |
| CVEs | CVE-2026-63030 (REST batch route confusion), CVE-2026-60137 (SQL injection) |
| Combined severity | CVSS 9.8 of 10 |
| Impact | Unauthenticated remote code execution, full site takeover |
| Affected | WordPress 6.9.0 to 6.9.4, and 7.0.0 to 7.0.1 |
| Fixed in | 6.9.5 (6.9 branch), 7.0.2 (7.0 branch). A 6.8.6 release was also issued for the 6.8 branch |
| Disclosed | 17 July 2026 |
| CISA KEV listing | 21 July 2026, before any public exploit existed |
| Public proof of concept | 22 July 2026 |
| Exploitation status | Active and widespread |
| Discovered by | Adam Kues, Assetnote / Searchlight Cyber, reported via the WordPress HackerOne programme |
WordPress.org considered this serious enough to force automatic updates on affected installations. That is not a routine step. Treat it as the signal it is.
Official checker from the research team: wp2shell.com
What wp2shell actually is
wp2shell is not a single bug. It is two, and the interesting part is the relationship between them. On its own, neither one gives an attacker code execution. Chained, they take an anonymous HTTP request all the way to a shell on your server.
CVE-2026-63030, the batch endpoint confusion. WordPress exposes a REST endpoint that lets a client bundle several API calls into one HTTP request, reachable at /wp-json/batch/v1 and equivalently at /?rest_route=/batch/v1. The flaw is a validation and dispatch mismatch. When a sub-request path is crafted with a triple forward slash prefix, the validation layer reads the path one way and the execution layer reads it another. Validation approves the request. Execution then hands it to the wrong handler, one that does not enforce the authentication the original route required.
The observable trace of this in your logs is an HTTP 207 Multi-Status response from the batch endpoint. Remember that. It is the single most useful string you will grep for today.
CVE-2026-60137, the SQL injection. A SQL injection in WordPress's post query layer, reachable through the author__not_in parameter. Under normal circumstances this parameter sits behind authentication and capability checks. The batch desync from the first bug delivers a crafted sub-request to it without those checks ever firing, which allows UNION based extraction of data out of the WordPress database.
The bridge to administrator. From database read access, the published attack chain abuses two legitimate WordPress mechanisms as a write primitive: the oEmbed cache (where WordPress stores generated previews of embedded links) and a customizer changeset (a staged, unapplied theme configuration record). Both write to the database. Used together, they insert a crafted value into the options table that results in a working administrator account.
The payload. With administrator rights, an attacker can install plugins. That is not a vulnerability, that is WordPress working as designed, which is precisely why the admin account is the crown jewel. The observed chain uploads a plugin dropper, which writes PHP webshells at randomised paths inside wp-content, protected by per run tokens. Reported variants include a minimal shell that returns HTTP 404 to stay boring in access logs, and custom REST backdoors that accept base64 encoded commands.
Total elapsed time from first exploit request to administrator access, once the automated chain starts: roughly two minutes.
We are deliberately not publishing request payloads or exploit code here. Nothing in this section helps an attacker who does not already have the public tooling. All of it helps you defend.
The detail most write-ups buried, and it changes your checklist
Bitdefender's MDR team investigated a real compromise and found something not present in the earlier public reporting: the attack ran three times against that server before it fully succeeded.
The first run created a rogue administrator account and then stalled. No plugin, no webshell. The second run did the same thing: another rogue admin, another stall. The third run went the distance, installed plugin droppers, dropped webshells, and achieved code execution.
The reason failed attempts leave residue is structural. The tool's cleanup routine only executes on full success. A partial run abandons the administrator account it created.
This has a direct consequence for how you check your sites. A rogue administrator account is your first indicator of compromise, not a webshell. An unexplained admin user with no malicious files anywhere on disk is not a false positive and not a near miss to shrug off. It means the chain ran against your install, it got at least as far as account creation, and whoever ran it may come back.
The public tooling prefixes those accounts with w2s_ and uses fun- and fun-proof- in its file paths. Those are default strings in one tool. Any attacker can change them in thirty seconds. Search for the concept, not the string: an administrator account nobody authorised, a plugin directory nobody installed, a PHP file at a random path that belongs to nothing.
Fixing it: the sequence
Step 1. Establish what you actually have
This step defeats more organisations than the vulnerability does. You cannot patch an inventory you do not have. If you cannot answer "which internet facing hosts run WordPress, and on what version" within ten minutes, that gap is the real finding here.
Per site:
bash
wp core version
Or without WP-CLI, read the version from wp-includes/version.php. Do not rely on the readme file, and do not rely on public fingerprinting if you have shell access.
If automatic updates were enabled, verify that they ran and applied. Forced auto updates reached a lot of installations. They did not reach installations with updates disabled by constant, filtered off by a plugin, blocked by file permissions, or sitting on a host that silently fails them. Assume nothing, confirm per site.
Step 2. Patch
Update to 6.9.5 or 7.0.2, matching your branch. If you are on the 6.8 branch, take 6.8.6 anyway, since staying on a branch that is one release behind on security is not a position worth defending.
bash
wp core update wp core version
Update, then confirm the version changed. A patch you did not verify is a patch you did not apply.
Step 3. If you cannot patch in the next hour, block the endpoint
These are temporary controls to buy time, not alternatives to updating. All of them can break legitimate integrations that use the batch API, including some page builders, headless front ends and mobile apps, so test and communicate before you deploy them broadly.
Option A, at the edge (preferred). Block or restrict anonymous requests to both:
- the path /wp-json/batch/v1
- the query parameter rest_route=/batch/v1
Both patterns matter. Blocking only the pretty permalink route leaves the query string route wide open. Cloudflare rolled out managed rules covering this, and most WAF vendors have followed, but verify your ruleset is enabled rather than assuming coverage.
Option B, in WordPress. A tiny must-use plugin that rejects unauthenticated batch dispatch. This is our own implementation of the approach the Searchlight Cyber team published on wp2shell.com. Drop it in wp-content/mu-plugins/ so it loads unconditionally and cannot be deactivated from the admin panel:
php
<?php
/**
* Plugin Name: Require Auth For REST Batch
* Description: Rejects anonymous requests to the WordPress REST batch endpoint. Temporary mitigation for wp2shell (CVE-2026-63030). Remove after updating Core.
* Version: 1.0.0
* License: GPL-2.0-or-later
*/
defined( 'ABSPATH' ) || exit;
add_filter(
'rest_pre_dispatch',
function ( $result, $server, $request ) {
$route = strtolower( untrailingslashit( $request->get_route() ) );
if ( '/batch/v1' !== $route || is_user_logged_in() ) {
return $result;
}
return new WP_Error(
'rest_batch_auth_required',
'Authentication is required for batch requests.',
array( 'status' => 401 )
);
},
-1000,
3
);
Option C, blunt instrument. A plugin that disables the REST API for unauthenticated users entirely. Easiest to deploy, most likely to break something. Reasonable on a brochure site, risky on anything with integrations.
Remove all three once Core is updated. Temporary mitigations that outlive their purpose become the undocumented reason something mysteriously fails eighteen months later.
Step 4. Assume you need to check, then check properly
Patching stops the next attempt. It does nothing about a shell already sitting on disk. Any site that was internet facing and on an affected version between 17 July and the moment you patched needs a compromise assessment, not an assumption.
Administrator accounts first:
bash
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
Reconcile every single row against a person you can name. Sort by registration date and look hard at anything created on or after 17 July 2026.
Files that appeared recently under wp-content:
bash
find wp-content -type f -name "*.php" -newermt "2026-07-15" -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort
Then look specifically where the reported campaigns put things:
bash
ls -la wp-content/plugins/ find wp-content/cache wp-content/uploads -type f -name "*.php" 2>/dev/null ls -la wp-content/mu-plugins/ 2>/dev/null
There is never a legitimate reason for a PHP file to live in wp-content/uploads or in a cache directory. SANS ISC documented shells being written to cache paths specifically to sit outside the plugin directory everyone inspects.
Core file integrity:
bash
wp core verify-checksums
Scheduled tasks, a favourite persistence spot:
bash
wp cron event list
Access logs. This is where you find out whether you were merely scanned or actually exploited:
bash
grep -iE "batch/v1|rest_route=/batch/v1" access.log | grep -E "\" 207 " grep -E "batch/v1.*///" access.log grep -iE "wp2shell|rezwp2shell" access.log
A 207 response from the batch endpoint is the high fidelity signal. Nested batch requests with triple slash path segments, and batch sub-requests chaining into /wp/v2/posts and /wp/v2/block-renderer/, are the durable behavioural patterns. User agent strings containing wp2shell are a supporting signal only, since they are trivially edited.
Step 5. If you find something, clean properly or rebuild
Once unauthenticated RCE has happened, you are not dealing with a vulnerability any more. You are dealing with an intrusion, and the person on the other end had the same privileges your deployment pipeline has.
Our honest recommendation on any site where a webshell executed: rebuild rather than clean. Fresh host, fresh Core, plugins and themes reinstalled from their original sources, database imported after inspection, custom code reviewed by eye. Cleaning a compromised WordPress install means proving a negative across tens of thousands of files. Rebuilding means proving a positive across a known set. The second is cheaper and it is the one you can actually stand behind in front of a client.
If you clean in place, the minimum sequence:
- Take the site offline or behind maintenance mode. Snapshot everything first, files and database, for forensics. Do not skip this. It is your only evidence.
- Remove rogue administrator accounts and audit every remaining privileged user.
- Delete unknown plugins, unknown PHP files, and unexpected mu-plugins. Compare against a clean copy of every plugin at the same version rather than eyeballing.
- Reinstall Core and all plugins and themes from source, forcing overwrite. wp core download --force, then reinstall each plugin.
- Rotate everything. All user passwords. Application passwords. wp config shuffle-salts to invalidate every existing session and cookie. Database credentials. SFTP, SSH and control panel credentials. API keys and tokens stored in the site, including payment gateway, SMTP, CRM and analytics keys. Assume anything readable from disk or database was read.
- Review .htaccess, wp-config.php and any nginx config for injected rules and redirects.
- Check outbound connections and cron for persistence at the server level, not just inside WordPress.
- Re-scan, then monitor for at least thirty days. Returning attackers are the norm, not the exception.
Step 6. The regulatory part, if you operate in the EU or DACH
This is the item that gets overlooked and is expensive to overlook. A WordPress compromise involving RCE and database access is a personal data breach under GDPR if your site holds personal data, and almost every site does: form submissions, WooCommerce customers, user accounts, newsletter lists, comment data.
Article 33 gives you 72 hours from becoming aware of the breach to notify the competent supervisory authority, unless you can demonstrate the breach is unlikely to result in a risk to data subjects. That assessment is a documented decision, not a shrug. Involve legal counsel and your data protection officer early rather than after the clock runs out. If you act as processor for a client, your notification obligation to the controller is immediate.
Document your timeline as you go: when you became aware, what you found, what you did, what data was in scope. Reconstructing it a week later from memory is miserable and unconvincing.
Hardening so the next one costs you less
wp2shell will not be the last Core level pre-auth issue. The work that pays off is the work that reduces what any single one of them can do.
- Turn on automatic Core security updates and verify they function. Forced auto updates were the difference between a bad week and a non-event for a large number of sites in July.
- Maintain a real inventory. Every WordPress instance, its version, its owner, its exposure. Scanning to exploitation gave defenders a window of hours in this campaign. A window you cannot see is not a window.
- Put a WAF in front of everything. Not because rules are perfect, but because managed rules arrive faster than your patch window, as they did here.
- Least privilege on administrator accounts. Editors do not need admin. Contractors do not need permanent admin. Fewer admin accounts means a shorter reconciliation list when you need one.
- Disable file editing and, where practical, plugin installation in production. DISALLOW_FILE_EDIT and DISALLOW_FILE_MODS in wp-config.php mean an attacker who reaches administrator cannot trivially install a plugin dropper. This one control breaks the specific step wp2shell relies on for payload delivery.
- Make wp-content/uploads non-executable at the web server level. No PHP execution in upload paths, ever.
- Off site, versioned backups with tested restores. An untested backup is a hope.
- Centralise logs off the web server, so an attacker with shell access cannot edit the record of their own arrival.
- File integrity monitoring on wp-content, so a new PHP file at a random path generates an alert rather than waiting for a quarterly review.
One footnote worth sitting with
This chain was found with the help of a large language model. The researcher's own write-up on the discovery is titled around the fact that exploit brokers pay six figures for a WordPress RCE and that this one was found using GPT-5.6.
The lesson is not that AI broke WordPress. The lesson is about asymmetry of pace. Bug discovery is being industrialised on the offensive side faster than patch adoption is being industrialised on the defensive side. Seventeen July to twenty-two July, disclosure to weaponised public tooling, was five days. Plenty of WordPress estates take longer than five days to answer the question "what versions are we running".
If you take one structural change from this incident rather than one patch, make it that: shorten the time between "a critical Core vulnerability was announced" and "I know, with evidence, that none of my sites are on the affected versions". Everything else is downstream of that number.
Indicators of compromise
Behavioural indicators, durable across tool variants:
- Nested batch requests to /wp-json/batch/v1 or /?rest_route=/batch/v1 containing triple slash path segments
- Batch sub-requests chaining into /wp/v2/posts and /wp/v2/block-renderer/
- HTTP 207 Multi-Status responses from the batch endpoint, especially at odd hours or from unfamiliar source addresses
- Any administrator account with no corresponding authorised action
- Unknown plugin directories under wp-content/plugins/
- PHP files at randomised paths inside wp-content/ subdirectories
Weak or default indicators, useful but trivially changed by attackers: administrator usernames prefixed w2s_, path segments containing fun- or fun-proof-, user agents containing wp2shell or rezwp2shell.
Bitdefender published source IP addresses observed in active exploitation along with SHA-256 hashes for the specific webshells and plugin droppers recovered from a live compromise. Pull those from the advisory directly rather than from a secondary source, so you get any subsequent corrections.
Sources
- Searchlight Cyber, wp2shell disclosure and vulnerability checker
- Searchlight Cyber Research Center, wp2shell: Pre Authentication RCE in WordPress Core
- Bitdefender, Technical Advisory: wp2shell, unauthenticated RCE and full site takeover
- Elastic Security Labs, detecting wp2shell end to end
- BleepingComputer, critical wp2shell flaws exploited to install webshells
- SecurityWeek, WP2Shell vulnerabilities exploited in the wild
- The Hacker News, new wp2shell WordPress Core flaw
- CISA Known Exploited Vulnerabilities catalog
Need this handled rather than read about
We maintain WordPress infrastructure for clients across the DACH region, Italy and the Balkans, and we spent the second half of July on exactly this: inventory, patch verification, compromise assessment, and cleanup where cleanup was needed.
If you are unsure whether your sites were patched in time, or you have found an administrator account nobody can account for, we can run a compromise assessment and give you a written answer rather than a reassurance.
Comments