- vừa được xem lúc

[CVE-2023-39361] Unauthenticated SQL injection in Cacti v1.2.24

0 0 18

Người đăng: Kaito Vũ

Theo Viblo Asia

Description

1.Cacti

Cacti là một công cụ giám sát mạng dựa trên PHP/ MySQL sử dụng RRDTool (Round-robin database tool) với mục đích lưu trữ dữ liệu và tạo đồ họa. Cacti thu thập dữ liệu định kì thông qua Net-SNMP (một bộ phần mềm dùng để thực hiện SNMP-Simple Network Management Protocol).

2.CVE-2023-39361

Đây là lỗ hổng Unauthenticated SQLi ảnh hưởng tới phiên bản 1.2.24.Bản vá cho lỗ hổng này là phiên bản 1.2.25 và 1.2.30.Lỗ hổng SQLi được phát hiện ở trang graph_view.php và vì guest users cũng có thể truy cập vào trang này nên bất kì ai cũng có thể khai thác được lỗ hổng này.

Patch Analysis

Tiến hành tải bản có lỗi 1.2.24 và bản patch 1.2.25 sau đó mình diff hai bản bằng vscode.

Thấy có sự thay đổi nhỏ ở hàm grow_right_pane_tree dòng 1286 của file lib/html_tree.php.Dấu nháy kép khi truyền tham số rfilter được đổi thành nháy đơn.Vậy giờ ta đã biết điểm mà cần chú ý ở đâu.Giờ thì setup và phân tích.

Setup

Để tiện nhất mình dùng docker setup môi trường.File docker-compose.yml như sau:

version: '3.5'
services: cacti: image: "smcline06/cacti" container_name: cacti 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: cacti_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:

Sau đó chạy docker-compose up.Tuy nhiên docker trên là bản 1.2.17 do đó ta cần truy cập vào container chạy cacti và upgrade lên 1.2.24. Tải file upgrade_1.2.24 rồi chạy file này và ta sẽ có bản 1.2.24

Analysis

Tìm nơi gọi hàm grow_right_pane_tree trong file graph_view.php.

graph_view.php

switch (get_nfilter_request_var('action')) {
// ...
case 'tree_content': html_validate_tree_vars(); // ... 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); }

Lỗ hổng nằm ở hàm grow_right_pane_tree trong chức năng graph view.Trong case tree_content input của user dược kiểm tra bằng hàm html_validate_tree_vars() sau đó hàm grow_right_pane_tree được gọi khi tham số tree_id lớn hơn 0.

lib/html_tree.php

function grow_right_pane_tree($tree_id, $leaf_id, $host_group_data) { // ... if (($leaf_type == 'header') || (empty($leaf_id))) { $sql_where = ''; if (get_request_var('rfilter') != '') { $sql_where .= ' (gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '")'; } // ... $graph_list = get_allowed_tree_header_graphs($tree_id, $leaf_id, $sql_where); } 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');

Hàm grow_right_pane_tree truyền trực tiếp input của user thông qua param rfilter vào sau toán tử RLIKE của mệnh đề WHERE.Nhưng trước đó rfilter đã dược hàm html_validate_tree_vars() kiểm tra.Hàm html_validate_tree_vars() đặt kiểu filter cho rfilterFILTER_VALIDATE_IS_REGEX và gọi hàm validate_store_request_vars.

lib/html_utility.php

function validate_store_request_vars(array $filters, string $sess_prefix = ''):void { // ... if (cacti_sizeof($filters)) { foreach ($filters as $variable => $options) { // Establish the session variable first if ($sess_prefix != '') { // ... } else { if (get_nfilter_request_var($variable) == '0') { // ... } elseif ($options['filter'] == FILTER_VALIDATE_IS_REGEX) { if (is_base64_encoded($_REQUEST[$variable])) { $_REQUEST[$variable] = base64_decode($_REQUEST[$variable], true); } $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; }

hàm validate_store_request_vars sẽ kiểm tra input của user nếu kiểu filter là FILTER_VALIDATE_IS_REGEX thì hàm validate_is_regex sẽ đươc gọi để kiểm tra input bằng cách dùng preg_match.=>Như ta có thể thấy từ đoạn code trên thì kiểu filter FILTER_VALIDATE_IS_REGEX sẽ kiểm tra input của người dùng có tồn tại dấu nháy đơn không nếu có thì sẽ bị dấu nháy ngoài escape.

Tuy nhiên,quay trở lại hàm grow_right_pane_tree và quan sát đoạn code ban đầu ở phần Patch Analysis.

function grow_right_pane_tree($tree_id, $leaf_id, $host_group_data) { // ... $sql_where .= ' (gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '")';

ta có thể thấy biến rfilter được truyền vào trong dấu nháy kép -> Điều này có nghĩa là cái filter cho param rfilter không có ý nghĩa gì khi nó chỉ filter input có dấu nháy đơn.Như vậy nếu ta truyền dấu nháy kép vào input thì sẽ không bị filter đồng thời cũng escape được câu truy vấn ban đầu.

POC

Account Takeover

Tìm đến nơi xử lý đăng nhập của user:

function local_auth_login_process($username) { $user = array(); if (!api_plugin_hook_function('login_process', false)) { $user = secpass_login_process($username); /** * If the password needs to be rehashed for security purposes, * do that now. */ $stored_pass = db_fetch_cell_prepared('SELECT password FROM user_auth WHERE username = ? AND realm = 0', array($username)); if ($stored_pass != '') { $password = get_nfilter_request_var('login_password'); $valid = compat_password_verify($password, $stored_pass); cacti_log("DEBUG: User '" . $username . "' password for rehash is " . ($valid ? '':'in') . 'valid', false, 'AUTH', POLLER_VERBOSITY_DEBUG); if ($valid) { $user = db_fetch_row_prepared('SELECT * FROM user_auth WHERE username = ? AND realm = 0', array($username)); if (compat_password_needs_rehash($stored_pass, PASSWORD_DEFAULT)) { $password = compat_password_hash($password, PASSWORD_DEFAULT); db_check_password_length(); db_execute_prepared('UPDATE user_auth SET password = ? WHERE username = ?', array($password, $username)); } } } } return $user;
}

Sau khi kiểm tra tính hợp lệ của username ở hàm secpass_login_process,server sẽ tiến hành kiểm tra password được nhập với password trong db bằng compat_password_verify:

function compat_password_verify($password, $hash) { if (function_exists('password_verify')) { if (password_verify($password, $hash)) { return true; } } $md5 = md5($password); return ($md5 == $hash);
}

tiếp tục gọi đến password_verify.Theo như doc của php thì pasword_verify sẽ kiểm tra hash được đưa vào có match với password không bằng cách sử dụng hàm password_hash.Nếu để ý phía dưới ta có thể thấy server sử dụng hàm compat_password_hash rồi gọi tới password_hash để băm password bằng thuật toán mặc định tức thuật toán bcrypt

function compat_password_hash($password, $algo, $options = array()) { if (function_exists('password_hash')) { // Check if options array has anything, only pass when required return (cacti_sizeof($options) > 0) ? password_hash($password, $algo, $options) : password_hash($password, $algo); } return md5($password);
}

Như vậy ta đã biết được cách thức mà server sẽ lưu password của user dưới dạng mã bcrypt. Attacker có thể lợi dụng điều này để thay đổi password và chiếm đoạt tài khoản của user khác.

<?php
$password="fake_password";
$password=password_hash($password,PASSWORD_DEFAULT);
echo $password; Result: $2y$10$.4/6G5Ag8QodIHiIxn5gRuiFP5Wl3KZySwgcPmeHTMGN638srXJz.

Tài khoản admin đã bị chiếm đoạt.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tấn công Injection là gì ?

Một trong những kiểu tấn công phổ biến nhất được biết đến đối với ứng dụng web là SQL injection. SQL injection là một kiểu tấn công nhắm vào cơ sở dữ liệu SQL, cho phép người dùng cấp các tham số của riêng họ cho một truy vấn SQL.

0 0 108

- vừa được xem lúc

Sử dụng SQLMap để khai thác lỗ hổng SQL Injection (SQLi)

Trước khi đi vào tìm hiểu về SQLMap, các bạn nên nắm được khái niệm về lỗ hổng SQL Injection. Trước đây, mình đã từng có bài viết giới thiệu các khái niệm cơ bản về kiểu tấn công này ở đây.

0 0 87

- vừa được xem lúc

Phân tích lỗi Unauthen SQL injection Woocommerce

Tóm tắt. Woocommerce là wordpress plugin cho phép xây dựng website bán hàng miễn phí.

0 0 172

- vừa được xem lúc

Tìm hiểu về SQL injection

Với yêu cầu từ các dự án của công ty, một trong những điểm bắt buộc trước khi release dự án là phải PASS qua “Security Testing”. Mình sẽ viết một loạt bài giới thiệu cũng như hướng dẫn thực hiện Secur

0 0 41

- vừa được xem lúc

Blog#186: 🔐Securing Your Application Against SQL Injection in Node.js Express

Hi, I'm Tuan, a Full-stack Web Developer from Tokyo . Securing Your Application Against SQL Injection in Node.

0 0 25

- vừa được xem lúc

[Write-up] Hackthebox: Zipping - SQL Injection to LFI

. . OS. Difficulty. .

0 0 13