+14

Phân tích lỗ hổng Unauthenticated Command Injection (CVE-2022-46169) trong phần mềm Cacti

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

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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:

image.png

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.

image.png

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.

image.png

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.

image.png

Để khai thác thành công attacker buộc phải cung cấp host_idlocal_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

image.png

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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí