## # 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("#{Rex::Socket.to_authority(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("#{Rex::Socket.to_authority(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("#{Rex::Socket.to_authority(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("#{Rex::Socket.to_authority(rhost, rport)} - #{res_array[0]}") return when 4 # Nagios XI is still not fully installed print_error("#{Rex::Socket.to_authority(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("#{Rex::Socket.to_authority(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("#{Rex::Socket.to_authority(rhost, rport)} - #{res_array[0]}") return when 5 # the Nagios XI license agreement still has not been signed print_error("#{Rex::Socket.to_authority(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("#{Rex::Socket.to_authority(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