Phân tích lỗ hổng Unauthenticated Command Injection (CVE-2022-46169) trong phần mềm Cacti
Bài đăng này đã không được cập nhật trong 2 năm
Giới thiệu về Cacti và CVE-2022-46169
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).
CVE-2022-46169
Lỗ hổng này là lỗ hổng Unauthenticated Command Injection ảnh hưởng trên phiên bản <= 1.2.22. Bản vá hiện tại cho lỗ hổng là phiên bản 1.2.23 và 1.3.0. Tuy nhiên bản vá này vẫn chưa được release. Do đó các hệ thống chạy phiên bản mới nhất hiện tại (1.2.22) vẫn ảnh hưởng bởi lỗ hổng này.
Lỗ hổng này là unauthenticated nên attacker không cần có quyền gì đối với hệ thống vẫn có thể khai thác thành công. Tuy nhiên để khai thác thì vẫn cần có điều kiện nhất định. Cụ thể mình sẽ trình bày ở phần tiếp theo của bài viết.
Xây dựng môi trường
Để tiện nhất ở đây mình dùng docker để xây dựng môi trường. Docker compose sẽ như sau:
version: '2'
services:
cacti:
image: "smcline06/cacti"
container_name: cacti
domainname: example.com
hostname: locahost
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:
- "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 đó các bạn chạy docker-compose up
và truy cập vào localhost:8088
tiến hành cài đặt cacti như thông thường.
Tuy nhiên docker trên là phiên bản 1.2.17. Do đó ta cần truy cập vào container chạy cati và tiến hành upgarde.
Trong container của cacti đã có sẵn file upgrade.sh
tuy nhiên khi chạy file này lên thì hệ thống lại xảy ra lỗi do đó ta download file upgrade khác như sau:
wget https://raw.githubusercontent.com/scline/docker-cacti/master/upgrade.sh
Tiếp theo cấp quyền và chạy file vừa download về ta sẽ được Cacti phiên bản mới nhất là 1.2.22
Phân tích CVE-2022-46169
Theo github advisories https://github.com/Cacti/cacti/security/advisories/GHSA-6p93-p743-35gf, lỗ hổng nằm trong file remote_agent.php
. Khi truy cập trực tiếp vào file này ta nhận được thông báo như sau.
Cùng xem trong mã nguồn xem điều gì xảy ra khi ta truy cập trực tiếp vào file này.
Chương trình kiểm tra client xem đã được xác thực hay chưa bằng cách gọi đến hàm remote_client_authorized()
nếu chưa được xác thực sẽ in ra dòng FATAL: You are not authorized to use this service
sau đó exit. Kiểm tra mã nguồn của hàm remote_client_authorized
.
Hàm này lấy địa chỉ IP của máy client thông qua get_client_addr
và phân giải địa chỉ IP này thành tên máy chủ tương ứng thông qua gethostbyaddr
. Sau đó, chương trình tìm kiếm tên máy chủ trong bảng poller
nếu tìm thấy, hàm trả về true và máy client được xác thực.
Việc xác thực này có thể được bypass, đi sâu hơn vào hàm này tại lib/functions.php
.
Tại đây, chương trình lấy giá trị client IP từ một trong các header
- 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
Tức là attacker hoàn toàn có thể kiểm soát giá trị này thông qua việc thêm header và giá trị tùy ý vào request.
Quay về hàm remote_client_authorized
. Sau khi lấy ra client ip, chương trình tiến hành tìm hostname từ IP thông qua hàm gethostbyaddr
sau đó tìm kiếm trong bảng poller
xem có hostname tương ứng hay không.
Theo cài đặt mặc định của cacti, trong bảng poller
có sẵn một hostname tương ứng với IP của máy chủ. Trong bối cảnh của bài viết, ta dựng docker trên local nên hostname tương ứng sẽ là localhost.
Do đó khi ta thêm header: X-Forwarded-For: <IP Target>
sẽ bypass được bước xác thực của Cacti.
Có thể thấy response trả về đã khác so với lúc ban đầu.
Sau khi qua bước xác thực chương trình tiếp tục thực thi đoạn code sau:
Chương trình nhận param action
và thực hiện Switch/Case
. Nếu action=polldata
chương trình sẽ gọi đến hàm poll_for_data
. Tiếp tục quan sát mã nguồn của hàm này.
Tại đây 3 biến $local_data_ids, $host_id, $poller_id
được lấy giá trị lần lượt từ các param local_data_ids, host_id, poller_id
. Tiếp theo chương trình sẽ lấy các giá trị từ bảng poller_item
tương ứng với các biến trên và đưa vào biến mảng $items
. Tại đây, chương trình một lần nữa thực hiện Switch/Case
với giá trị $item['action']
. Nếu $item['action'] = POLLER_ACTION_SCRIPT_PHP
tức là bằng 2 do POLLER_ACTION_SCRIPT_PHP=2
.
Thì chương trình sẽ thưc thi câu lệnh proc_open
với đối số $poller_id
do attacker kiểm soát. Lỗ hổng Command injection xảy ra tại đây ví dụ khi ta truyền poller_id=;<command>
thì command sẽ được thực thi.
Để khai thác thành công attacker buộc phải cung cấp host_id
và local_data_id
sao cho action tương ứng trong poller_item
bằng 2. Cách này có thể thực hiện bằng cách brute force trên target thực tế. Tuy nhiên, mặc định khi cài Cacti, trong bảng poller_item
không có action nào có giá trị là 2.
Tuy nhiên, điều này dễ xảy ra khi add các template như Device - Uptime
hoặc Device - Polling Time
.
Có thể thấy sau khi thêm template ta được action bằng 2 tương ứng với host_id=1
, local_data_id=6
.
Do đây là blind command injection nên ta không thể đọc trực tiếp kết quả của command. Chúng ta có thể sử dụng out-of-band để đọc kết quả của command.
POC
Cảm ơn các bạn đã đọc hết bài viết. Hẹn gặp các bạn ở một bài viết khác.
P/S: Cảm ơn anh @minhtuan.nguy đã giúp đỡ em xây dựng môi trường để phân tích
All rights reserved