
تحليل خطوة بخطوة لـ CVE-2022-46169: تنفيذ تعليمات برمجية عن بُعد غير مصادق عليه في Cacti عبر تجاوز المصادقة وحقن الأوامر، مع إعداد مختبر Docker ودليل الاستغلال.
Cacti هي أداة مراقبة تشغيلية مفتوحة المصدر مكتوبة بلغة PHP و MySQL/MariaDB، وتوفر واجهة سهلة الاستخدام.
تم اكتشاف الثغرة الأمنية في عام 2022 والتي أثرت على جميع الإصدارات قبل 1.2.23. يتطلب هذا الخلل سلسلة من تجاوز المصادقة وحقن الأوامر لتحقيق RCE (تنفيذ التعليمات البرمجية عن بُعد).
في تحليل CVE هذا، سأقوم بتشغيل Cacti في Docker واستخدام VSCode لتحليل الكود. سيكون الإعداد بسيطًا إلى حد ما، أولاً نحتاج إلى ملف docker-compose.yaml لإنشاء بيئة جديدة. فيما يلي ملف docker-compose.yaml:
version: '2'
services:
cacti:
image: "smcline06/cacti"
container_name: cacti
domainname: example.com
hostname: localhost
ports:
- "8088:80"
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: cacti_db
domainname: example.com
hostname: db
ports:
- "3307:3306" # Change host port to 3307
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=User@123
- TZ=America/Los_Angeles
volumes:
- cacti-db:/var/lib/mysql
volumes:
cacti-db:
cacti-data:
cacti-spine:
cacti-backups:
بعد إنشاء الملف، افتح سطر الأوامر وانتقل إلى دليل الملف، وشغل الأمر docker-compose up -d، ثم افتح المتصفح وانتقل إلى localhost:8088. أولاً ستظهر لك صفحة تسجيل الدخول:

بيانات الاعتماد الافتراضية هي admin/admin. سيتم عرض عملية الإعداد من خلال الصور أدناه:

إنشاء كلمة مرور جديدة

بعد الانتهاء من التثبيت، سيكون لدينا شاشة وحدة التحكم مثل هذه

الآن لنبدأ في تحليل الثغرة الأمنية. كما نعلم، الملف المعرض للخطر هو remote_agent.php، لذا سنحاول الوصول إلى الملف في المتصفح

يقول إننا غير مصرح لنا بالوصول إلى الملف. حان الوقت لعرض الكود المصدري للملف

يقوم بالتحقق عن طريق استدعاء الدالة remote_client_authorized(). دعنا نتعمق في هذه الدالة.

أولاً، يحصل الخادم على عنوان IP الخاص بنا عبر الدالة get_client_addr() ثم يستخدم الدالة gethostbyaddr() لترجمة عنوان IP الخاص بنا إلى اسم مضيف. يقوم الخادم بعد ذلك بجلب جميع pollers المتاحة في جدول poller ويقارن كل hostname من poller مع hostname الخاص بك المترجم من عنوان IP. هناك تجاوز هنا، داخل الدالة get_client_addr():

يمكننا أن نرى أن الخادم سيحصل على عنوان IP عبر أحد الرؤوس:
- X-Forwarded-For
- X-Client-IP
- X-Real-IP
- X-ProxyUser-Ip
- CF-Connecting-IP
- True-Client-IP
- HTTP_X_FORWARDED
- HTTP_X_FORWARDED_FOR
- HTTP_X_CLUSTER_CLIENT_IP
- HTTP_FORWARDED_FOR
- HTTP_FORWARDED
- HTTP_CLIENT_IP
- REMOTE_ADDR
يسمح لنا هذا بالتحكم الكامل في قيمة عنوان IP الخاص بنا. في هذه الحالة، يمكننا استخدام رأس X-Forwarded-For لتزوير عنوان IP الخاص بنا إلى عنوان IP صالح، مما يسمح لنا بتجاوز الترخيص. غالبًا ما يُستخدم رأس X-Forwarded-For لتحديد عنوان IP الأصلي في حالة وجود وكيل أو موازن تحميل بين العميل والخادم. ومع ذلك، سيكون هذا سطح هجوم للمهاجمين لاستغلاله. نظرًا لأننا نشغل cacti محليًا، نحتاج إلى تحديد عنوان IP الذي سيتم ترجمته إلى localhost، وهو 127.0.0.1.

يبدو جيدًا الآن، أليس كذلك؟ ومع ذلك، فهذه مجرد البداية يا أخوان!!! نحتاج إلى مزيد من تحليل الكود لنجاح حقن الأوامر والحصول على تنفيذ التعليمات البرمجية عن بُعد. بعد الانتهاء من المصادقة، سيقوم البرنامج بتنفيذ هذا الكود

سيحصل الخادم على معامل action ويدخل في عبارة Switch/Case. إذا كانت قيمة action هي pollerdata، فسيستدعي البرنامج الدالة poll_for_data(). هذه الدالة معرضة لحقن الأوامر، لذا سنقوم بتحليلها بعناية.

تأخذ الدالة 3 معاملات $local_data_ids و $host_id و $poller_id المستلمة من معاملات طلب المستخدم local_data_ids و host_id و poller_id. لاحظ الفرق في الدالة لاسترجاع المعاملات، واحدة هي get_filter_request_var والأخرى هي get_nfilter_request_var، هناك حرف n إضافي في الدالة الأخيرة، سنناقش هذا أكثر لاحقًا. بعد ذلك، سيتحقق البرنامج مما إذا كنا قد قدمنا معامل local_data_ids وسيقوم بالتكرار على كل واحد لاسترجاع البيانات من جدول poller_item بناءً على local_data_ids و host_id. سيتم حفظ الاستعلام في $items.

إذا أعاد الاستعلام نتائج، فسيقوم البرنامج بالتكرار على كل $item في $items ويدخل في عبارة Switch/Case التي تأخذ $item['action'] كقيمة للـ Switch. هناك العديد من الحالات ولكن الحالة التي يجب أن نتحرى عنها هي POLLER_ACTION_SCRIPT_PHP والتي تعني أن action تساوي 2.

بمجرد أن تكون action هي 2، سيقوم البرنامج بتنفيذ أمر proc_open()، والذي يشبه إلى حد كبير exec()، ويأخذ $poller_id كأحد المتغيرات، والتي نتحكم فيها بالكامل. للحصول على تصور أفضل، يجب أن نصل إلى قاعدة البيانات لاسترجاع محتويات جدول poller_item لنرى كيف يبدو.