
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 повторно выполнить собственный конвейер запросов от имени этого администратора — окно, которого как раз хватает, чтобы поставленный в очередь вызов создал нового администратора.
Для чтения (--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
$wp$2y$… / $P$… / $2y$…) передаются напрямую в hashcat (-m 3200 для phpass или режим WP bcrypt). WordPress 6.8+ использует bcrypt $wp$….--dump по умолчанию извлекает данные только первого администратора. Используйте --dump all для получения полного списка администраторов.--oracle time --dump, чтобы получить хэши.Сценарий --exec спроектирован так, чтобы не оставлять ничего после себя: временный администратор, его usermeta, сфабрикованные строки oembed_cache и загруженный веб-шелл удаляются до выхода из инструмента. Флага --keep нет — оператор никогда не должен оставлять живую учётную запись администратора, созданную атакующим, в системе клиента.
wp2shell.py — эксплойт (один файл, только стандартная библиотека).Авторство в обнаружении уязвимостей, их раскрытии и оригинальном разборе «wp2shell» принадлежит исследовательской команде Searchlight Cyber / Assetnote:
/batch/v1WP_Query author__not_inЭтот репозиторий — независимая proof-of-concept реализация цепочки эксплойтов, которую они раскрыли; он не претендует на обнаружение лежащих в основе уязвимостей. Если какая-то ссылка на авторство неполна или некорректна, откройте issue, и она будет исправлена.
Это proof of concept для исследований в области безопасности, предназначенный для авторизованного тестирования на проникновение и защитной проверки. Запуск его против систем, которыми вы не владеете или на тестирование которых у вас нет письменного разрешения, незаконен. Автор не несёт ответственности за неправомерное использование.
POST /wp/v2/usersОболочка. Входим под новым администратором, загружаем плагин, выполняем команду. Учётная запись, её метаданные, oembed-строки и веб-шелл после этого полностью удаляются — не остаётся никакого постоянного следа.
| Флаг | Назначение |
|---|
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-под-ответы (отладка) |
| Флаг | Назначение |
|---|
--form | отправлять batch как multipart/form-data (чистый URL, без ?rest_route=) |
--permalink [PREFIX] | направлять запрос через /<PREFIX>/batch/v1 (по умолчанию wp-json); payload не попадает ни в URL, ни в $_POST. Совет: --permalink index.php/wp-json обращается к REST через PATH_INFO, обходя правила, привязанные к /wp-json |
--dummy N | добавлять N байт мусора в тело запроса, чтобы переполнить буфер инспекции WAF |
--donor {widgets,categories,tags} | переключить donor-маршрут путаницы, если какой-то из них отдаёт 500 (по умолчанию widgets) |
--per-page N | подделывать per_page (-1 и большое положительное значение одинаково возвращают все строки; используйте большое положительное значение, если цель отдаёт 500 на -1) |
--users-route PATH | альтернативный регистр/запрос для вызова создания пользователя (/wp/v2/Users, …?_=1) |
--primer PATH | альтернативный повреждённый путь для desync, если WAF распознаёт стандартный http://: |
--user-agent UA | зафиксировать UA (по умолчанию — случайный реалистичный браузерный UA при каждом запуске) |
--embed-base URL | переопределить базовый oembed URL (чтобы избежать loopback при self-embed) |