Icex0 49fe874bb7 Merge pull request #1 from mosajjal/fix/webshell-cleanup
Fix webshell cleanup and CLI behavior
2026-07-18 06:37:58 +02:00
2026-07-17 22:18:22 +02:00
2026-07-18 00:50:04 +02:00
2026-07-18 00:50:04 +02:00
2026-07-18 02:04:58 +02:00
2026-07-18 00:50:04 +02:00

wp2shell-poc

Independent proof-of-concept for the unauthenticated WordPress REST batch route-confusion SQL injection associated with Searchlight Cyber's wp2shell advisory.

The unauthenticated primitive reaches the injection through a single endpoint: POST /wp-json/batch/v1.

This repository is not Searchlight Cyber's official checker and does not claim to reproduce undisclosed wp2shell internals. check confirms the SQLi path, read demonstrates database read, and shell is an optional post-authentication helper that requires valid administrator credentials before uploading a plugin webshell.

Affected versions

Searchlight Cyber's advisory lists these wp2shell RCE exposure ranges:

Version range Status
<= 6.8.5 Not affected
6.9.0 6.9.4 Affected
7.0.0 7.0.1 Affected

How it works

The REST batch endpoint (/batch/v1) is unauthenticated and runs several sub-requests in one call, relying on each sub-request being validated and permission-checked on its own.

serve_batch_request_v1() builds two parallel arrays — $matches (the matched handler per sub-request) and $validation (the validation result per sub-request) — 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, so 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:

  1. A POST /wp/v2/posts request that carries a requests body is dispatched under the batch handler itself. Having been validated as a posts request, its requests list is never checked against the batch schema, so its sub-requests may use GET — the method allow-list is bypassed.
  2. Inside that inner batch, a GET /wp/v2/users request carrying an author_exclude string (the users schema has no such parameter, so the value passes validation untouched) is dispatched under posts get_items(). There 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 pre-authentication. This PoC can use that database read path to recover administrator password hashes. Turning those hashes into plugin-upload code execution is outside the unauthenticated primitive and depends on obtaining valid administrator credentials.

Searchlight has not published the final jump from SQLi to pre-auth RCE. That may involve a database file-write trick such as MySQL OUTFILE, or it may be something else in core. This repo does not include that step.

Requirements

Python 3.8+ and the standard library. No third-party dependencies.

Usage

Run it from the repository directory:

./wp2shell.py <command> <url> [options]

Or pip install . to get a wp2shell command on your PATH.

check — confirm the vulnerability (safe)

Prints passive WordPress markers and public version hints first, then sends a benign batch marker probe. A vulnerable batch implementation returns HTTP 207 with the route-confusion marker pattern parse_path_failed, block_cannot_read, and rest_batch_not_allowed.

The marker probe is based on the WordPress core fix. The malformed /// request creates parse_path_failed; a /wp/v2/posts request acts as a batch-allowed spacer; the /wp/v2/block-renderer/... route is not batch-allowed but returns block_cannot_read if its handler is reached anonymously; /batch/v1 gives rest_batch_not_allowed. On vulnerable builds the parse error shifts the batch handler arrays out of step, so the spacer request is dispatched under the block-renderer handler. Fixed builds keep the arrays aligned, so this exact all-three pattern should not appear for the crafted probe.

By default, check stops there and does not send a SQL timing payload. Use --confirm-sqli when you also want the active paired timing confirmation. The timing step reads no data and changes nothing; it sends three baseline/delayed pairs by default and decides on the median delta.

Treat the signals separately: an affected public version is strong triage evidence, the batch marker pattern confirms the vulnerable route-confusion behavior, and timing confirmation proves the SQLi path reached the database. A WAF or edge rule can block the timing payload, so a failed timing check does not override a positive marker probe.

./wp2shell.py check http://target

read — extract data (blind SQL injection)

./wp2shell.py read http://target                      # server fingerprint
./wp2shell.py read http://target --preset users       # user logins and password hashes
./wp2shell.py read http://target --query "SELECT @@version"

shell — post-auth plugin webshell helper

Optional post-exploitation helper. This is not a pre-authentication RCE step: it requires valid administrator credentials. If read --preset users recovers a password hash, supply the recovered plaintext here after offline cracking.

./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recovered>' -i   # interactive shell

shell uploads a plugin webshell (locked behind a random path and a per-run token) and prints its path. Remove it when finished — either pass --cleanup to have it delete itself after running, or delete the plugin manually.

Options

Option Applies to Description
--rest-route check, read Use /?rest_route=/batch/v1 (for sites without pretty permalinks).
--proxy URL all Route traffic through an HTTP proxy (for example, Burp).
--timeout N all Request timeout in seconds.
--sleep N check Delay used with --confirm-sqli.
--samples N check Timing pairs used with --confirm-sqli (default 3).
--confirm-sqli check Also send the active SQL timing confirmation payload.
--preset read fingerprint or users.
--query read A scalar SQL expression to read.
--prefix read Database table prefix (default wp_).
--max-length N read Maximum characters read per value (default 128).
--user / --password shell Admin credentials (plaintext, recovered from the hash).
--cmd shell Command to run (omit when using -i).
-i / --interactive shell Open an interactive shell after deploying.
--cleanup shell Delete the webshell from the target when finished.

Remediation

Update to WordPress 7.0.2, or 6.9.5 if the site is on the 6.9 branch. Until then, block both /wp-json/batch/v1 and the rest_route=/batch/v1 query parameter at the edge, or require authentication for the batch endpoint via the rest_pre_dispatch filter.

For authorized security testing only. Use it exclusively against systems you own or have explicit written permission to test. No warranty is provided and no liability is accepted for misuse.

References

S
Description
Automated archival mirror of github.com/Icex0/wp2shell-poc
Readme MIT 72 KiB
Languages
Python 100%