
CVE-2023-39361 के लिए चरण-दर-चरण विश्लेषण और लैब सेटअप, जो Cacti v1.2.24 में एक बिना प्रमाणीकरण वाला SQL इंजेक्शन है, जिसमें शोषण और शमन वॉकथ्रू शामिल है।
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:
कंटेनर बनाने के बाद, हम http://localhost:80 पर ब्राउज़ करके Cacti तक पहुँच सकते हैं। अगला कदम Cactii को संस्करण 1.2.24 पर अपडेट करना है। आप अपग्रेड स्क्रिप्ट यहाँ से डाउनलोड कर सकते हैं: https://pastebin.com/NfRiHLjR। फ़ाइल को docker-compose.yml फ़ाइल के समान निर्देशिका में सहेजें और उसका नाम बदलकर upgrade_cacti.sh करें। फिर ये 2 कमांड चलाएँ:
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 है और इस फ़ाइल तक पहुँचने के लिए हमें केवल guest उपयोगकर्ता की आवश्यकता होती है, जिससे हर कोई इस भेद्यता का शोषण कर सकता है। इस फ़ाइल में, असुरक्षित फ़ंक्शन grow_right_pane_tree() है।

इस फ़ंक्शन में गहराई से जाने से पहले, हमें पीछे की ओर जाकर यह जानना होगा कि फ़ंक्शन कैसे निष्पादित होता है।
<?php
switch (get_nfilter_request_var('action')) {
//....
// Many other cases
case 'tree_content':
//..... Some code here
// ---------
if (isset_request_var('node')) {
$parts = explode('-', sanitize_search_string(get_request_var('node')));
// Check for tree anchor
if (strpos(get_nfilter_request_var('node'), 'tree_anchor') !== false) {
$tree_id = $parts[1];
$node_id = 0;
}
//..... Some code here
if ($tree_id > 0) {
if (!is_tree_allowed($tree_id)) {
header('Location: permission_denied.php');
exit;
}
// Vulnerable function is called here
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 है जो सीधे WHERE क्लॉज के अंदर RLIKE ऑपरेंड में पास किया जाता है। जो चीज़ इसे भेद्य बनाती है वह यह है कि rfilter डबल कोट " के बीच लपेटा जाता है, फिर भी पैरामीटर की जाँच html_validate_tree_vars() फ़ंक्शन द्वारा की जाती है, जो इस प्रकार है:
function html_validate_tree_vars() {
/* ================= input validation and session storage ================= */
$filters = array(
// .......
'rfilter' => array(
'filter' => FILTER_VALIDATE_IS_REGEX,
'pageset' => true,
'default' => '',
),
// ..........
);
validate_store_request_vars($filters, 'sess_grt');
/* ================= input validation ================= */
// ........
}
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 होता है, तो फ़ंक्शन यह सत्यापित करने के लिए कि हमारा इनपुट मान्य है, preg_match फ़ंक्शन के साथ validate_is_regex() फ़ंक्शन को कॉल करता है। PHP दस्तावेज़ीकरण के अनुसार:
preg_match() 1 लौटाता है यदि
patternदिए गएsubjectसे मेल खाता है, 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 पास किया जाता है।