wp2shell-PoC: a WordPress pre-auth RCE chain in one Python script
CVE-2026-63030 & CVE-2026-60137 RCE chain proof-of-concept
At a glance
- What is it?
- The repository packages CVE-2026-63030 and CVE-2026-60137 into a single-file checker and exploit runner. It is a lab tool, not a scanner, and its own README says so.
- Who is it for?
- Use wp2shell-PoC only on WordPress instances you own or are authorised to test, and only if you can run Python 3.8 or later from the repository directory. Do not use it as an inventory scanner: it is a single-target PoC whose check mode depends on one specific error pattern, and the README does not document rollback, rate limiting or a licence.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 16, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem wp2shell-PoC addresses
WordPress core shipped two bugs that only matter together. CVE-2026-63030 is a route confusion in the batch REST endpoint; CVE-2026-60137 is a SQL injection in WP_Query. Either one alone is awkward to reason about. Chained, the README describes them as an unauthenticated path to full WordPress compromise and remote code execution.
That claim is hard to verify by reading patches, because the chain crosses several subsystems: REST routing, query building, the object cache, changesets and hooks. wp2shell-PoC exists to make the chain reproducible. It is a proof-of-concept for people who already understand WordPress internals and want a runnable artefact instead of a write-up: vulnerability researchers, bug bounty hunters working inside a program's scope, and defenders who need to see what the traffic looks like. It is not a general-purpose scanner and it does not try to be one. The README opens with a warning that the tool is for educational or bug bounty purposes and that unauthorised use outside controlled environments is prohibited.
How the route confusion desynchronises the batch endpoint
The mechanism is worth stating precisely, because the whole chain rests on it. The README says `/wp-json/batch/v1` processes sub-requests through two arrays, `$matches` and `$validation`, indexed by position. A sub-request with a malformed path such as `http://:` hits a `continue` statement. It is appended to `$validation` but not to `$matches`. From that point the arrays are offset by one, and later requests are dispatched under the handler intended for the next request in the list. Schema validation and permission checks are skipped for those requests.
The SQL injection rides on the same desync. Two nested batch calls are used. The outer batch bypasses the method allow-list that would normally block GET. The inner batch delivers a scalar `author_exclude` string to `GET /wp/v2/posts`. Because the desync routes it past validation, `WP_Query` interpolates the unsanitized string straight into SQL, giving a UNION-based blind injection.
What follows is the part that makes this more than an SQL bug. The forged `WP_Post` objects returned by the injection are cached in memory. Those fake posts carry `[embed]` shortcodes, so WordPress writes real `oembed_cache` rows derived from fake references. The attacker then forges a `customize_changeset` post in memory with `"user_id": 1` in its JSON; a cycle-detection gadget calls `wp_update_post()` without overwriting `post_content`, so the payload survives, and applying the changeset temporarily assumes the administrator's identity. A fabricated post with status `parse` and type `request` fires the `parse_request` hook, replaying the batch under that assumed role. A `POST /wp/v2/users` sub-request then succeeds and creates an admin account. Code execution is the mundane end of the chain: log in as the created admin and upload a plugin.
Installing wp2shell-PoC and running a first check
There is no package to install. The README states the only requirement is Python 3.8 or later, and the repository holds three top-level entries: `README.md`, `exp.py` and `wp2shell.py`. You run the script from the repository directory.
The default mode is a vulnerability check. It sends a benign batch marker probe that detects the route confusion without executing SQLi payloads. A vulnerable target returns HTTP 207 with the error pattern `parse_path_failed`, `block_cannot_read` and `rest_batch_not_allowed`.
wp2shell.py http://victim.comIf you want the check to also send an active SQLi confirmation payload, add `--confirm-sqli`. The README says the confirmation tries UNION reflection first and falls back to timing-based probes.
wp2shell.py http://target.com --check --confirm-sqliThe README also documents an explicit `--check` form, which is the same mode spelled out. Note the flag table: `--check` is the default when no other mode is given, so the bare invocation and the explicit one behave the same way.
From there, read mode extracts data through the injection. The default `--technique auto` tries union, then error, then blind. The README describes union as the fastest at one request per value, error as roughly 15 bytes per request and dependent on MySQL errors being reflected (for example with `WP_DEBUG_DISPLAY` on), and blind as a boolean binary search at about 8 requests per character using the `X-WP-Total` header as the signal. Read paths are described as read-only and do not write to the database.
wp2shell.py http://target.com --read --preset usersShell mode is the last step. With credentials it logs in as an existing admin and uploads a plugin shell; without credentials it runs the full SQLi to admin bridge first. The README says the plugin webshell is uploaded with a random path and a per-run token, and is removed automatically. When the pre-auth bridge creates an administrator, that generated account is removed automatically after the shell session finishes.
wp2shell.py http://target.com --shell --cmd idWhere wp2shell-PoC is the wrong tool
The check mode is narrower than it looks. It detects one specific error pattern in an HTTP 207 response. A target behind a WAF, a reverse proxy that rewrites REST routes, or a host that returns a generic 403 for `/wp-json/batch/v1` will not produce that pattern, and the tool has no second signal to fall back on. Absence of the pattern is not evidence that the target is patched.
Version coverage is another boundary. The README lists WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1 as vulnerable, with 6.9.5 and 7.0.2 and later as fixed. Anything outside those ranges is undocumented here, including older branches and forks. If you need fleet-wide coverage, this repository does not provide it: there is no target list, no concurrency, no output format for aggregation, and no release artefacts. It is one script against one URL.
The shell mode also writes to the target. It uploads a plugin and, in the pre-auth path, creates an administrator account. The README states both are removed automatically, but cleanup is a code path, not a guarantee. On a target where the plugin upload partially fails, or where the process is interrupted mid-session, the README does not document rollback. Treat shell mode as a state-changing operation and only run it where you can restore the instance from a snapshot.
How this differs from WPScan and generic WordPress scanners
WPScan and similar tools take a fingerprinting approach. They enumerate the WordPress version, installed plugins and themes, and match those against a vulnerability database. They are built to be pointed at many hosts and to produce a report. They do not exploit anything by default, and they cannot tell you whether a specific chain is reachable on a hardened target, because reachability depends on request routing rather than on version strings.
wp2shell-PoC inverts that. It carries no database and does no enumeration. It sends a crafted batch request and reads the response. The trade-off is scope: it can confirm exploitability of this one chain, including on a target whose version banner has been hidden, but it will never tell you that a site is running an outdated plugin. The two approaches answer different questions. If your job is to inventory a thousand WordPress sites, this repository is not the instrument. If your job is to prove or disprove one specific chain on one host, a version-based scanner is the weaker option, because the README's own mechanism depends on how the batch endpoint dispatches requests, not on which release is installed.
Maintenance, licence and upgrade cost
The repository is not archived and the last push was on 2026-09-08, which is recent. There are no retrieved releases, so there is no versioned artefact to pin and no changelog to read. Upgrades mean pulling the branch and re-reading `wp2shell.py` and `exp.py`. For a PoC tied to a live CVE chain, that is a real cost: the script's behaviour can change between pulls with nothing in the repository recording what changed.
The licence is a bigger gap. The repository metadata does not state one, and the README does not name one either. Without a licence file, the default position is that the author retains rights, which matters if you intend to vendor the script into an internal toolkit or ship it in a product. The README's warning limits stated use to educational or bug bounty purposes and prohibits unauthorised use outside controlled environments. That is a usage statement, not a licence grant, and the two are not the same thing. If you need to redistribute this code, ask the author rather than assuming the warning text covers you. Nothing here is legal advice; the point is simply that the terms are undocumented.
Editorial conclusion
Use wp2shell-PoC only on WordPress instances you own or are authorised to test, and only if you can run Python 3.8 or later from the repository directory. Do not use it as an inventory scanner: it is a single-target PoC whose check mode depends on one specific error pattern, and the README does not document rollback, rate limiting or a licence. Before running anything beyond the default check, verify the target's WordPress version against the table in the README, because 6.9.5 and 7.0.2+ are listed as fixed and the probe will simply return nothing useful.
Frequently asked questions
What does PoC mean in security?
A proof-of-concept is a minimal implementation that demonstrates a vulnerability is real and reachable, rather than only describing it. wp2shell-PoC plays that role for the CVE-2026-63030 and CVE-2026-60137 chain, providing runnable check, read and shell modes instead of a written advisory alone.
What is a PoC vulnerability?
It is a vulnerability accompanied by code that exercises it, so that the claim can be reproduced rather than taken on trust. In this repository the PoC spans a route confusion in the batch REST endpoint and a WP_Query SQL injection, chained into an unauthenticated path to remote code execution.
What are the new vulnerabilities in WordPress covered by wp2shell-PoC?
The repository covers CVE-2026-63030, a route confusion in the batch REST endpoint, and CVE-2026-60137, a SQL injection in WP_Query. The README lists WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1 as vulnerable, with 6.9.5 and 7.0.2 and later as fixed.
Community notes