wp2shell (CVE-2026-63030 & CVE-2026-60137): Unauthenticated SQL Injection in WordPress Core
Written by 0xM4L · OFFCEPT Security Research
wp2shell chains a REST API batch validation desync with an unsanitised WP_Query parameter into unauthenticated, anonymous SQL injection against WordPress Core. It leaks the admin username and password hash. Reaching a full shell from there needs a few extra steps on top, not something either CVE grants outright. Patch to 6.9.5 or 7.0.2 now.
wp2shell was found by Adam Kues at Assetnote, part of Searchlight Cyber, and reported through WordPress's HackerOne programme. It carries two CVEs. CVE-2026-63030 is a validation desync in the WordPress REST API batch endpoint. CVE-2026-60137 is a SQL injection in WP_Query's author__not_in parameter. Chained, an anonymous attacker can run blind SQL injection against a default WordPress install and pull data straight out of the users table, no login, no plugin, no special configuration required. Security researcher Andy Gill published a full code-level trace of the patch diff on zsec.uk, which is what the technical breakdown below is based on.
What the two CVEs actually do
- CVE-2026-63030, batch validation desync. The REST batch endpoint (
/wp-json/batch/v1) builds parallel arrays to track each subrequest, its matched route, and its validation result. A malformed subrequest path fails silently and only gets added to one of those arrays, not the other. Every subrequest after it is now paired with the wrong route's match and validation state. - CVE-2026-60137, unsanitised author__not_in. Using that desync, a request aimed at the public posts endpoint can be dispatched through a route whose parameter schema never ran, so
author_excludereachesWP_Queryas a raw string instead of an integer array. The array branch runs everything throughabsint(). The scalar branch skips it entirely and gets concatenated straight into the SQLNOT IN (...)clause.
The result is blind SQL injection, not a visible data dump. There is no error output and no reflected query result, just a timing side channel: wrap the injected condition in SLEEP(), and a slow response means the condition was true. That is enough to extract data character by character, including username:password_hash from the users table, but it is a few hundred requests per finding, not a single click.
The CVE record calls this remote code execution, and a path to a shell does exist: crack the recovered password offline, log in normally with it, then use that account's own plugin-upload feature to drop and directly request a PHP file. But that second half runs on its own preconditions (a crackable password, no MFA, install_plugins rights, file mods and system()both enabled) that have nothing to do with either CVE. Worth knowing, but the CVEs themselves are what you patch for, so that's the part this post focuses on.
Affected and fixed versions
| Status | Versions | Notes |
|---|---|---|
| Not affected | 6.8.5 and earlier | Confirmed via code-level patch diff |
| Vulnerable | 6.9.0 – 6.9.4 | Update to 6.9.5 |
| Vulnerable | 7.0.0 – 7.0.1 | Update to 7.0.2 |
| Fixed | 6.9.5, 7.0.2, 7.1 Beta 2 | Aligns the batch arrays and normalises author__not_in at the SQL boundary |
What to do now
Public PoCs exist, and most walk the full chain end to end: blind SQLi, credential recovery, login, plugin upload. Don't wait for confirmed in-the-wild reports. If it's unpatched and internet-facing, assume it's already being scanned.
- Patch every WordPress instance you run. 6.9.x → 6.9.5, 7.0.x → 7.0.2. Managed hosts usually push this automatically, but verify it, and don't forget the microsites and staging environments you forgot existed.
- Can't patch immediately? Block anonymous access to
/wp-json/batch/v1and?rest_route=/batch/v1at the WAF as a 24 to 48 hour bridge. It can break legitimate integrations (block editor, mobile apps), so remove it the moment the patched version is deployed. - Hunt for the specific IOCs. Bursts of near-identical
POST /wp-json/batch/v1requests with malformedhttp://:paths and anauthor_excludevalue containingSLEEP,CHAR_LENGTH, orSUBSTRING. A plugin directory matchingwp-content/plugins/wp2shell-*. Response bodies containingWP2SHELL_OUT_START. Any of these means the chain reached further than credential disclosure. - If your admin password was ever weak or reused, rotate it now regardless of whether you find IOCs, and turn on MFA if you haven't. That single control breaks the chain even after the SQLi has already leaked the hash.
- Consider
DISALLOW_FILE_MODSon sites that don't need to install plugins through the admin UI. It removes one of the preconditions this chain depends on, for this bug and the next one that reaches an admin session some other way.
If you want us to check whether your WordPress estate was hit before you patched, get in touch.