
Independent root cause analysis and proof-of-concept for unauthenticated SQL injection to RCE in WordPress (CVE-2026-63030 + CVE-2026-60137), with Docker lab and detailed exploit chain documentation.
WordPress core disclosed wp2shell on 2026-07-17 as a critical, unauthenticated RCE affecting default installs (WordPress 6.9.0-6.9.4, 7.0.0-7.0.1, zero plugins required). The original discoverer (Searchlight Cyber) withheld technical details at disclosure time. This repo documents an independent root-cause analysis derived entirely from diffing WordPress core source between the vulnerable (6.9.4) and patched (6.9.5) releases, plus live verification in a local lab.
Credit: the reproduction here follows the request shape from sergiointel/wp2shell-poc, confirmed byte-for-byte and cross-checked against the real patch diff.
root_cause_analysis.md - full writeup: the two chained bugs, exact
vulnerable code, the patch diff, the exploit chain, and live before/after
verification against a real WP 6.9.4 instance.poc_upstream.py - unmodified copy of sergiointel's original PoC (SQLi only).poc_upstream_rce.py - sergiointel's PoC as updated roughly 22 hours after
disclosure, adding unauthenticated privilege escalation and RCE without
cracking any credential. See section 7 of root_cause_analysis.md.poc_extract.py / poc_extract2.py - a tuned variant (larger SLEEP(),
higher timing threshold, longer character-search ceiling) needed to get a
reliable signal in a Dockerized/proxied lab where the upstream script's
default SLEEP(0.15) was lost in network jitter.docker-compose.yml - spin up the same WordPress 6.9.4 + MariaDB lab used
for verification.payload.json, response.json, response_patched.json - a hand-crafted
request replicating the PoC's structure and the raw server responses,
before and after applying the 6.9.5 patch files in place.A second, independently written PoC (github.com/Icex0/wp2shell-poc) was reviewed
in full and describes the same root cause mechanism independently, corroborating
the analysis below. Using its content-based (boolean, not timing-based) blind
SQLi extraction, this lab's admin credential hash was recovered with 100%
accuracy, and, using that known credential, a full SQLi-to-RCE chain was
demonstrated live: SQL injection to credential extraction to authenticated
plugin-upload webshell to code execution as www-data. See section 6 of
root_cause_analysis.md.
That credential-cracking path is real but is not the only one. sergiointel's
PoC was subsequently updated to achieve unauthenticated RCE with no credential
cracking at all, by using UNION SELECT to forge fake database rows that
WordPress's own Customizer/nav-menu/oEmbed-cache code trusts enough to let an
unauthenticated user-creation request succeed. See section 7 of
root_cause_analysis.md. The unauthenticated SQL injection alone is sufficient
for full RCE on a default install.
CVE-2026-63030 (REST batch route confusion, CWE-436):
WP_REST_Server::serve_batch_request_v1() appends to a $matches[] array by
simple push, but skips the append when a sub-request's path fails to parse
(e.g. a deliberately malformed "http://:" entry). That single skip
desynchronizes $matches[] from $requests[] by one index for every
following entry. At dispatch time, $matches[$i] no longer corresponds to
$requests[$i], so the code ends up executing a request's data under a
different route's matched handler than the one it was actually validated
against.
CVE-2026-60137 (SQL injection): WP_Query::get_posts() only ran
absint() sanitization on author__not_in inside an is_array() branch. A
scalar string value skipped sanitization entirely and was concatenated
directly into ... post_author NOT IN ($value).
Chained: the route-confusion bug lets an attacker get a request declared
against /wp/v2/categories (which doesn't recognize author_exclude, so it's
never sanitized) actually dispatched through the posts controller (which
does read author_exclude and forwards it into WP_Query). No authentication
required.
On the RCE claim: confirmed live that unauthenticated blind SQL injection
works and can exfiltrate arbitrary DB content, including wp_users.user_pass.
A single timing-based extraction pass in a virtualized lab had meaningful
bit-error noise (~94% per-character accuracy in testing here); a content-based
(boolean) oracle did not share that failure mode and recovered the same field
with 100% accuracy. RCE was confirmed via two distinct paths: cracking a
recovered password hash (conditional on password strength), and an
unauthenticated privilege-escalation gadget chain using UNION-forged database
rows that WordPress's own Customizer/nav-menu/oEmbed-cache code trusts
(unconditional, no credential compromise required). See section 7 of
root_cause_analysis.md.
docker compose up -d
# wait for WordPress install wizard to be reachable on :8890, then complete setup
HTTP_PROXY=http://127.0.0.1:8080 HTTPS_PROXY=http://127.0.0.1:8080 \
python3 poc_extract2.py http://localhost:8890 "SELECT DATABASE()"
See root_cause_analysis.md for the full technical writeup.
WordPress has published official fixes: 6.9.5 and 7.0.2 (7.1 Beta 2 for the beta branch), released 2026-07-17. Update immediately if you run an affected version. This repository is published for defensive and educational purposes after the patch was already public. Only run any of the code here against systems you own or are explicitly authorized to test.