lib/msf/core/exploit/remote/http/nagios_xi/login.rb
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::NagiosXi::Login
include Msf::Exploit::Remote::HTTP::NagiosXi::URIs
AUTH_RESULTS = {
:connection_failed => 1,
:unexpected_error => 2,
:not_nagios_application => 3,
:not_fully_installed => 4,
:failed_to_handle_license_agreement => 5,
:failed_to_extract_tokens => 6,
:unable_to_obtain_version => 7
}
# Returns a status code an a error message on failure.
# On success returns the status code and an array so we
# can update the login_result and res_array variables appropriately.
def handle_unsigned_license(res_array, username, password, finish_install)
auth_cookies, nsp = res_array
sign_license_result = sign_license_agreement(auth_cookies, nsp)
if sign_license_result
return 5, 'Failed to sign license agreement'
end
print_status('License agreement signed. The module will wait for 5 seconds and retry the login.')
sleep 5
login_result, res_array = login_after_install_or_license(username, password, finish_install)
case login_result
when 1..4 # An error occurred, propagate the error message
return login_result, res_array[0]
when 5 # The Nagios XI license agreement still has not been signed
return 5, 'Failed to sign the license agreement.'
end
return login_result, res_array
end
# Returns a status code an a error message on failure.
# On success returns the status code and an array so we
# can update the login_result and res_array variables appropriately.
def install_full_nagios(username, password, finish_install)
install_result = install_nagios_xi(password)
if install_result
return install_result[0], install_result[1]
end
login_result, res_array = login_after_install_or_license(username, password, finish_install)
case login_result
when 1..4 # An error occurred, propagate the error message
return login_result, res_array[0]
when 5 # The license agreement still needs to be signed
login_result, res_array = handle_unsigned_license(res_array, username, password, finish_install)
return login_result, res_array
end
return login_result, res_array
end
def authenticate(username, password, finish_install, handle_full_install = true, handle_license = true, handle_nsp = false)
login_result, res_array = nagios_xi_login(username, password, finish_install)
case login_result
when 1..3
return login_result, res_array[0]
when 4 # Nagios XI is not fully installed
return login_result, 'Nagios is not fully installed.' unless handle_full_install
login_result, res_array = install_full_nagios(username, password, finish_install)
return login_result, res_array unless (login_result == 0)
when 5
return login_result, 'The Nagios license has not been signed.' unless handle_license
login_result, res_array = handle_unsigned_license(res_array, username, password, finish_install)
return login_result, res_array unless (login_result == 0)
end
print_good('Successfully authenticated to Nagios XI.')
return 6, "Failed to extract auth cookies and nsp string" unless res_array.length == 2
auth_cookies = extract_auth_cookies(res_array) # if we are here, this cannot be nil since the mixin checks for that already
return 6, 'Failed to extract authentication cookies' unless auth_cookies.present?
nsp = get_nsp(res_array[0]) if handle_nsp
return 6, 'Failed to extract nsp string' if handle_nsp && !nsp.present?
# Obtain the Nagios XI version
nagios_version = nagios_xi_version(res_array[0])
if nagios_version.nil?
return 7, 'Unable to obtain the Nagios XI version from the dashboard'
end
print_status("Target is Nagios XI with version #{nagios_version}.")
# Versions of NagiosXI pre-5.2 have different formats (5r1.0, 2014r2.7, 2012r2.8b, etc.) that Rex cannot handle,
# so we set pre-5.2 versions to 1.0.0 for easier Rex comparison because the module only works on post-5.2 versions.
if /#{Msf::Exploit::Remote::HTTP::NagiosXi::PRE_5_2_VERSION_REGEX}/.match(nagios_version) || nagios_version == '5r1.0'
nagios_version = '1.0.0'
end
version = Rex::Version.new(nagios_version)
return 0, 'Successfully authenticated and retrieved NagiosXI Version.', auth_cookies, version, nsp
end
# performs a Nagios XI login
#
# @param user [String] Username
# @param pass [String] Password
# @param finish_install [Boolean] Boolean indicating if the module should finish installing Nagios XI on target hosts if the installation hasn't been completed or the license agreement is not signed
# @return [Array] Array containing a response code and an Array containing one of four possibilities: the HTTP response body and session cookies if the Nagios XI backend was accessed successfully; nil if Nagios XI hasn't been fully installed; cookies and the nsp token if the license agreement is not signed; otherwise an error message
def nagios_xi_login(user, pass, finish_install)
print_status('Attempting to authenticate to Nagios XI...')
# Visit the login page in order to obtain the cookies and the `nsp_str` token required for authentication
res_pre_login = send_request_cgi({
'method' => 'GET',
'uri' => nagios_xi_login_url,
})
unless res_pre_login
return [1, ['Connection failed']]
end
unless res_pre_login.code == 200 && res_pre_login.body.include?('>Nagios XI<')
# Check if we are perhaps dealing with a Nagios XI app that hasn't been fully installed yet
unless res_pre_login.code == 302 && res_pre_login.body.include?('>Nagios XI<') && res_pre_login.headers['Location'].end_with?('/install.php')
return [3, ['Target is not a Nagios XI application']]
end
print_warning('The target seems to be a Nagios XI application that has not been fully installed yet.')
unless finish_install
return [2, ['You can let the module complete the installation by setting `FINISH_INSTALL` to true.']]
end
return [4, [nil]]
end
# Grab the necessary tokens required for authentication
nsp = get_nsp(res_pre_login)
if nsp.nil?
return [2, ['Unable to obtain the value of the `nsp_str` token required for authentication']]
end
pre_auth_cookies = res_pre_login.get_cookies
if pre_auth_cookies.blank?
return [2, ['Unable to obtain the cookies required for authentication']]
end
# authenticate
res_login = send_request_cgi({
'method' => 'POST',
'uri' => nagios_xi_login_url,
'cookie' => pre_auth_cookies,
'vars_post' => {
'nsp' => nsp,
'pageopt' => 'login',
'username' => user,
'password' => pass
}
})
unless res_login
return [1, ['Connection failed']]
end
unless res_login.code == 302 && res_login.headers['Location'] == 'index.php'
return [2, ['Received unexpected reply while trying to authenticate. Please check your credentials.']]
end
# Grab the cookies
auth_cookies = res_login.get_cookies
if auth_cookies.blank?
return [2, ['Unable to obtain the cookies required for authentication']]
end
# Make sure we only use the cookies we need, otherwise we may encounter a session timeout
auth_cookies = clean_cookies(pre_auth_cookies, auth_cookies)
# Try to visit the dashboard
visit_nagios_dashboard(auth_cookies, finish_install)
end
# Compares cookies obtained before and after authentication and modifies the
# latter to remove cookies that may cause session timeouts
#
# @param pre_auth_cookies [String] Cookies obtained before authenticating to Nagios XI
# @param auth_cookies [String] Cookies obtained while authenticating to Nagios XI
# @return [String, nil] String containing the cookies required for authentication, stripped off unnecessary/unwanted cookies, nil if one or both of the parameters passed to this method are nil
def clean_cookies(pre_auth_cookies, auth_cookies)
if pre_auth_cookies.nil? || auth_cookies.nil?
return nil
end
# Nagios XI may sometimes send the cookie `nagiosxi=deleted;` as part of the cookies after authentication.
# This was observed when trying to access Nagios XI 5.3.0 when the license agreement had not been accepted yet.
# The `nagiosxi=deleted;` cookie should be filtered out, since it may break authentication.
if auth_cookies.include?('nagiosxi=deleted;')
auth_cookies = auth_cookies.gsub('nagiosxi=deleted;', '').strip
end
# Remove duplicate cookies, necessary to make the next check work in case multiple
# identical cookies were set (as observed on older Nagios versions)
auth_cookies_array = auth_cookies.split(' ')
auth_cookies_array.uniq!
auth_cookies = auth_cookies_array.join(' ')
# For newer Nagios XI versions, we need to remove the pre_auth cookies from the auth_cookies
# string, otherwise the session will timeout. However, older Nagios XI versions use a single cookie
# which has the same name both before and after authentication.
unless pre_auth_cookies == auth_cookies
if auth_cookies.include?(pre_auth_cookies)
auth_cookies = auth_cookies.gsub(pre_auth_cookies, '').strip
end
end
auth_cookies
end
# Performs an HTTP GET request to the Nagios XI backend to verify if authentication succeeded
#
# @param auth_cookies [String] Cookies required for authentication
# @param finish_install [Boolean] Boolean indicating if the module should finish installing Nagios XI on target hosts if the installation hasn't been completed or the license agreement is not signed
# @return [Array] Array containing a result code and an Array containing one of three possibilities: an HTTP response body and cookies if the Nagios XI backend was accessed successfully; cookies and the nsp token if the license agreement is not signed; otherwise an error message
def visit_nagios_dashboard(auth_cookies, finish_install)
# Visit the index page to verify we successfully authenticated
res_index = send_request_cgi({
'method' => 'GET',
'uri' => nagios_xi_backend_url,
'cookie' => auth_cookies
})
unless res_index
return [1, ['Connection failed']]
end
unless res_index.code == 200 && res_index.body.include?('>Home Dashboard<')
# Check if we need to sign the license agreement
unless res_index.code == 302 && res_index.headers['Location'].end_with?('login.php?showlicense')
return [2, ['Received unexpected reply while trying to access the NagiosXI home dashboard after authenticating.']]
end
print_warning('The Nagios XI license agreement has not yet been signed on the target.')
unless finish_install
return [2, ['You can let the module sign the Nagios XI license agreement by setting `FINISH_INSTALL` to true.']]
end
nsp = get_nsp(res_index)
if nsp.nil?
return [2, ['Failed to obtain the nsp token required for signing the license agreement.']]
end
return [5, [auth_cookies, nsp]]
end
# Return the HTTP response body and the authentication cookies.
# The response body can be used to obtain the version number.
# The cookies can be used by exploit modules to send authenticated requests.
[0, [res_index.body, auth_cookies]]
end
# Grabs the auth_cookies value from an HTTP response and validate using regex
#
# @param res [Rex::Proto::Http::Response] HTTP response
# @return [String, nil] auth_cookies value, nil if not found
def extract_auth_cookies(res_array)
auth_cookies = res_array[1]
return auth_cookies if auth_cookies && /nagiosxi=[a-z0-9]+;/.match(auth_cookies)
end
# Grabs the nsp_str value from an HTTP response using regex
#
# @param res [Rex::Proto::Http::Response] HTTP response
# @return [String, nil] nsp_str value, nil if not found
def get_nsp(res)
res = res.body if res.kind_of?(Rex::Proto::Http::Response)
nsp = res.scan(/nsp_str = "([a-z0-9]+)/)&.flatten&.first
end
# Performs an authentication attempt. If the server does not return a response,
# a second attempt is made after a delay of 5 seconds
#
# @param username [String] Username required for authentication
# @param password [String] Password required for authentication
# @param finish_install [Boolean] Boolean indicating if the module should finish installing Nagios XI on target hosts if the installation hasn't been completed or the license agreement is not signed
# @return [Array] Array containing the HTTP response body and session cookies if the Nagios XI backend was accessed successfully, otherwise Array containing an error code and an error message
def login_after_install_or_license(username, password, finish_install)
# After installing Nagios XI or signing the license agreement, we sometimes don't receive a server response.
# This loop ensures that at least 2 login attempts are performed if this happens, as the second one usually works.
second_attempt = false
while true
login_result, error_message = nagios_xi_login(username, password, finish_install)
break unless error_message == ['Connection failed']
if second_attempt
print_warning('The server is still not responding. If you wait a few seconds and rerun the module, it might still work.')
break
else
print_warning('No response received from the server. This can happen after installing Nagios XI or signing the license agreement')
print_status('The module will wait for 5 seconds and retry.')
second_attempt = true
sleep 5
end
end
return [login_result, error_message]
end
end