
Analysis and end-to-end implementation of the patched wordpress RCE vulnerability - CVE-2026-60137 and CVE-2026-63030

all pocs weaponize the same two bugs - the REST batch route confusion (CVE-2026-63030) and the
author__not_in SQL injection (CVE-2026-60137) - with the same double-nested batch shape.
What differs between them is the RCE path chosen, the environment preconditions,
and the safety defaults. This document states, concretely, where the implementation in
this repository sits in that landscape.
check sends no SQL payload unless asked;
all traffic can carry an attribution tag; everything the shell command writes to the
target is removed automatically afterwards. See §3.[1] Timing/blind only as a read channel; the UNION fake-post primitive exists inside the
bridge but is not exposed as an extraction oracle.
[2] The naive availability probe (0) UNION SELECT …) is silently dropped during object-cache
hydration, available() returns false, and the whole pre-auth bridge aborts - see §1.
[3] The project's own README lists "no persistent object cache (Redis/Memcached)" under
Preconditions.
[4] Public variant mirrored at Sploitus (link below): blind read plus an INTO OUTFILE
dropper as the RCE step, using a per_page=-1 categories carrier.
[5] The OUTFILE RCE path does not depend on fake-post rendering, so object caches do not
block it - the MySQL FILE privilege and a shared writable directory do. Managed hosting
almost never grants FILE to the WordPress DB user, and secure_file_priv is commonly set.
The UNION fake-post primitive depends on how WP_Query returns rows:
wp_posts rows; a UNION-injected row becomes
a WP_Post directly. The forgery renders.On hosts with a persistent object cache, a populated base result set pushes WP_Query into
split mode. The stock probe used by the public PoCs -
0) UNION SELECT <forged row> -- -
post_author NOT IN (0) matches every row), so behind an
object cache the forged row evaporates: the availability probe false-negatives, available()
returns false, and the entire pre-auth bridge is reported "dead" on a host that is in fact
fully exploitable. The public unifier documents the same boundary by listing "no persistent
object cache" as a hard precondition.This repo empties the base set instead:
1) AND 1=0 UNION ALL SELECT <forged row> -- -
With zero base rows, the forged row is the only row; the query stays in full-row mode;
no hydration lookup ever runs. One injected keyword (AND 1=0) is the entire difference
between "UNION channel dead" and "full pre-auth RCE" on object-cached hosts - which are the
majority of managed WordPress production environments. The diagnosis, the probe matrix
(per_page × injection form).
Scope note: the blind/timing read channel is not object-cache-sensitive (counting rows in SQL does not involve fake-post hydration), so every PoC's blind read works everywhere. What the object cache kills in the other PoCs is specifically the UNION-dependent part: in-band extraction and the SQLi-to-admin bridge.
A second, related lesson documented in the case study: when both channels work, treat the in-band UNION read as authoritative - the production timing oracle produced bit flips under jitter on a value the in-band read settled unambiguously.
Three pre-auth RCE paths exist across the public PoCs:
This repo implements the bridge: it needs no database privileges beyond what WordPress
already has, works when DB and web tiers share nothing, and leaves no file behind for the
FILE-privilege path to depend on. The trade-off is complexity - the bridge is a seven-row
poisoned post graph - which is exactly where the §1 object-cache false negative used to
hide the path.
Built for running against production systems under authorization, not just labs:
check is non-destructive by default - passive fingerprint plus a benign marker
batch; no SQL payload is sent unless --confirm-sqli is given. After patching, the
disappearing marker triplet doubles as fix validation.--user-agent on every command so all exploit traffic is
identifiable in logs (an engagement rule-of-thumb the public tools don't default to).author_exclude → author__not_in sink, the fake-WP_Post UNION primitive, and
the customizer-bridge concept are all public techniques (lineage acknowledged below).INTO OUTFILE dropper), mirrored at Sploitus -
https://sploitus.com/exploit?id=7CD079AD-E27B-5C54-A696-60635BFDB241| Capability | This repo | Icex0/wp2shell-poc | sergiointel/wp2shell-poc | 0xsha/wp2shell | OUTFILE variant [4] |
|---|
| Pre-auth blind/timing SQLi read | yes | yes | yes (timing) | yes | yes |
| In-band UNION read (1 request/value) | yes | yes | - [1] | - [1] | - |
| Error-based read (EXTRACTVALUE) | yes | yes | - | - | - |
| UNION channel survives persistent object cache | yes (killed-base) | no - probe false-negatives [2] | not documented | no - documented precondition [3] | n/a [5] |
| Crack-free pre-auth RCE | yes (SQLi-to-admin bridge) | yes (same bridge) | yes (bridge origin) | yes (same bridge) | yes, via INTO OUTFILE [5] |
| Extra preconditions for RCE | none beyond default install | none (on non-object-cache hosts) | none (same) | none (same) | MySQL FILE priv + web-writable path shared with mysqld |
| Non-destructive check / patch validation | yes (marker triplet; no payload by default) | yes | no | yes (block_cannot_read) | yes (marker batch) |
| Attribution/User-Agent tagging | yes, on all commands | no | no | transport flag | no |
| Automatic cleanup (webshell + generated admin) | yes | yes | not documented | token-gated webshell only | dropper removed [5] |
| Blue-team detection guide | yes, from a production run | no | no | lab matrix instead | mitigation notes |
| Dependencies | stdlib only | stdlib only | single file | stdlib only, single file | Python ≥3.10 package |
| Path | Used by | Extra preconditions |
|---|
SQLi-to-admin bridge (forge oEmbed/changeset/nav rows → POST /wp/v2/users → login → plugin upload) | this repo, sergiointel (origin), Icex0, 0xsha | none beyond a default install |
INTO OUTFILE dropper (write a PHP file via SQLi, fetch it for a shell) | OUTFILE variant [4] | MySQL FILE privilege, secure_file_priv allowing it, and a directory writable by mysqld and served by the web server |
Hash recovery → crack → login (dump user_pass, crack offline, then plugin upload) | all (as fallback) | the bcrypt hash must actually crack ($wp$2y$, hashcat -m 35500) - slow, often never |