
Laboratoire Docker A/B + PoC pour CVE-2026-32475 (téléversement arbitraire de fichier non authentifié dans Elementor Pro Forms -> RCE via désynchronisation de la boucle validation/déplacement)
Laboratoire Docker A/B qui reproduit CVE-2026-32475 (CVSS 9.x, Patchstack ; champ de téléversement de fichier des formulaires Elementor Pro, versions affectées ≤ 4.2.1, corrigé en 4.2.2 le 2026-08-19 ; signalé par Tin Pham / TF1T). Non authentifié, sans interaction utilisateur.
Tests autorisés / usage éducatif uniquement. Ce laboratoire s'exécute entièrement sur
127.0.0.1dans des conteneurs Docker jetables. Ne pointez pas le PoC vers des systèmes que vous n'êtes pas explicitement autorisé à tester.build.shrécupère le code source d'Elementor Pro depuis un miroir GPL public ; aucun code premium n'est redistribué dans ce dépôt.
modules/forms/fields/upload.php. Deux boucles itèrent sur le même $_FILES['form_fields'][$id] reformaté mais traitent une partie vide () différemment :
UPLOAD_ERR_NO_FILE// validation() — ligne ~269
foreach ( $_FILES['form_fields'][ $id ] as $index => $file ) {
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // <-- abandonne TOUTE la validation restante de ce champ
}
...
if ( ! $this->is_file_type_valid( $field, $file ) ) { // liste d'autorisation/interdiction d'extensions
$ajax_handler->add_error( $id, 'This file type is not allowed.' );
}
}
// process_field() — ligne ~418
foreach ( $_FILES['form_fields'][ $id ] as $index => $file ) {
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // <-- ignore UNIQUEMENT la partie vide, continue
}
$filename = uniqid() . '.' . pathinfo( $file['name'], PATHINFO_EXTENSION ); // extension attaquant, pas de re-vérification
move_uploaded_file( $file['tmp_name'], trailingslashit( $uploads_dir ) . $filename );
}
Envoyez un champ de téléversement optionnel avec deux parties — une vide d'abord, puis la charge utile PHP. validation() atteint la partie vide et fait return avant même de vérifier le .php ; process_field() fait continue au-delà de la partie vide et déplace le .php vers wp-content/uploads/elementor/forms/<uniqid()>.php. La liste noire get_blacklist_file_ext() (php,phtml,pht,shtml,…) ne s'exécute que dans validation(), elle est donc complètement contournée.
Totalement non authentifié : wp_ajax_nopriv_elementor_pro_forms_send_form (ajax-handler.php:295).
get_ensure_upload_dir() dépose un .htaccess dans le dossier des formulaires contenant uniquement Options -Indexes + Header set Content-Disposition attachment. C'est une indication de téléchargement navigateur — sur Apache + mod_php, le .php s'exécute toujours côté serveur ; l'attaquant (curl) reçoit simplement la sortie de la commande avec un en-tête de téléchargement. Le module de formulaires ne contient aucun nettoyage unlink, donc le shell persiste.
Une page publiée avec un widget Formulaire Elementor Pro comportant un champ de téléversement de fichier dont Requis = Non (la branche ! $field['required']). La configuration du champ est lue côté serveur depuis _elementor_data, donc l'attaquant ne peut pas la modifier — mais les champs « pièce jointe optionnelle » sont courants.
Nom stocké = uniqid() = %08x%05x = (seconde unix)(microseconde). La seconde est divulguée exactement par l'en-tête de réponse HTTP Date ; seule la microseconde (0–999999) est inconnue → une force brute GET en ligne bornée ≤10⁶. poc.py --recover ancre la microseconde sur l'heure d'arrivée de la réponse (co-localisé / synchronisé NTP ⇒ secondes–minutes ; ici : ~26k requêtes) et transforme l'écriture en exécution de code à distance.
| Service | Port | Elementor Pro |
|---|---|---|
wp-vuln | http://127.0.0.1:8975 | 3.6.4 — vulnérable (arborescence authentique ; désynchronisation validation() identique à ≤4.2.1) |
wp-patched | http://127.0.0.1:8976 | 3.6.4 avec le correctif équivalent à 4.2.2 (return → continue, alignant les boucles) |
Pile : wordpress:php7.4-apache (mod_php) + MariaDB 10.6 + Elementor (gratuit) 3.6.8 + Elementor Pro 3.6.4.
Chaque variante possède un administrateur (admin/labpass) et une page publiée « CVE-2026-32475 Lab » (post_id=5, form_id=frm00001, champ de téléversement field_cv, Requis=Non).
Note de version. Aucun code source propre 4.2.1/4.2.2 n'est publiquement redistribuable, donc le laboratoire exécute l'arborescence authentique 3.6.4, dont la désynchronisation
validation()/process_field()est octet pour octet identique au code décrit par CVE-2026-32475 (le bug est latent depuis longtemps ; 4.2.2 a aligné les deux boucles). La construction A/B « corrigée » applique exactement cet alignement.
Nécessite Docker + Docker Compose et un accès réseau sortant (pour tirer les images, Elementor gratuit et le miroir du code source d'Elementor Pro). build.sh clone automatiquement l'arborescence authentique d'Elementor Pro.
git clone https://github.com/dinosn/cve-2026-32475-elementor-pro-lab
cd cve-2026-32475-elementor-pro-lab
bash build.sh # clone du plugin + compose up + installation WP/Elementor + création de la page de formulaire (les deux variantes)
bash verify.sh # preuve A/B : la vulnérable écrit+exécute un shell ; la corrigée rejette. Nettoie.
# RCE non authentifiée entièrement à distance (extraction automatique de post_id/form_id/field_id depuis la page) :
python3 poc.py -t http://127.0.0.1:8975 --page-id 5 --recover -c "id; uname -a"
# prouver simplement le contournement de la liste noire (laisser le shell pour inspection) :
python3 poc.py -t http://127.0.0.1:8975 --post-id 5 --form-id frm00001 --field-id field_cv
docker-compose.yml — 2× (WordPress + MariaDB), vuln:8975 / patched:8976elementor-pro-vuln/ — Elementor Pro 3.6.4 authentique (vulnérable) ; généré par build.sh, ignoré par gitelementor-pro-patched/ — même arborescence, validation() return→continue ; généré par patch_pro.sh, ignoré par gitevidence/verify_transcript.txt — une exécution réussie capturée de verify.shinstall_wp.sh — installation wp-cli de WP + Elementor gratuit + activation de Pro + création de la page de formulaire + correction des permissions d'uploadssetup_page.php — construit la page de formulaire Elementor avec un champ de téléversement optionnelpatch_pro.sh — dérive l'arborescence corrigée + affiche le diff d'une lignepoc.py — téléversement désynchronisé non authentifié + récupération du nom de fichier uniqid() + RCEverify.sh — preuve A/B avec un oracle indépendant du système de fichiers (prouve l'exécution, pas seulement l'écriture)build.sh — construction en une foiscd /root/cve-2026-32475-lab && docker compose down -v