WordPress Core <= 7.1.1 неаутентифицированный LFI до RCE — лаборатория валидации, PoC, шаблон nuclei (GHSA-7hp8-65ch-5whp)
Материалы для исследования безопасности, предназначенные для воспроизведения и валидации CVE-2026-87902 (GHSA-7hp8-65ch-5whp) в изолированной Docker-лаборатории, доступной только через loopback: неаутентифицированное локальное включение файлов (LFI) в механизме разрешения шаблонов страниц ядра WordPress, эскалируемое до удалённого выполнения кода.
AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)Статус: доказано в предоставленной лаборатории.
WordPress 7.1.1 (тема Twenty Twelve) позволяет неаутентифицированному атакующему
включать PHP-файлы из-за пределов директории темы через переменную запроса pagename, и - при стандартном register_argc_argv = On официального образа PHP - записывать и выполнять файл-маркер через PEAR pearcmd.php:
WordPress: 7.1.1
Theme: twentytwelve (page-templates/ present)
PHP: 8.2 (official image, register_argc_argv=On, PEAR present)
Result: LFI oracle fired (<opml version="1.0">); marker probe executed as
uid=33(www-data) and self-deleted
WordPress 7.1.2 с идентичной конфигурацией полностью блокирует оракул (негативный контроль). Это демонстрирует выполнение кода от имени пользователя веб-сервера WordPress внутри контейнера. Это не демонстрирует root-доступ, выход из контейнера, боковое перемещение или компрометацию хоста.
Требования: Docker с Compose v2, Python 3.10+, сетевой доступ к Docker Hub и wordpress.org (установка темы).
./lab verify
Это собирает уязвимую лабораторию, требует срабатывания LFI-оракула и RCE-маркера, затем собирает пропатченную лабораторию и требует, чтобы оракул молчал.
Чтобы использовать другой loopback-порт: LAB_PORT=9499 ./lab verify.
Удалить всё после этого с помощью ./lab reset.
Уязвимый контроль:
[+] page-* folder(s) reachable: page-templates (traversal can resolve)
[!!!] LFI CONFIRMED (safe oracle: wp-links-opml.php)
...
[RCE] pearcmd.php reachable and taking argv: /usr/local/lib/php/pearcmd.php
[!!!] RCE CONFIRMED (pearcmd.php config-create file write):
evidence: uid=33(www-data) (command `id` ran as the web user)
cleanup : probe self-deleted on execution: confirmed
RESULT: VULNERABLE CONTROL CONFIRMED
Пропатченный контроль:
[=] oracles did not fire: target patched (>= 7.1.2), ...
RESULT: PATCHED CONTROL CONFIRMED
HTTP 200, блокировка WAF или частичный обход без маркера оракула не принимаются в качестве доказательства.
./lab start vulnerable # fresh 7.1.1 lab on 127.0.0.1:9471
./lab test # assert LFI + RCE
./lab start patched # fresh 7.1.2 lab
./lab test # assert oracle absence
./lab status
./lab reset # containers, volumes, state
Запуск инструментов вручную против работающей лаборатории:
python3 poc/triage_cve_2026_87902.py --url http://127.0.0.1:9471
python3 poc/poc_cve_2026_87902.py --url http://127.0.0.1:9471 --rce
nuclei -t nuclei/cve-2026-87902.yaml -u http://127.0.0.1:9471 -silent
Для систем, которыми вы владеете или которые явно авторизованы для тестирования (область bug-bounty, письменное соглашение), драйверы требуют явного переопределения:
python3 poc/poc_cve_2026_87902.py --url https://target.example \
--allow-authorized-non-loopback [--insecure] [--rce]
get_page_template() в wp-includes/template.php строит имя файла-кандидата шаблона
из переменной запроса pagename. Соседняя ветка тремя строками выше передаёт
slug шаблона страницы через validate_file(); ветка pagename никогда этого не делала
и дополнительно добавляет лишнего кандидата, декодированного через urldecode():
// wp-includes/template.php, get_page_template(), WordPress <= 7.1.1
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) { // 7.1.2 adds validate_file()
$templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";
}
7.1.2 применяет validate_file() к декодированному кандидату и добавляет
шлюз ограничения (_wp_is_template_path_allowed()), который должен пройти
каждый разрешённый путь шаблона.
locate_template() проверяет обе)
содержит директорию верхнего уровня, начинающуюся с page-. Устаревшие темы по умолчанию
(Twenty Twelve, Twenty Fourteen) и несколько тем с wp.org (Neve, Hestia,
Sydney) подходят. Исследованные коммерческие темы (Divi, Avada, Flatsome,
Woodmart, Betheme, The7, Enfold, Salient, Jupiter/X, Porto, Houzez, XStore,
Kalium, Uncode) не содержат ни одной - обход не может разрешиться; см.
PRO_THEME_PAGE_DIRS в PoC для списка, полученного из зеркала..php (добавляется кодом).register_argc_argv = On и pearcmd.php из PEAR.Рекомендация и статья Patchstack не публикуют рабочий запрос. Эти детали были установлены эмпирически в данной лаборатории:
page_id обязателен. Неразрешённый pagename переключает
is_404, чей шаблон полностью затеняет get_page_template(). Привяжите
запрос к любому разрешимому опубликованному page_id.get_posts() перезаписывает pagename
через sanitize_title_for_query( wp_basename( … ) ): сырой .. становится
-, но последовательности %hex выживают; дополнительный urldecode() в
get_page_template() раскрывает обход. Работает только %252e/%252f.redirect_canonical делает 301 для GET и отбрасывает
полезную нагрузку; он немедленно возвращается для не-GET/HEAD).../ от page-templates/ на стандартном docroot.$_POST в переменные запроса)
$argv, а
сериализатор конфигурации PEAR экранирует оба стиля кавычек - отсюда сырой,
без кавычек, разделённый + зонд.| Путь | Назначение |
|---|---|
poc/poc_cve_2026_87902.py | Полный PoC: версия + фингерпринт WAF, перечисление темы/папок page-* (wp.org SVN, обнаружение ссылок на ресурсы, исследованная карта pro-тем), обнаружение якоря, 8-вариантный LFI-оракул, сравнительный оракул пустого тела как запасной вариант, цепочка --rce pearcmd, --lab docker демо |
poc/triage_cve_2026_87902.py | Вердикт по цели или пакету с указанием не прошедшего шлюза (EXPLOITABLE / NOT EXPLOITABLE / WAF SUSPECTED / INCONCLUSIVE) |
poc/bounty_screen.py | Обрабатывает входящий в область список активов bug-bounty: расширение wildcard через crt.sh → фильтр WP → проверка предусловий → передача кандидатов |
nuclei/cve-2026-87902.yaml | Шаблон Nuclei: безопасный LFI-оракул (174 полезные нагрузки: 10 остатков page-*, уровни двойного/тройного кодирования, ids 1–30) |
Фингерпринтинг WAF охватывает Imperva/Incapsula, Cloudflare, Akamai, AWS
CloudFront/WAF, Sucuri, F5. Восемь вариантов оракула: двойное кодирование, альтернативное
написание %25%32%65, загрязнение параметров, тройное кодирование - каждый в строке
запроса и теле POST. Multipart-тела и полезные нагрузки с сырым слешем были протестированы и
отклонены (WP не обрабатывает их для этого стока).
| Версия | Оценка |
|---|---|
| WordPress 7.1.1 | LFI + pearcmd RCE воспроизведены от начала до конца |
| WordPress 6.5.3 / 5.9.17 | LFI-оракул воспроизведён |
| WordPress 4.7.0–7.1.1 (другие ветки) | Заявлены как затронутые; таблица веток согласно GHSA |
| WordPress 7.1.2 | Воспроизведён пропатченный негативный контроль |
.
├── .github/workflows/validate.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── lab
├── nuclei/
│ └── cve-2026-87902.yaml
└── poc/
├── bounty_screen.py
├── poc_cve_2026_87902.py
└── triage_cve_2026_87902.py
Не коммитится: .lab-state, __pycache__/, выходные файлы скринера.
| Утверждение | Статус |
|---|---|
| Неаутентифицированный LFI за пределами директории темы | Доказано (7.1.1, 6.5.3, 5.9.17) |
RCE через pearcmd.php config-create от имени веб-пользователя | Доказано (лаборатория 7.1.1) |
| Самоудаление зонда / отсутствие постоянных артефактов | Доказано |
| Пропатченный негативный контроль (7.1.2) | Доказано |
| Каждый промежуточный затронутый релиз протестирован | Не тестировалось |
| Варианты обхода WAF против живых коммерческих WAF | Получены в лаборатории, не тестировались на WAF |
| Root-привилегии / выход из контейнера / компрометация хоста | Не заявлено |
page_id и двойному кодированию; источник
сравнительного оракула пустого тела wp-content/index.php, включённого здесь как
запасной вариант для укреплённых сайтов)Уязвимость обнаружена Robert Ressl. Этот репозиторий является независимым сторонним анализом и инструментарием.
MIT - см. LICENSE.
Запускайте этот репозиторий только на системах, которыми вы владеете или которые явно авторизованы для
тестирования. Драйвер лаборатории отказывается от вариантов, кроме vulnerable/patched,
Python-драйверы отказываются от не-loopback целей, если не указан
--allow-authorized-non-loopback, а порт Compose привязан
к 127.0.0.1.
См. SECURITY.md для обработки раскрытия. Этот репозиторий не включает производственные цели, данные клиентов, токены доступа или доказательства из реальных систем.
Конкретно:
127.0.0.1.wp-links-opml.php, стандартный файл ядра, который выводит документ OPML.--rce включается по желанию и записывает без кавычек, самоудаляющийся маркерный зонд
(<?=id,unlink(__FILE__)?>) через публично документированную технику pearcmd.php
config-create; зонд удаляет себя при единственном выполнении,
и PoC проверяет удаление. Никаких шеллов, обратных вызовов или персистентности.start; никакой
инструментации цели.