
تحليل خطوة بخطوة وإعداد مختبر لـ CVE-2023-39361، حقن SQL غير مصادق عليه في Cacti v1.2.24، مع دليل استغلال وتخفيف.
Cacti هي أداة مراقبة تشغيلية مفتوحة المصدر مكتوبة بلغة PHP وMySQL/MariaDB، وتوفر واجهة سهلة الاستخدام. تم اكتشاف الثغرة الأمنية في عام 2023 والتي أثرت على جميع الإصدارات قبل 1.2.24. يكمن هذا الخلل الأمني في التنفيذ غير السليم عند إدخال القيم في استعلام SQL. هذه ثغرة حقن SQL حرجة تسمح للمهاجمين بـ تعديل قاعدة البيانات وكذلك تنفيذ الأكواد عن بُعد.
في هذا التحليل، سأقوم بتشغيل Cacti في Docker للتبسيط. أولاً، سنقوم بإنشاء ملف docker-compose.yml كما هو موضح أدناه وتشغيل الأمر docker-compose up -d.
version: '3.5'
services:
cacti:
image: "smcline06/cacti"
container_name: CVE-2023-39361
domainname: example.com
hostname: cacti
ports:
- "80:80"
- "443:443"
environment:
- DB_NAME=cacti_master
- DB_USER=cactiuser
- DB_PASS=cactipassword
- DB_HOST=db
- DB_PORT=3306
- DB_ROOT_PASS=rootpassword
- INITIALIZE_DB=1
- TZ=America/Los_Angeles
volumes:
- cacti-data:/cacti
- cacti-spine:/spine
- cacti-backups:/backups
links:
- db
db:
image: "mariadb:10.3"
container_name: CVE-2023-39361_db
domainname: example.com
hostname: db
ports:
- "3306:3306"
command:
- mysqld
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --max_connections=200
- --max_heap_table_size=128M
- --max_allowed_packet=32M
- --tmp_table_size=128M
- --join_buffer_size=128M
- --innodb_buffer_pool_size=1G
- --innodb_doublewrite=ON
- --innodb_flush_log_at_timeout=3
- --innodb_read_io_threads=32
- --innodb_write_io_threads=16
- --innodb_buffer_pool_instances=9
- --innodb_file_format=Barracuda
- --innodb_large_prefix=1
- --innodb_io_capacity=5000
- --innodb_io_capacity_max=10000
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- TZ=America/Los_Angeles
volumes:
- cacti-db:/var/lib/mysql
volumes:
cacti-db:
cacti-data:
cacti-spine:
cacti-backups:
بعد بناء الحاوية، يمكننا الوصول إلى Cacti عن طريق التصفح إلى http://localhost:80. الخطوة التالية هي تحديث Cacti إلى الإصدار 1.2.24. يمكنك تنزيل سكريبت التحديث من هنا: https://pastebin.com/NfRiHLjR. احفظ الملف في نفس الدليل مع ملف docker-compose.yml وأعد تسميته إلى upgrade_cacti.sh. ثم قم بتشغيل هذين الأمرين:
docker cp upgrade_cacti.sh CVE-2023-39361:/tmp/upgrade_cacti.sh && docker exec -it CVE-2023-39361 bash -c "bash /tmp/upgrade_cacti.sh"
الآن نحن جاهزون، دعنا نتعمق في الشفرة لنرى ما حدث الذي سمح لنا بهجوم حقن SQL. الملف المعرض للخطر هو graph_view.php ونحتاج فقط إلى مستخدم ضيف للوصول إلى هذا الملف، مما يؤدي إلى السماح للجميع باستغلال هذه الثغرة. في هذا الملف، الدالة غير الآمنة هي grow_right_pane_tree().

قبل أن نتعمق في هذه الدالة، نحتاج إلى تتبع المسار العكسي لمعرفة كيف يتم تنفيذ الدوال.
<?php
switch (get_nfilter_request_var('action')) {
//....
// حالات أخرى كثيرة
case 'tree_content':
//..... بعض الشيفرات هنا
// ---------
if (isset_request_var('node')) {
$parts = explode('-', sanitize_search_string(get_request_var('node')));
// التحقق من مرساة الشجرة
if (strpos(get_nfilter_request_var('node'), 'tree_anchor') !== false) {
$tree_id = $parts[1];
$node_id = 0;
}
//..... بعض الشيفرات هنا
if ($tree_id > 0) {
if (!is_tree_allowed($tree_id)) {
header('Location: permission_denied.php');
exit;
}
// يتم استدعاء الدالة المعرضة للخطر هنا
grow_right_pane_tree($tree_id, $node_id, $hgdata);
}
من مقتطف الشفرة أعلاه، يمكننا أن نرى أن الدالة سيتم تنفيذها إذا كان $tree_id > 0. يمكن شرح جميع الخطوات على النحو التالي:
أولاً، يقوم البرنامج بأخذ قيمة معامل action من إدخال المستخدم والدخول في جملة switch/case.
في حالة tree_content، تأخذ الشفرة معامل الطلب node من المستخدم. بعد ذلك، تقوم بتقسيم الإدخال بواسطة الحرف -، وحفظه في $part وأخيراً التحقق مما إذا كان tree_anchor موجوداً في node. إذا تم استيفاء جميع الشروط، سيتم تعيين $tree_id كعنصر ثانٍ في $part. على سبيل المثال، tree_content-1 أو 1-2-tree_content ستكون صالحة وستكون قيمة $tree_id على التوالي.
من هذه النقطة، سيبدو عنوان URL الخاص بنا هكذا: http://localhost:80?action=tree_content&node=tree_anchor-1. الآن حان الوقت لتحليل الدالة grow_right_pane_tree() لاستغلال الخلل.

المعامل المعرض للخطر هو rfilter والذي يتم تمريره مباشرة إلى عامل RLIKE داخل جملة WHERE. ما يجعله عرضة للخطر هو أن rfilter محاط بعلامتي اقتباس مزدوجة "، ومع ذلك يتم التحقق من المعامل بواسطة الدالة html_validate_tree_vars()، والتي تبدو هكذا:
function html_validate_tree_vars() {
/* ================= التحقق من الإدخال وتخزين الجلسة ================= */
$filters = array(
// .......
'rfilter' => array(
'filter' => FILTER_VALIDATE_IS_REGEX,
'pageset' => true,
'default' => '',
),
// ..........
);
validate_store_request_vars($filters, 'sess_grt');
/* ================= التحقق من الإدخال ================= */
// ........
}
تم تعيين نوع الفلتر الخاص بـ rfilter إلى FILTER_VALIDATE_IS_REGEX وتخزينه داخل $filters، ثم يتم تمرير $filter إلى الدالة validate_store_request_vars(). دعنا نتعمق في هذه الدالة، سيكشف هذا باقي تحليلنا:
function validate_store_request_vars($filters, $sess_prefix = '') {
// .......
if (cacti_sizeof($filters)) {
foreach($filters as $variable => $options) {
// ....
elseif ($options['filter'] == FILTER_VALIDATE_IS_REGEX) {
if (is_base64_encoded($_REQUEST[$variable])) {
$_REQUEST[$variable] = base64_decode($_REQUEST[$variable]);
}
$valid = validate_is_regex($_REQUEST[$variable]);
if ($valid === true) {
$value = $_REQUEST[$variable];
} else {
$value = false;
$custom_error = $valid;
}
}
// ........
function validate_is_regex($regex) {
// ........
if (@preg_match("'" . $regex . "'", NULL) !== false) {
ini_set('track_errors', $track_errors);
return true;
}
الدالة validate_store_request_vars() مسؤولة عن التحقق من صحة إدخال المستخدم. عندما يكون نوع filter هو FILTER_VALIDATE_IS_REGEX، تستدعي الدالة validate_is_regex() مع الدالة preg_match للتحقق مما إذا كان إدخالنا صالحاً. وفقاً لوثائق PHP:
preg_match() ترجع 1 إذا كان
النمطيطابقالموضوعالمحدد، و0 إذا لم يطابق، أو false عند الفشل.
تستخدم جملة if مقارنة صارمة مما يعني أنه فقط عند حدوث خطأ لا يمكننا الدخول إلى جملة if. إن $regex، وهو إدخالنا، محاط بين علامتي اقتباس مفردة '. كان نية المطورين تجنب قيام المستخدم بحقن علامة الاقتباس المفردة '، والتي غالباً ما تستخدم لبدء هجوم حقن SQL. يبدو هذا وكأنه تنفيذ أمني قوي، لكن دعنا نلقي نظرة مرة أخرى على كيفية تمرير rfilter إلى جملة WHERE:
$sql_where .= ' (gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '")';
آه، لقد أصبح جهد التحقق من صحة إدخال المستخدم بلا معنى حيث أن rfilter محاط بعلامات اقتباس مزدوجة "، مما يسمح للمهاجمين بحقن " بسهولة للخروج من الجملة.
بعد أن فهمنا لماذا هي عرضة للخطر، سنتعمق الآن في الاستعلام حيث يتم تمرير $sql_where إليه.

بعد تعيين $sql_where، سيقوم البرنامج باستدعاء الدالة get_allowed_tree_header_graphs(). ستبدو الدالة هكذا: