220 lines
8.0 KiB
Ruby
220 lines
8.0 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Exploit::Remote::HTTP::NagiosXi
|
|
include Msf::Auxiliary::Scanner
|
|
include Msf::Auxiliary::Report
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'Nagios XI Scanner',
|
|
'Description' => %q{
|
|
The module detects the version of Nagios XI applications and
|
|
suggests matching exploit modules based on the version number.
|
|
Since Nagios XI applications only reveal the version to authenticated
|
|
users, valid credentials for a Nagios XI account are required.
|
|
Alternatively, it is possible to provide a specific Nagios XI version
|
|
number via the `VERSION` option. In that case, the module simply
|
|
suggests matching exploit modules and does not probe the target(s).
|
|
|
|
},
|
|
'Author' => [ 'Erik Wynter' ], # @wyntererik
|
|
'License' => MSF_LICENSE,
|
|
'References' => [
|
|
['CVE', '2019-15949'],
|
|
['CVE', '2020-5791'],
|
|
['CVE', '2020-5792'],
|
|
['CVE', '2020-35578'],
|
|
['CVE', '2021-37343']
|
|
]
|
|
)
|
|
register_options [
|
|
OptString.new('VERSION', [false, 'Nagios XI version to check against existing exploit modules', nil])
|
|
]
|
|
end
|
|
|
|
def finish_install
|
|
datastore['FINISH_INSTALL']
|
|
end
|
|
|
|
def username
|
|
datastore['USERNAME']
|
|
end
|
|
|
|
def password
|
|
datastore['PASSWORD']
|
|
end
|
|
|
|
def version
|
|
datastore['VERSION']
|
|
end
|
|
|
|
def parse_version(nagios_version)
|
|
# Strip the xi- prefix used for Nagios XI versions in the official documentation
|
|
clean_version = nagios_version.downcase.delete_prefix('xi-')
|
|
|
|
# Check for special case of 5r1.0, which is actually 5.1.0 but for some reason they still added
|
|
# the `r` used in legacy versions (this is the only version with this format)
|
|
if clean_version == '5r1.0'
|
|
clean_version = '5.1.0'
|
|
end
|
|
|
|
# Since we are dealing with a user provided version here, a strict regex check seems appropriate
|
|
# So far all modern Nagios XI versions start with 5. This regex can be updated if Nagios XI 6 ever
|
|
# comes out and modules for this new version are added
|
|
modern_check = clean_version.scan(/5\.\d{1,2}\.\d{1,2}/).flatten.first
|
|
|
|
unless modern_check.blank?
|
|
return ['modern', clean_version]
|
|
end
|
|
|
|
# Nagios XI legacy version numbers vary a lot, but they always start with a year followed by `r` and a single digit
|
|
legacy_check = clean_version.scan(/(20\d{2}r\d{1})/).flatten.first
|
|
|
|
unless legacy_check.blank?
|
|
return ['legacy', clean_version]
|
|
end
|
|
|
|
return 'unsupported'
|
|
end
|
|
|
|
def rce_check(version, real_target: false)
|
|
version_type, clean_version = parse_version(version)
|
|
if version_type == 'unsupported'
|
|
print_error("Invalid version format: `#{version}`. Please provide an existing Nagios XI version or use `unset VERSION` to cancel")
|
|
return
|
|
end
|
|
|
|
if version_type == 'legacy'
|
|
print_error("This module does not support the legacy Nagios XI version #{version}")
|
|
return
|
|
end
|
|
|
|
begin
|
|
gem_version = Rex::Version.new(clean_version)
|
|
rescue ArgumentError
|
|
print_error("Invalid version format: `#{version}`. Please provide an existing Nagios XI version or use `unset VERSION` to cancel")
|
|
return
|
|
end
|
|
|
|
cve_rce_hash = nagios_xi_rce_check(gem_version)
|
|
|
|
if cve_rce_hash.empty?
|
|
print_error("Nagios XI version #{version} doesn't match any exploit modules.")
|
|
return
|
|
end
|
|
|
|
# Adjust the output based on whether a version was provided, or we obtained a version from a target
|
|
if real_target
|
|
print_good("The target appears to be vulnerable to the following #{cve_rce_hash.length} exploit(s):")
|
|
else
|
|
print_good("Version #{version} matches the following #{cve_rce_hash.length} exploit(s):")
|
|
end
|
|
|
|
print_status('')
|
|
# Get the length of the longest key, this is used to align the output when printing
|
|
max_key_length = 0
|
|
cve_rce_hash.each_key { |k| max_key_length = k.length if k.length > max_key_length }
|
|
# Iterate over the hash, and print out the keys and values while using max_key_length to calculate the padding
|
|
cve_rce_hash.each do |cve, exploit_module|
|
|
padding = (max_key_length + 4) - cve.length
|
|
print_status("\t#{cve}#{' ' * padding}exploit/linux/http/#{exploit_module}")
|
|
end
|
|
print_status('')
|
|
return
|
|
end
|
|
|
|
# The first undercore in _target_host is used since this parameter is passed in by default but our code never uses it
|
|
def run_host(_target_host)
|
|
# Check if we have a valid version to test
|
|
if version
|
|
if version.empty?
|
|
print_error('VERSION cannot be empty. Please provide an existing Nagios XI VERSION or use `unset VERSION` to cancel')
|
|
return
|
|
end
|
|
|
|
return rce_check(version)
|
|
end
|
|
|
|
# Check if we have credentials, if not, try to obtain the Nagios XI version from login.php
|
|
if username.blank? || password.blank?
|
|
print_warning('No credentials provided. Attempting to obtain the Nagios XI version from the login page. This will not work for newer versions.')
|
|
nagios_version_result, error_message = nagios_xi_version_no_auth
|
|
|
|
if error_message
|
|
print_error("#{rhost}:#{rport} - #{error_message}")
|
|
print_warning('Please provide a valid Nagios XI USERNAME and PASSWORD, or a specific VERSION to check')
|
|
return
|
|
end
|
|
|
|
print_status("Target is Nagios XI with version #{nagios_version_result}")
|
|
|
|
# Check if the Nagios XI version matches any exploit modules
|
|
return rce_check(nagios_version_result, real_target: true)
|
|
end
|
|
|
|
# Use nagios_xi_login to try and authenticate. If authentication succeeds, nagios_xi_login returns
|
|
# an array containing the http response body of a get request to index.php and the session cookies
|
|
login_result, res_array = nagios_xi_login(username, password, finish_install)
|
|
case login_result
|
|
when 1..3 # An error occurred
|
|
print_error("#{rhost}:#{rport} - #{res_array[0]}")
|
|
return
|
|
when 4 # Nagios XI is not fully installed
|
|
install_result = install_nagios_xi(password)
|
|
if install_result
|
|
print_error("#{rhost}:#{rport} - #{install_result[1]}")
|
|
return
|
|
end
|
|
|
|
login_result, res_array = login_after_install_or_license(username, password, finish_install)
|
|
case login_result
|
|
when 1..3 # An error occurred
|
|
print_error("#{rhost}:#{rport} - #{res_array[0]}")
|
|
return
|
|
when 4 # Nagios XI is still not fully installed
|
|
print_error("#{rhost}:#{rport} - Failed to install Nagios XI on the target.")
|
|
return
|
|
end
|
|
end
|
|
|
|
# when 5 is excluded from the case statement above to prevent having to use this code block twice.
|
|
# Including when 5 would require using this code block once at the end of the `when 4` code block above, and once here.
|
|
if login_result == 5 # the Nagios XI license agreement has not been signed
|
|
auth_cookies, nsp = res_array
|
|
sign_license_result = sign_license_agreement(auth_cookies, nsp)
|
|
if sign_license_result
|
|
print_error("#{rhost}:#{rport} - #{sign_license_result[1]}")
|
|
return
|
|
end
|
|
|
|
login_result, res_array = login_after_install_or_license(username, password, finish_install)
|
|
case login_result
|
|
when 1..3
|
|
print_error("#{rhost}:#{rport} - #{res_array[0]}")
|
|
return
|
|
when 5 # the Nagios XI license agreement still has not been signed
|
|
print_error("#{rhost}:#{rport} - Failed to sign the license agreement.")
|
|
return
|
|
end
|
|
end
|
|
|
|
print_good('Successfully authenticated to Nagios XI')
|
|
|
|
# Obtain the Nagios XI version
|
|
nagios_version = nagios_xi_version(res_array[0])
|
|
if nagios_version.nil?
|
|
print_error("#{rhost}:#{rport} - Unable to obtain the Nagios XI version from the dashboard")
|
|
return
|
|
end
|
|
|
|
print_status("Target is Nagios XI with version #{nagios_version}")
|
|
|
|
# Check if the Nagios XI version matches any exploit modules
|
|
rce_check(nagios_version, real_target: true)
|
|
end
|
|
end
|