
Анализ первопричины, пассивный чекер версий и лабораторный PoC для CVE-2026-18322 — неаутентифицированного повышения привилегий в WordPress-плагине Smart Popup от Supsystic.
Исследование безопасности CVE-2026-18322 — уязвимости повышения привилегий без аутентификации в WordPress-плагине Smart Popup by Supsystic (popup-by-supsystic). Этот репозиторий содержит анализ первопричины, основанный на diff исходного кода upstream, извлечённые патчи, инструмент обнаружения (пассивный фингерпринтер версий) для выявления уязвимых установок и лабораторию для PoC эксплуатации.
Уязвимость публична и исправлена. Эта работа опубликована для защитного использования: чтобы помочь администраторам находить и устранять уязвимые хосты.
| Диапазон версий | Статус |
|---|---|
< 1.13.0 | Уязвима |
>= 1.13.0 | Исправлена |
1.13.0 (выпущена 31.07.2026) — первый исправленный релиз; он устраняет все три звена описанной ниже цепочки. См. §1 анализа.
Три независимые слабости складываются в создание администратора без аутентификации:
1. Коллизия карт разрешений — havePermissions() объединяла две карты разрешений с помощью array_merge(). Обе используют строковый ключ PPS_USERLEVELS ('userlevels'), а array_merge() перезаписывает строковые ключи, поэтому короткий список по умолчанию базового контроллера полностью заменял список модуля popup — молча лишая ограничения администратора метод save и девять других методов:
$permissions = $mod->getController()->getPermissions(); // [... 'save' ...]
$permissionsBase = $mod->getController()->getBasePermissions(); // ['getListForTbl','removeGroup','clear']
$permissions = array_merge($permissions, $permissionsBase); // base wins — 'save' is gone
Проверка отказывает открыто: действие, отсутствующее в карте, никогда не запрещается.
2. Многоразовый nonce, отправляемый по почте посторонним — save всё ещё требовал pps_nonce, но письмо с подтверждением подписки содержало именно это действие nonce. Для неавторизованных пользователей WordPress nonce привязан к uid=0, поэтому токен, созданный для анонимного подписчика, проходит проверку для любого анонимного атакующего.
3. Отсутствие серверного списка разрешённых ролей — createWpSubscriber() передавал настроенную роль напрямую в WP_User::set_role(). Безопасный список ролей плагина (который исключает administrator) применялся только при отрисовке выпадающего списка в админке — контроль только на уровне UI.
В цепочке: подписаться, чтобы получить nonce → воспроизвести его против popup::save, чтобы установить sub_wp_create_user_role=administrator → запустить поток подписки → постоянная учётная запись администратора.
Полный разбор со ссылками на файлы/строки: docs/ANALYSIS.md.
poc/cve_2026_18322_check.py определяет, работает ли сайт на уязвимой версии. Он никогда не пытается эксплуатировать уязвимость: он отправляет обычные HTTP GET-запросы и читает версию, которую установка сообщает о себе.
git clone <this-repo> && cd CVE-2026-18322
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
requests рекомендуется, но необязателен — инструмент переключается на стандартную библиотеку.
# Single target
python3 poc/cve_2026_18322_check.py https://example.com
# With supporting evidence
python3 poc/cve_2026_18322_check.py https://example.com -v
# Many targets, concurrently, exporting results
python3 poc/cve_2026_18322_check.py -f targets.txt -t 20 --json results.json
python3 poc/cve_2026_18322_check.py -f targets.txt --csv results.csv --only-vulnerable
# Through a proxy, ignoring TLS errors (lab use)
python3 poc/cve_2026_18322_check.py https://staging.internal -k --proxy http://127.0.0.1:8080
Цели можно указывать как аргументы или через -f (- читает stdin). Голое имя хоста предполагается как https://.
$ python3 poc/cve_2026_18322_check.py https://example.com -v
[VULNERABLE] https://example.com/ (Smart Popup by Supsystic 1.11.2) < 1.13.0 affected by CVE-2026-18322; upgrade to 1.13.0+
- asset-version: plugin asset enqueued with ?ver=1.11.2 (https://example.com/)
- homepage-reference: references /plugins/popup-by-supsystic/ (https://example.com/)
- readme: plugin readme.txt is publicly readable (https://example.com/wp-content/plugins/popup-by-supsystic/readme.txt)
- readme-stable-tag: Stable tag: 1.11.2 (.../readme.txt)
| Статус | Значение |
|---|---|
VULNERABLE | Плагин обнаружен версии < 1.13.0 |
NOT_VULNERABLE | Плагин обнаружен версии >= 1.13.0 |
PLUGIN_DETECTED_VERSION_UNKNOWN | Плагин присутствует, версию определить не удалось — проверьте вручную |
PLUGIN_NOT_DETECTED | Нет свидетельств наличия плагина (не доказательство отсутствия) |
ERROR | Цель недоступна или некорректна |
Коды выхода: 0 = уязвимых целей нет, 1 = как минимум одна уязвима, 2 = ошибка использования.
Два пассивных сигнала, оба — обычные HTTP GET-запросы к публичным ресурсам:
?ver=PPS_VERSION (classes/frame.php:410,473), поэтому главная страница часто раскрывает точную версию.readme.txt — Stable tag: авторитетен и имеет приоритет над потенциально устаревшим кэшированным ресурсом; changelog служит запасным вариантом.Сканирование главной страницы также обнаруживает переименованные каталоги wp-content (например, /app/ в Bedrock), поэтому поиск readme следует реальной структуре сайта.
Инструмент обнаружения не пытается эксплуатировать уязвимость. Никакого боевого эксплойта для этой CVE здесь не публикуется: единственный код, выполняющий полную цепочку, жёстко ограничен локальной лабораторией (см. ниже).
Пассивный чекер выполняет только фингерпринтинг версий. Его HttpClient не предоставляет никакого HTTP-метода, кроме GET — это обеспечено конструкцией и проверяется в наборе тестов, поэтому он не может отправить изменяющий состояние запрос даже по ошибке. Он никогда не вызывает popup::save, не отправляет параметр sub_wp_create_user_role, не отправляет форму подписки, не инициирует письмо с подтверждением и не создаёт и не изменяет ни пользователя, ни popup, ни настройку. Он читает два публичных ресурса — главную страницу и readme.txt — и ничего больше.
У этой безопасности есть цена, и о ней сказано прямо: вердикт настолько хорош, насколько хороши метаданные версии, публикуемые хостом. Компромиссы и режимы отказа: §9.