wp2shell — разбираем от и до.
Это цепочка уязвимостей, которая состоит из таких элементов:
📍 CVE-2026-63030 — уязвимость путаницы маршрутизации конечной точки пакетного REST API. CWE-436.
📍 CVE-2026-60137 — уязвимость внедрения SQL-кода (SQL-инъекция). CWE-89.
wp2shell позволяет неавторизованному злоумышленнику выполнить произвольный код (RCE) в СMS Wordpress. Затронутые версии: 6.8.0–6.8.5; 6.9.0–6.9.4; 7.0.0– 7.0.1.
🫡 Об уязвимости:
Уязвимость находится в функции serve_batch_request_v1, которая обрабатывает пакетные запросы к /wp-json/batch/v1.
Функция создает два массива для обработки входящих подзапросов: $requests[] (сами запросы) и $matches[] (найденные для них обработчики).
Если путь одного из подзапросов некорректный (например, http://), функция wp_parse_url() возвращает false. В этом случае в массив $validation[] записывается ошибка (WP_Error), но запись в массив $matches[] не происходит (через continue).
Ниже показали часть уязвимого кода, полный код находится wp-includes/rest-api/class-wp-rest-server.php
foreach ( $batch_request['requests'] as $args ) {
$parsed_url = wp_parse_url( $args['path'] );
if ( false === $parsed_url ) {
$requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) ); // запись ошибки для http://
continue;
}
$single_request = new WP_REST_Request( $args['method'] ?? 'POST', $parsed_url['path'] );
....
$matches = array();
$validation = array();
$has_error = false;
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
$validation[] = $single_request;
continue; // пропуск записи в $matches[]
}
Из-за continue массивы $requests и $matches рассинхронизируются по индексам. Это позволяет одному подзапросу получить обработчик, предназначенный для другого.
Сдвиг индексов → Некорректный путь вызывает continue → Массивы $requests и $matches рассинхронизируются → Запросы получают чужие обработчики → Вложенный batch → Запрос /wp/v2/posts выполняется как batch → Внутри снова происходит сдвиг индексов → Запрос к /categories?author_exclude=SLEEP(2) получает обработчик /posts (скриншот) -> SQLi.
Уязвимость SQLi
author_exclude регистрируется как параметр типа array в get_collection_params().
В get_items() он мапится в author_not_in без проверки типа.
Из-за путаницы маршрутов параметр передается как строка.
WP_Query не санитизирует строковые значения. Строка попадает в SQL.
if (is_array($query_vars['author__not_in'])) {
$query_vars['author__not_in'] = array_map('absint', ...); // sanitize
}
$author__not_in = implode(',', (array) $query_vars['author__not_in']);
$where .= " AND post_author NOT IN ($author__not_in) "
🫡 Возможные конечные точки:
Запрос:
– POST /wordpress/batch/v1 + тело запроса
– POST /?rest_route=/batch/v1 + тело запроса
– GET /?_method=POST&rest_route=/batch/v1&validation=normal + тело запроса
SQLi - "path": "/wp/v2/ author_exclude=
Пример одного из возможных запросов — на скриншоте.
🫡 Как защищаться:
1. Обновиться до версии 7.0.2.
2. Использовать WAF/IDS с настроенными правилами от SQLi.
3. Проверить систему на предмет подозрительных php-файлов.
4. Провести аудит запросов, где в качестве конечной точки или значения параметра выступал /batch/v1.
5. Временно ограничить доступ к /batch/v1 из внешней сети.
🫡 Ловите лабораторную — docker-compose.yml с уязвимым wordpress прикреплен к посту.
Запуск docker-compose up.
Это цепочка уязвимостей, которая состоит из таких элементов:
📍 CVE-2026-63030 — уязвимость путаницы маршрутизации конечной точки пакетного REST API. CWE-436.
📍 CVE-2026-60137 — уязвимость внедрения SQL-кода (SQL-инъекция). CWE-89.
wp2shell позволяет неавторизованному злоумышленнику выполнить произвольный код (RCE) в СMS Wordpress. Затронутые версии: 6.8.0–6.8.5; 6.9.0–6.9.4; 7.0.0– 7.0.1.
🫡 Об уязвимости:
Уязвимость находится в функции serve_batch_request_v1, которая обрабатывает пакетные запросы к /wp-json/batch/v1.
Функция создает два массива для обработки входящих подзапросов: $requests[] (сами запросы) и $matches[] (найденные для них обработчики).
Если путь одного из подзапросов некорректный (например, http://), функция wp_parse_url() возвращает false. В этом случае в массив $validation[] записывается ошибка (WP_Error), но запись в массив $matches[] не происходит (через continue).
Ниже показали часть уязвимого кода, полный код находится wp-includes/rest-api/class-wp-rest-server.php
foreach ( $batch_request['requests'] as $args ) {
$parsed_url = wp_parse_url( $args['path'] );
if ( false === $parsed_url ) {
$requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) ); // запись ошибки для http://
continue;
}
$single_request = new WP_REST_Request( $args['method'] ?? 'POST', $parsed_url['path'] );
....
$matches = array();
$validation = array();
$has_error = false;
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
$validation[] = $single_request;
continue; // пропуск записи в $matches[]
}
Из-за continue массивы $requests и $matches рассинхронизируются по индексам. Это позволяет одному подзапросу получить обработчик, предназначенный для другого.
Сдвиг индексов → Некорректный путь вызывает continue → Массивы $requests и $matches рассинхронизируются → Запросы получают чужие обработчики → Вложенный batch → Запрос /wp/v2/posts выполняется как batch → Внутри снова происходит сдвиг индексов → Запрос к /categories?author_exclude=SLEEP(2) получает обработчик /posts (скриншот) -> SQLi.
Уязвимость SQLi
author_exclude регистрируется как параметр типа array в get_collection_params().
В get_items() он мапится в author_not_in без проверки типа.
Из-за путаницы маршрутов параметр передается как строка.
WP_Query не санитизирует строковые значения. Строка попадает в SQL.
if (is_array($query_vars['author__not_in'])) {
$query_vars['author__not_in'] = array_map('absint', ...); // sanitize
}
$author__not_in = implode(',', (array) $query_vars['author__not_in']);
$where .= " AND post_author NOT IN ($author__not_in) "
🫡 Возможные конечные точки:
Запрос:
– POST /wordpress/batch/v1 + тело запроса
– POST /?rest_route=/batch/v1 + тело запроса
– GET /?_method=POST&rest_route=/batch/v1&validation=normal + тело запроса
SQLi - "path": "/wp/v2/ author_exclude=
Пример одного из возможных запросов — на скриншоте.
🫡 Как защищаться:
1. Обновиться до версии 7.0.2.
2. Использовать WAF/IDS с настроенными правилами от SQLi.
3. Проверить систему на предмет подозрительных php-файлов.
4. Провести аудит запросов, где в качестве конечной точки или значения параметра выступал /batch/v1.
5. Временно ограничить доступ к /batch/v1 из внешней сети.
🫡 Ловите лабораторную — docker-compose.yml с уязвимым wordpress прикреплен к посту.
Запуск docker-compose up.