
wp2shell (CVE-2026-63030 & CVE-2026-60137) - vollständige RCE-Kette
Unabhängiger Proof-of-Concept für die nicht authentifizierte WordPress REST Batch-Route-Confusion SQL-Injection im Zusammenhang mit Searchlight Cyber's wp2shell Advisory.
Dieses Repository ist nicht Searchlight Cyber's offizieller Checker. check bestätigt den SQLi-Pfad, read demonstriert das Datenbanklesen und shell öffnet eine plugin-gestützte Befehlsshell entweder mit bereitgestellten Administrator-Anmeldedaten oder durch vorheriges Ausführen der SQLi-to-Admin-Brücke.
Searchlight Cyber's Advisory listet diese wp2shell RCE-Betroffenenbereiche auf:
| Versionsbereich | Status |
|---|---|
| <= 6.8.5 | Nicht betroffen |
| 6.9.0 – 6.9.4 | Betroffen |
| 7.0.0 – 7.0.1 | Betroffen |
Der REST Batch-Endpunkt (/batch/v1) ist nicht authentifiziert und führt mehrere Unteranfragen in einem Aufruf aus, wobei darauf vertraut wird, dass jede Unteranfrage einzeln validiert und auf Berechtigungen geprüft wird.
serve_batch_request_v1() erstellt zwei parallele Arrays — $matches (der zugeordnete Handler pro Unteranfrage) und $validation (das Validierungsergebnis pro Unteranfrage) — und indiziert beide beim Versand mit demselben Offset. Eine Unteranfrage, deren Pfad bei wp_parse_url() scheitert, wird an $validation angehängt, aber nicht an $matches, sodass die Arrays aus dem Tritt geraten und eine Unteranfrage unter einem anderen Handler der Unteranfrage versendet wird. Das ist die Routenverwirrung.
Der PoC verschachtelt die Basisoperation zweimal:
POST /wp/v2/posts-Anfrage, die einen requests-Body enthält, wird unter dem Batch-Handler selbst versendet. Nachdem sie als Posts-Anfrage validiert wurde, wird ihre requests-Liste nie gegen das Batch-Schema geprüft, sodass ihre Unteranfragen GET verwenden können – die Methoden-Whitelist wird umgangen.GET /wp/v2/posts/999999-Item-Route-Anfrage Query-Parameter der Posts-Collection wie author_exclude, orderby und per_page. Die ID 999999 muss nicht existieren; es ist nur eine unwahrscheinliche Post-ID, die verwendet wird, um die Item-Route zu treffen, deren Schema diese nur für Collections bestimmten Parameter nicht validiert. Die Desynchronisation versendet dann dieselbe Anfrage unter get_items() der Posts, wo author_exclude auf die WP_Query-Abfragevariable author__not_in abgebildet wird, die die verwundbare Version als String in SQL interpoliert.Das Ergebnis ist eine boolesche und zeitbasierte blinde SQL-Injection, die ohne Authentifizierung erreichbar ist. Dieser PoC enthält auch das UNION-Fake-Post-Primitiv, das in der SQLi-to-Admin-Kette verwendet wird.
Der hier implementierte RCE-Pfad ist:
wp_posts-Zeilen, um vom Angreifer kontrollierte Inhalte durch eine Posts-Collection zu rendern. Die Render-Brücke verwendet die /wp/v2/posts/999999-Item-Route-Quelle – dieselbe Route, die der SQLi-Read verwendet, um get_items() zu erreichen.POST /wp/v2/users erreichen, wodurch ein generierter Administrator erstellt wird.Schritte 1–5 sind ohne Authentifizierung; der Schritt der Befehlsausführung ist ein authentifizierter Admin-Plugin-Upload.
Python 3.8+ und die Standardbibliothek. Keine Abhängigkeiten von Drittanbietern.
Führe es aus dem Repository-Verzeichnis aus:
./wp2shell.py <command> <url> [options]
Oder pip install ., um einen wp2shell-Befehl in deinem PATH zu erhalten.
Gibt zunächst passive WordPress-Marker und öffentliche Versionshinweise aus, sendet dann eine gutartige Batch-Marker-Sonde. Eine verwundbare Batch-Implementierung gibt HTTP 207 mit dem Routenverwirrungs-Markermuster parse_path_failed, block_cannot_read und rest_batch_not_allowed zurück.
Die Marker-Sonde basiert auf dem WordPress-Core-Fix. Die fehlerhafte ///-Anfrage erzeugt parse_path_failed; eine /wp/v2/posts-Anfrage fungiert als batch-erlaubter Spacer; die /wp/v2/block-renderer/...-Route ist nicht batch-erlaubt, gibt aber block_cannot_read zurück, wenn ihr Handler anonym erreicht wird; /batch/v1 ergibt rest_batch_not_allowed. Bei verwundbaren Versionen verschiebt der Parse-Fehler die Batch-Handler-Arrays aus dem Tritt, sodass die Spacer-Anfrage unter dem Block-Renderer-Handler versendet wird. Bei behobenen Versionen bleiben die Arrays ausgerichtet, sodass dieses exakte Drei-Muster für die konstruierte Sonde nicht auftreten sollte.
Standardmäßig stoppt check dort und sendet kein SQLi-Payload. Verwende --confirm-sqli, wenn du auch eine aktive SQLi-Bestätigung wünschst. Die Bestätigung versucht zuerst das UNION-Read-Primitiv und fällt auf gepaarte Timing-Sonden zurück, wenn die UNION-Reflexion nicht verfügbar ist.
Die Signale sind unabhängig: ein Versionshinweis ist nur ein Hinweis, das Markermuster zeigt Routenverwirrung, und --confirm-sqli zeigt, dass ein Payload die Datenbank erreicht hat. Eine WAF kann das Payload blockieren, daher beweist eine fehlgeschlagene Bestätigung nicht die Abwesenheit des Fehlers.
./wp2shell.py check http://target
./wp2shell.py check targets.txt # scan every URL in the file
./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"
Standardmäßig erfolgt die Extraktion mit --technique auto, was die verfügbaren Methoden in dieser Reihenfolge versucht:
WP_Post-Zeile mittels UNION und liest deren Titel aus der REST-Antwort als ||HEX(value)||. Das Payload verwendet dieselbe /wp/v2/posts/999999-Quellroute mit orderby=none und per_page=500, sodass die gefälschte Zeile als gerenderter Post überlebt. Eine Anfrage pro Wert.EXTRACTVALUE/UPDATEXML gibt ~15 Bytes pro Anfrage preis, wenn das Ziel MySQL-Fehler widerspiegelt (z.B. WP_DEBUG_DISPLAY an).X-WP-Total-Header der Posts-Collection als True/False-Signal und benötigt keinen reflektierten Wert.Erzwinge eine mit --technique union|error|blind. Diese Lesepfade schreiben keine Datenbankzeilen.
Mit --user und --password meldet sich shell mit bereitgestellten Administrator-Anmeldedaten an und verwendet das WordPress-Plugin-Upload-Verhalten.
Ohne Anmeldedaten führt shell zuerst die Pre-Auth-SQLi-to-Admin-Brücke aus, meldet sich als der generierte Administrator an und lädt dann die Plugin-Shell hoch.
./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recovered>' -i # interactive shell
./wp2shell.py shell http://target --cmd id # pre-auth bridge
./wp2shell.py shell http://target -i # pre-auth interactive