wp2shell-poc: an independent PoC for the WordPress REST batch route-confusion SQL injection
wp2shell (CVE-2026-63030 & CVE-2026-60137) - full RCE chain
At a glance
- What is it?
- Icex0/wp2shell-poc is a Python proof of concept for the wp2shell chain (CVE-2026-63030 and CVE-2026-60137). It checks for the route confusion, reads data through the SQL injection, and, given administrator credentials, uploads a plugin shell.
- Who is it for?
- Adopt wp2shell-poc if you are doing authorised testing of WordPress 6.9.0 to 6.9.4 or 7.0.0 to 7.0.1 and you need to confirm the route confusion without touching the database, or to demonstrate the pre-auth SQLi-to-admin bridge on a lab instance.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 62 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 October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What wp2shell-poc is, and the specific bug it demonstrates
The README describes this repository as an independent proof of concept for the unauthenticated WordPress REST batch route-confusion SQL injection associated with Searchlight Cyber's wp2shell advisory. It is not Searchlight Cyber's official checker, and the README says so directly. The project carries two CVE identifiers, CVE-2026-63030 and CVE-2026-60137, and is written in Python with no third-party dependencies.
The affected versions come from the advisory table in the README. WordPress 6.8.5 and below are listed as not affected, 6.9.0 through 6.9.4 as affected, and 7.0.0 through 7.0.1 as affected. If a target is outside those ranges, the chain described here does not apply, and the check command is not a general-purpose WordPress vulnerability scanner.
The audience is narrow by design: penetration testers and defenders who need to reproduce or rule out this specific chain. The README points to detection guidance from Elastic Security Labs and Eye Security for the defensive side, which suggests the authors expect the tool to be used alongside detection work rather than instead of it.
How the batch endpoint loses track of its own sub-requests
The mechanism is array desynchronisation inside the REST batch handler. According to the README, serve_batch_request_v1() builds two parallel arrays, $matches for the matched handler per sub-request and $validation for the validation result per sub-request, and then indexes both by the same offset when dispatching. A sub-request whose path fails wp_parse_url() is appended to $validation but not to $matches. The arrays fall out of step, and a sub-request is dispatched under a different sub-request's handler. That is the route confusion.
The PoC nests the primitive twice. First, a POST /wp/v2/posts request carrying a requests body is dispatched under the batch handler itself. Because it was validated as a posts request, its requests list is never checked against the batch schema, so its sub-requests may use GET and the method allow-list is bypassed. Second, inside that inner batch, a GET /wp/v2/posts/999999 item-route request carries posts collection query params such as author_exclude, orderby, and per_page. The 999999 ID does not need to exist; it is an unlikely post ID used to match the item route, whose schema does not validate those collection-only params. The desync then dispatches the same request under posts get_items(), where author_exclude maps to the WP_Query author__not_in query var, which the vulnerable build interpolates into SQL as a string.
The result is a boolean- and time-based blind SQL injection reachable before authentication. The repository also includes the UNION fake-post primitive used by the SQLi-to-admin chain.
Installing wp2shell-poc and running a first check
The README states the requirement plainly: Python 3.8 or later and the standard library, with no third-party dependencies. You can run the script from the repository directory, or install it with pip to get a wp2shell command on your PATH.
Start by confirming the interpreter version, then install the package from the repository root:
python3 --version
pip install .After that, wp2shell should be available as a command. The README gives the direct script form as well, which is useful if you would rather not install anything:
./wp2shell.py check http://targetA check prints passive WordPress markers and public version hints first, then sends a benign batch marker probe. On a vulnerable batch implementation the README says you should see HTTP 207 with the marker pattern parse_path_failed, block_cannot_read, and rest_batch_not_allowed. The probe is built around the WordPress core fix: the malformed /// request creates parse_path_failed, a /wp/v2/posts request acts as a batch-allowed spacer, and /wp/v2/block-renderer/... is not batch-allowed but returns block_cannot_read if its handler is reached anonymously. Fixed builds keep the arrays aligned, so the README states this exact all-three pattern should not appear for the crafted probe.
By default check stops there and does not send an SQLi payload. If you also want an active confirmation, add the flag the README documents:
./wp2shell.py check http://target --confirm-sqliThe confirmation tries the UNION read primitive first and falls back to paired timing probes if UNION reflection is unavailable. You can also point check at a file of URLs, which the README shows as ./wp2shell.py check targets.txt, scanning every URL in the file.
Reading data: three extraction techniques and their costs
The read command extracts data through the injection. The README gives the basic form and two shortcuts:
./wp2shell.py read http://target
./wp2shell.py read http://target --preset users
./wp2shell.py read http://target --query "SELECT @@version"With no options, read returns a server fingerprint. The users preset returns user logins and password hashes. The query form takes an arbitrary SELECT.
By default extraction is --technique auto, which tries methods in a fixed order. The union technique forges a fake WP_Post row via UNION and reads its title back from the REST response as ||HEX(value)||. The README notes the payload uses the same /wp/v2/posts/999999 source route with orderby=none and per_page=500 so the fake row survives as a rendered post, and that it costs one request per value. The error technique uses EXTRACTVALUE or UPDATEXML and leaks roughly 15 bytes per request, but only when the target reflects MySQL errors, for example when WP_DEBUG_DISPLAY is on. The blind technique does a boolean binary search at roughly 8 requests per character, reading the posts collection X-WP-Total header as the true/false signal, so it needs no reflected value at all.
You can force one with --technique union, --technique error or --technique blind. The README states these read paths do not write database rows, which is the property that makes read the safer half of the tool.
The SQLi-to-admin bridge and where the chain stops being pre-auth
The shell command is the part with real consequences. With --user and --password it logs in using supplied administrator credentials and uses WordPress plugin upload behaviour:
./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd idWithout credentials, shell first runs the pre-auth SQLi-to-admin bridge, logs in as the generated administrator, then uploads the plugin shell. The README also documents an interactive mode with -i.
The bridge itself is a five-step sequence, and the README is explicit about which steps are unauthenticated. First, UNION fake wp_posts rows render attacker-controlled content through a posts collection, using the /wp/v2/posts/999999 item-route source, the same route the SQLi read uses to reach get_items(). Second, that render makes WordPress create real oEmbed cache posts. Third, those real cache post IDs are recovered through the SQLi. Fourth, in one poisoned batch request, those IDs are recast as a customizer changeset, a navigation item, and a request hook shape. Fifth, the same request reaches POST /wp/v2/users and creates a generated administrator. Steps one through five are pre-authentication; the command-execution step is authenticated admin plugin upload.
That boundary matters when you are scoping a test. The pre-auth portion can create an administrator account, so even a run that never reaches --cmd id leaves state behind on the target.
Limitations, failure modes, and when this is the wrong tool
The README itself names the most important limitation: a WAF can block the payload, so a failed confirmation does not prove the bug is absent. That single sentence rules out using --confirm-sqli as a negative test in a report. A clean result from check means the marker pattern was not observed or the payload did not reach the database, not that the site is patched.
The three signals are independent, and the README says so: a version hint is only a hint, the marker pattern shows route confusion, and --confirm-sqli shows a payload reached the database. Reading any one of them as proof of the others is a misuse of the tool.
The extraction techniques have uneven reliability. The error technique depends on the target reflecting MySQL errors, which most hardened installs do not. The union technique depends on the fake row surviving as a rendered post, which is why it pins orderby=none and per_page=500. If reflection is unavailable, auto falls back to blind, and blind costs roughly 8 requests per character, so pulling a full password hash list over a slow or rate-limited target is impractical. There is no documented option for throttling, parallelism or request pacing in the README.
Finally, this is not a fleet scanner. There is no inventory, no scheduling, no reporting format, and no authentication handling beyond the two credential flags. If you need to sweep thousands of hosts, the per-target request cost of the blind path and the absence of any concurrency control make it the wrong shape of tool.
How it differs from the official checker and from generic WordPress scanners
The README draws the first comparison itself: this repository is not Searchlight Cyber's official checker. The practical difference is scope. An advisory checker typically answers one question, whether the target is exposed, and stops. This PoC goes further in both directions: check is the narrow end, and read and shell are the wide end, up to and including command execution.
Against a generic WordPress vulnerability scanner the difference is the opposite. A scanner enumerates plugins, themes and core versions and matches them against a feed of known issues. wp2shell-poc does none of that. It targets one chain with a hand-built request sequence, and the README states that the marker probe is based on the WordPress core fix, meaning the detection logic is derived from the patch rather than from a signature. That makes it precise for this bug and useless for anything else. A scanner will also typically report a version range as a finding; here the version table is presented as a hint, and the README explicitly separates the hint from the marker pattern and from the SQLi confirmation.
For defenders, the README points to Elastic Security Labs and Eye Security rather than offering its own detection content. If your goal is detection rather than testing, those two write-ups are the sources the project itself recommends.
Licence, maintenance and the cost of keeping it working
The project is MIT licensed, both in the repository metadata and in pyproject.toml, which declares license = { text = "MIT" }. MIT is permissive: it allows use, modification and redistribution provided the copyright notice and permission notice are retained. This is not legal advice, and if you plan to redistribute the tool inside a commercial product you should read the LICENSE file at the repository root rather than relying on the classification.
The last push to the repository was on 2026-08-11. The repository is not archived. There are no releases retrieved for it, so distribution is from the repository itself, either by running wp2shell.py in place or by pip install . from a checkout. That matters for upgrades: there is no published package version to pin against beyond the 1.0.0 declared in pyproject.toml, and no release notes to read before pulling new commits.
The dependency surface is the cheapest part of the maintenance story. The README states Python 3.8 or later and the standard library, with no third-party dependencies, and pyproject.toml sets requires-python = ">=3.8". There is nothing to audit in a lockfile and nothing to break when a transitive package publishes a bad version. The real upgrade risk is behavioural: the check logic is tied to the WordPress core fix, so if that fix changes shape in a later release, the marker pattern the README describes may need to be re-derived. Nothing in the repository layout suggests a test suite that would catch that for you; the top-level entries are .gitattributes, .gitignore, LICENSE, README.md, docs/, pyproject.toml, wp2shell.py and the wp2shell package.
Editorial conclusion
Adopt wp2shell-poc if you are doing authorised testing of WordPress 6.9.0 to 6.9.4 or 7.0.0 to 7.0.1 and you need to confirm the route confusion without touching the database, or to demonstrate the pre-auth SQLi-to-admin bridge on a lab instance. Do not use it against systems you do not own or have written permission to test, and do not treat it as a scanner for production fleets: the README states that a WAF can block the payload, so a failed --confirm-sqli does not prove the bug is absent. Before relying on it, verify that python3 --version reports 3.8 or later, run ./wp2shell.py check http://target without --confirm-sqli to see the three-marker pattern, and confirm that the target version falls inside the ranges in the advisory table.
Frequently asked questions
What is wp2shell-poc?
It is an independent Python proof of concept for the unauthenticated WordPress REST batch route-confusion SQL injection associated with Searchlight Cyber's wp2shell advisory, covering CVE-2026-63030 and CVE-2026-60137. It provides a non-destructive check, a database read path, and a plugin-backed command shell. The README states it is not Searchlight Cyber's official checker.
Which WordPress versions are affected by wp2shell-poc's target bug?
The README reproduces the advisory table: 6.8.5 and below are not affected, 6.9.0 through 6.9.4 are affected, and 7.0.0 through 7.0.1 are affected. Outside those ranges the chain does not apply.
Does wp2shell-poc need any dependencies to run?
No. The README states Python 3.8 or later and the standard library, with no third-party dependencies, and pyproject.toml sets requires-python to 3.8 or above. You can run ./wp2shell.py from the repository directory or install it with pip install . to get a wp2shell command.
Does a failed wp2shell-poc confirmation mean the target is not vulnerable?
No. The README states that a WAF can block the payload, so a failed confirmation does not prove the bug is absent. It also notes the signals are independent: a version hint is only a hint, the marker pattern shows route confusion, and --confirm-sqli shows a payload reached the database.
Which part of the wp2shell-poc chain happens before authentication?
The README lists steps one through five of the RCE path as pre-authentication: the UNION fake-post render, the oEmbed cache post creation, recovering the cache post IDs through the SQLi, the poisoned batch request that recasts them, and reaching POST /wp/v2/users to create a generated administrator. The command-execution step is authenticated admin plugin upload.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/icex0-wp2shell-poc)