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

··Feeds·Contact·Privacy·© 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
GitHubmcipekci/wp2shell
1581 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 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.

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.

root@kitploit:~
chmod +x wp2shell.py

Usage

root@kitploit:~
# 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)

root@kitploit:~
./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

root@kitploit:~
./wp2shell.py -f targets.txt --check --target-threads 8

Options

WAF / hardening evasion


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

The --exec path is designed to leave nothing behind: the temporary administrator, its usermeta, the fabricated oembed_cache rows, and the uploaded webshell are all removed before the tool exits. There is no --keep flag — an operator should never leave a live, attacker-created admin account on a client's system.


Files

  • wp2shell.py — the exploit (single file, stdlib only).

Credits

Vulnerability discovery, disclosure, and the original "wp2shell" write-up credit belongs to the Searchlight Cyber / Assetnote research team:

  • CVE-2026-63030 — REST /batch/v1 route-confusion desync
  • CVE-2026-60137 — WP_Query author__not_in SQL injection

This repository is an independent proof-of-concept implementation of the exploit chain they disclosed; it does not claim discovery of the underlying vulnerabilities. If any attribution is incomplete or incorrect, open an issue and it will be corrected.

References

  • Searchlight Cyber / Assetnote — wp2shell: Pre-Authentication RCE in WordPress core https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/
  • WordPress 7.0.2 release (fix) — https://wordpress.org/news/2026/07/wordpress-7-0-2-release/
  • Advisories — GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf

Disclaimer

This is a security research proof of concept for authorised penetration testing and defensive validation. Running it against systems you do not own or have written permission to test is illegal. The author accepts no liability for misuse.

Download Tool
  • 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.

  • 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)
    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)