
Laboratorio Docker A/B + PoC para CVE-2026-32475 (Elementor Pro Forms: subida arbitraria de archivos sin autenticación -> RCE mediante desincronización del bucle de validación/movimiento)
Laboratorio A/B en Docker que reproduce CVE-2026-32475 (CVSS 9.x, Patchstack; campo de carga de archivos de Elementor Pro Forms, afectado ≤ 4.2.1, corregido en 4.2.2 el 2026-08-19; reportado por Tin Pham / TF1T). Sin autenticación, sin interacción del usuario.
Solo para pruebas autorizadas / fines educativos. Este laboratorio se ejecuta íntegramente en
127.0.0.1en contenedores Docker desechables. No apunte el PoC a sistemas para los que no tenga autorización explícita de prueba.build.shobtiene el código fuente de Elementor Pro desde un espejo GPL público; no se redistribuye código premium en este repositorio.
modules/forms/fields/upload.php. Dos bucles iteran sobre el mismo $_FILES['form_fields'][$id] reformateado, pero tratan una parte vacía () de forma diferente:
UPLOAD_ERR_NO_FILE// validation() — línea ~269
foreach ( $_FILES['form_fields'][ $id ] as $index => $file ) {
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // <-- abandona TODA la validación restante de este campo
}
...
if ( ! $this->is_file_type_valid( $field, $file ) ) { // lista de permitidos/denegados por extensión
$ajax_handler->add_error( $id, 'This file type is not allowed.' );
}
}
// process_field() — línea ~418
foreach ( $_FILES['form_fields'][ $id ] as $index => $file ) {
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // <-- omite SOLO la parte vacía, continúa con el resto
}
$filename = uniqid() . '.' . pathinfo( $file['name'], PATHINFO_EXTENSION ); // extensión del atacante, sin revalidación
move_uploaded_file( $file['tmp_name'], trailingslashit( $uploads_dir ) . $filename );
}
Envíe un campo de carga opcional con dos partes — primero una vacía, luego el payload PHP. validation() encuentra la parte vacía y ejecuta return antes de comprobar jamás el .php; process_field() ejecuta continue sobre la parte vacía y mueve el .php a wp-content/uploads/elementor/forms/<uniqid()>.php. La lista de bloqueo get_blacklist_file_ext() (php,phtml,pht,shtml,…) solo se ejecuta dentro de validation(), por lo que se omite por completo.
Totalmente sin autenticación: wp_ajax_nopriv_elementor_pro_forms_send_form (ajax-handler.php:295).
get_ensure_upload_dir() coloca un .htaccess en el directorio de formularios que contiene únicamente Options -Indexes + Header set Content-Disposition attachment. Eso es una pista de descarga en navegador — en Apache + mod_php el .php aún se ejecuta en el servidor; el atacante (curl) simplemente recibe la salida del comando con una cabecera de descarga. El módulo de formularios no contiene limpieza con unlink, por lo que el shell persiste.
Una página publicada con un widget de Formulario de Elementor Pro que tenga un campo de carga de archivos con Requerido = No (la rama ! $field['required']). La configuración del campo se lee del lado del servidor desde _elementor_data, por lo que el atacante no puede cambiarla — pero los campos de "adjunto opcional" son comunes.
Nombre almacenado = uniqid() = %08x%05x = (segundo-unix)(microsegundo). El segundo se filtra exactamente mediante la cabecera de respuesta HTTP Date; solo se desconoce el microsegundo (0–999999) → un ataque de fuerza bruta GET en línea acotado a ≤10⁶. poc.py --recover ancla el microsegundo en el tiempo de llegada de la respuesta (co-ubicado / sincronizado con NTP ⇒ segundos–minutos; aquí: ~26k solicitudes) y convierte la escritura en ejecución remota de código.
| Servicio | Puerto | Elementor Pro |
|---|---|---|
wp-vuln | http://127.0.0.1:8975 | 3.6.4 — vulnerable (árbol auténtico; desincronización de validation() idéntica a ≤4.2.1) |
wp-patched | http://127.0.0.1:8976 | 3.6.4 con el fix equivalente a 4.2.2 (return → continue, alineando los bucles) |
Stack: wordpress:php7.4-apache (mod_php) + MariaDB 10.6 + Elementor (gratuito) 3.6.8 + Elementor Pro 3.6.4. Cada variante tiene un administrador (admin/labpass) y una página publicada "CVE-2026-32475 Lab" (post_id=5, form_id=frm00001, campo de carga field_cv, Requerido=No).
Nota de versión. No hay código fuente limpio de 4.2.1/4.2.2 redistribuible públicamente, por lo que el laboratorio ejecuta el árbol auténtico 3.6.4, cuya desincronización de
validation()/process_field()es byte-idéntica al código que describe CVE-2026-32475 (el error es de larga latencia; 4.2.2 alineó los dos bucles). La compilación A/B "parcheada" aplica exactamente esa alineación.
Requiere Docker + Docker Compose y red saliente (para extraer imágenes, Elementor gratuito y el espejo del código fuente de Elementor Pro). build.sh clona automáticamente el árbol auténtico de Elementor Pro.
git clone https://github.com/dinosn/cve-2026-32475-elementor-pro-lab
cd cve-2026-32475-elementor-pro-lab
bash build.sh # clonar plugin + compose up + instalar WP/Elementor + crear página de formulario (ambas variantes)
bash verify.sh # prueba A/B: la vulnerable escribe+ejecuta un shell; la parcheada lo rechaza. Limpia.
# RCE sin autenticación totalmente remota (extrae automáticamente post_id/form_id/field_id de la página):
python3 poc.py -t http://127.0.0.1:8975 --page-id 5 --recover -c "id; uname -a"
# solo para demostrar la omisión de la lista de bloqueo (deja el shell para inspección):
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), vulnerable:8975 / parcheado:8976elementor-pro-vuln/ — Elementor Pro 3.6.4 auténtico (vulnerable); generado por build.sh, ignorado por gitelementor-pro-patched/ — mismo árbol, validation() return→continue; generado por patch_pro.sh, ignorado por gitevidence/verify_transcript.txt — una ejecución capturada de verify.sh que pasainstall_wp.sh — instalación wp-cli de WP + Elementor gratuito + activar Pro + crear página de formulario + corregir permisos de uploadssetup_page.php — construye la página de formulario de Elementor con un campo de carga opcionalpatch_pro.sh — deriva el árbol parcheado + imprime el diff de una líneapoc.py — carga desincronizada sin autenticación + recuperación del nombre de archivo uniqid() + RCEverify.sh — prueba A/B con un oráculo de sistema de archivos independiente (prueba la ejecución, no solo la escritura)build.sh — compilación de una sola ejecucióncd /root/cve-2026-32475-lab && docker compose down -v