About OFFCEPT
Built by operators who spent years breaking into networks before building a company around it.
Learn More
← Back
Technical Research18 July 20265 min read

wp2shell (CVE-2026-63030 & CVE-2026-60137): Unauthenticated SQL Injection in WordPress Core

Written by 0xM4L · OFFCEPT Security Research

TL;DR

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_exclude reaches WP_Query as a raw string instead of an integer array. The array branch runs everything through absint(). The scalar branch skips it entirely and gets concatenated straight into the SQL NOT 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

StatusVersionsNotes
Not affected6.8.5 and earlierConfirmed via code-level patch diff
Vulnerable6.9.0 – 6.9.4Update to 6.9.5
Vulnerable7.0.0 – 7.0.1Update to 7.0.2
Fixed6.9.5, 7.0.2, 7.1 Beta 2Aligns 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/v1 and ?rest_route=/batch/v1 at 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/v1 requests with malformed http://: paths and an author_exclude value containing SLEEP, CHAR_LENGTH, or SUBSTRING. A plugin directory matching wp-content/plugins/wp2shell-*. Response bodies containing WP2SHELL_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_MODS on 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.