
wp2shell (CVE-2026-63030 & CVE-2026-60137) - полная цепочка RCE
Независимый доказательный концепт (PoC) для неаутентифицированной SQL-инъекции через путаницу маршрутов (route confusion) batch-эндпоинта REST API WordPress, связанной с рекомендацией wp2shell от Searchlight Cyber.
Этот репозиторий не является официальным чекером Searchlight Cyber. check подтверждает путь SQLi, read демонстрирует чтение базы данных, а shell открывает командную оболочку на основе плагина либо с предоставленными учётными данными администратора, либо сначала выполняя мост SQLi-to-admin.
В рекомендации Searchlight Cyber перечислены следующие диапазоны версий, подверженных RCE через wp2shell:
| Version range | Status |
|---|
| <= 6.8.5 | Не затронута |
| 6.9.0 – 6.9.4 | Затронута |
| 7.0.0 – 7.0.1 | Затронута |
REST batch-эндпоинт (/batch/v1) не требует аутентификации и выполняет несколько подзапросов в одном вызове, полагаясь на то, что каждый подзапрос проверяется и проходит проверку прав доступа самостоятельно.
serve_batch_request_v1() создаёт два параллельных массива — $matches (подобранный обработчик для каждого подзапроса) и $validation (результат проверки для каждого подзапроса) — а затем при диспетчеризации индексирует оба по одному и тому же смещению. Подзапрос, путь которого не проходит wp_parse_url(), добавляется в $validation, но не в $matches, поэтому массивы расходятся, и подзапрос диспетчеризуется под обработчиком другого подзапроса. Это и есть путаница маршрутов.
PoC вкладывает примитив дважды:
POST /wp/v2/posts, содержащий тело requests, диспетчеризуется под самим batch-обработчиком. Поскольку он был проверен как запрос к записям (posts), его список requests никогда не проверяется по batch-схеме, поэтому его подзапросы могут использовать GET — список разрешённых методов обходится.GET /wp/v2/posts/999999 к item-маршруту несёт параметры запроса коллекции записей, такие как author_exclude, orderby и per_page. ID 999999 не обязан существовать; это просто маловероятный ID записи, используемый для сопоставления с item-маршрутом, чья схема не проверяет эти параметры, предназначенные только для коллекций. Из-за рассинхронизации тот же запрос диспетчеризуется под get_items() записей, где author_exclude сопоставляется с query-переменной author__not_in из WP_Query, которую уязвимая сборка интерполирует в SQL как строку.Результат — слепая SQL-инъекция на основе логических условий (boolean-based) и времени (time-based), достижимая до аутентификации. Этот PoC также включает примитив UNION-фейковой записи, используемый в цепочке SQLi-to-admin.
Реализованный здесь путь RCE выглядит так:
wp_posts через UNION, чтобы вывести содержимое, контролируемое атакующим, через коллекцию записей. Мост рендеринга использует источник item-маршрута /wp/v2/posts/999999 — тот же маршрут, который чтение SQLi использует для доступа к get_items().POST /wp/v2/users, создав сгенерированного администратора.Шаги 1–5 выполняются до аутентификации; шаг выполнения команды — это аутентифицированная загрузка плагина администратором.
Python 3.8+ и стандартная библиотека. Без сторонних зависимостей.
Запустите из каталога репозитория:
./wp2shell.py <command> <url> [options]
Или выполните pip install ., чтобы получить команду wp2shell в вашем PATH.
Сначала выводит пассивные маркеры WordPress и публичные подсказки по версии, затем отправляет безвредный маркерный зонд batch. Уязвимая реализация batch возвращает HTTP 207 с паттерном маркеров route-confusion: parse_path_failed, block_cannot_read и rest_batch_not_allowed.
Маркерный зонд основан на исправлении ядра WordPress. Некорректный запрос /// создаёт parse_path_failed; запрос /wp/v2/posts выступает в роли разрешённого для batch разделителя; маршрут /wp/v2/block-renderer/... не разрешён для batch, но возвращает block_cannot_read, если его обработчик вызывается анонимно; /batch/v1 даёт rest_batch_not_allowed. В уязвимых сборках ошибка разбора сдвигает массивы обработчиков batch относительно друг друга, поэтому запрос-разделитель диспетчеризуется под обработчиком block-renderer. В исправленных сборках массивы остаются выровненными, поэтому такой точный паттерн «все три» не должен появляться для созданного зонда.
По умолчанию check останавливается на этом и не отправляет SQLi-полезную нагрузку. Используйте --confirm-sqli, если хотите также получить активное подтверждение SQLi. Подтверждение сначала пробует примитив чтения через UNION, а если отражение UNION недоступно, переключается на парные временные зонды.
Эти сигналы независимы: подсказка по версии — это лишь подсказка, паттерн маркеров показывает путаницу маршрутов, а --confirm-sqli показывает, что полезная нагрузка достигла базы данных. WAF может заблокировать полезную нагрузку, поэтому неудачное подтверждение не доказывает отсутствие ошибки.
./wp2shell.py check http://target
./wp2shell.py check targets.txt # scan every URL in the file
./wp2shell.py read http://target # server fingerprint
./wp2shell.py read http://target --preset users # user logins and password hashes
./wp2shell.py read http://target --query "SELECT @@version"
По умолчанию извлечение выполняется через --technique auto, который перебирает доступные методы в следующем порядке:
WP_Post через UNION и считывает её заголовок из REST-ответа в виде ||HEX(value)||. Полезная нагрузка использует тот же исходный маршрут /wp/v2/posts/999999 с orderby=none и per_page=500, чтобы фейковая строка сохранилась как отображаемая запись. Один запрос на значение.EXTRACTVALUE/UPDATEXML утекают примерно 15 байт за запрос, когда цель отражает ошибки MySQL (например, при включённом WP_DEBUG_DISPLAY).X-WP-Total коллекции записей как сигнал true/false и не требует отражённого значения.Принудительно выберите один из них с помощью --technique union|error|blind. Эти пути чтения не записывают строки в базу данных.
С --user и --password команда shell входит с предоставленными учётными данными администратора и использует поведение загрузки плагинов WordPress.
Без учётных данных shell сначала выполняет мост SQLi-to-admin до аутентификации, входит как сгенерированный администратор, а затем загружает командную оболочку в виде плагина.
./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recovered>' -i # interactive shell
./wp2shell.py shell http://target --cmd id # pre-auth bridge
./wp2shell.py shell http://target -i # pre-auth interactive
shell загружает веб-шелл в виде плагина (защищённый случайным путём и одноразовым токеном на запуск) и выводит его путь. Загруженный веб-шелл удаляется автоматически. Когда мост до аутентификации создаёт администратора, эта сгенерированная учётная запись автоматически удаляется после завершения сеанса shell.
| Option | Applies to | Description |
|---|---|---|
--proxy URL | все | Направлять трафик через HTTP-прокси (например, Burp). |
--timeout N | все | Таймаут запроса в секундах. |
--sleep N | check | Задержка, используемая временным резервным методом для --confirm-sqli. |
--samples N | check | Пары замеров времени, используемые временным резервным методом для --confirm-sqli. |
--confirm-sqli | check | Также отправлять активную подтверждающую SQLi-полезную нагрузку. |
--preset | read | fingerprint или users. |
--technique | read | auto (по умолчанию), union (in-band, создаёт фейковую запись), error (in-band, требует видимых ошибок БД) или blind. |
--query | read | Скалярное SQL-выражение для чтения. |
--prefix | read | Префикс таблиц базы данных (по умолчанию wp_). |
--max-length N | read | Максимальное количество символов, читаемых за одно значение (по умолчанию 128). |
--user / --password | shell | Необязательные учётные данные администратора; опустите оба, чтобы использовать мост до аутентификации. |
--cmd | shell | Команда для выполнения (опустите при использовании -i). |
-i / --interactive | shell | Открыть интерактивную оболочку после развёртывания. |
Обновитесь до WordPress 7.0.2 или до 6.9.5, если сайт находится на ветке 6.9. До этого блокируйте на периметре как /wp-json/batch/v1, так и query-параметр rest_route=/batch/v1, либо требуйте аутентификацию для batch-эндпоинта через фильтр rest_pre_dispatch.
Только для авторизованного тестирования безопасности. Используйте исключительно против систем, которыми вы владеете или на тестирование которых у вас есть явное письменное разрешение. Никаких гарантий не предоставляется, и никакой ответственности за неправомерное использование не принимается.