rapid7/metasploit-framework

View on GitHub
lib/msf/core/exploit/remote/http/nagios_xi/login.rb

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: binary -*-

module Msf::Exploit::Remote::HTTP::NagiosXi::Login
  include Msf::Exploit::Remote::HTTP::NagiosXi::URIs

  AUTH_RESULTS = {
    :connection_failed => 1,
    :unexpected_error => 2,
    :not_nagios_application => 3,
    :not_fully_installed => 4,
    :failed_to_handle_license_agreement => 5,
    :failed_to_extract_tokens => 6,
    :unable_to_obtain_version => 7
  }

  # 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

  # 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 install_full_nagios(username, password, finish_install)
    install_result = install_nagios_xi(password)
    if install_result
      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
    end

    return login_result, res_array
  end

  def authenticate(username, password, finish_install, handle_full_install = true, handle_license = true, handle_nsp = false)
    login_result, res_array = nagios_xi_login(username, password, finish_install)
    case login_result
    when 1..3
      return login_result, res_array[0]
    when 4 # Nagios XI is not fully installed
      return login_result, 'Nagios is not fully installed.' unless handle_full_install
      login_result, res_array = install_full_nagios(username, password, finish_install)
      return login_result, res_array unless (login_result == 0)
    when 5
      return login_result, 'The Nagios license has not been signed.' unless handle_license
      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.')

    return 6, "Failed to extract auth cookies and nsp string" unless res_array.length == 2

    auth_cookies = extract_auth_cookies(res_array) # if we are here, this cannot be nil since the mixin checks for that already
    return 6, 'Failed to extract authentication cookies' unless auth_cookies.present?

    nsp = get_nsp(res_array[0]) if handle_nsp
    return 6, 'Failed to extract nsp string' if handle_nsp && !nsp.present?

    # Obtain the Nagios XI version
    nagios_version = nagios_xi_version(res_array[0])
    if nagios_version.nil?
      return 7, '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 /#{Msf::Exploit::Remote::HTTP::NagiosXi::PRE_5_2_VERSION_REGEX}/.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.', auth_cookies, version, nsp
  end

  # performs a Nagios XI login
  #
  # @param user [String] Username
  # @param pass [String] Password
  # @param finish_install [Boolean] Boolean indicating if the module should finish installing Nagios XI on target hosts if the installation hasn't been completed or the license agreement is not signed
  # @return [Array] Array containing a response code and an Array containing one of four possibilities: the HTTP response body and session cookies if the Nagios XI backend was accessed successfully; nil if Nagios XI hasn't been fully installed; cookies and the nsp token if the license agreement is not signed; otherwise an error message
  def nagios_xi_login(user, pass, finish_install)
    print_status('Attempting to authenticate to Nagios XI...')

    # Visit the login page in order to obtain the cookies and the `nsp_str` token required for authentication
    res_pre_login = send_request_cgi({
      'method' => 'GET',
      'uri' => nagios_xi_login_url,
    })

    unless res_pre_login
      return [1, ['Connection failed']]
    end

    unless res_pre_login.code == 200 && res_pre_login.body.include?('>Nagios XI<')
      # Check if we are perhaps dealing with a Nagios XI app that hasn't been fully installed yet
      unless res_pre_login.code == 302 && res_pre_login.body.include?('>Nagios XI<') && res_pre_login.headers['Location'].end_with?('/install.php')
        return [3, ['Target is not a Nagios XI application']]
      end

      print_warning('The target seems to be a Nagios XI application that has not been fully installed yet.')
      unless finish_install
        return [2, ['You can let the module complete the installation by setting `FINISH_INSTALL` to true.']]
      end

      return [4, [nil]]
    end

    # Grab the necessary tokens required for authentication
    nsp = get_nsp(res_pre_login)
    if nsp.nil?
      return [2, ['Unable to obtain the value of the `nsp_str` token required for authentication']]
    end

    pre_auth_cookies = res_pre_login.get_cookies
    if pre_auth_cookies.blank?
      return [2, ['Unable to obtain the cookies required for authentication']]
    end

    # authenticate
    res_login = send_request_cgi({
      'method' => 'POST',
      'uri' => nagios_xi_login_url,
      'cookie' => pre_auth_cookies,
      'vars_post' => {
        'nsp' => nsp,
        'pageopt' => 'login',
        'username' => user,
        'password' => pass
      }
    })

    unless res_login
      return [1, ['Connection failed']]
    end

    unless res_login.code == 302 && res_login.headers['Location'] == 'index.php'
      return [2, ['Received unexpected reply while trying to authenticate. Please check your credentials.']]
    end

    # Grab the cookies
    auth_cookies = res_login.get_cookies

    if auth_cookies.blank?
      return [2, ['Unable to obtain the cookies required for authentication']]
    end

    # Make sure we only use the cookies we need, otherwise we may encounter a session timeout
    auth_cookies = clean_cookies(pre_auth_cookies, auth_cookies)

    # Try to visit the dashboard
    visit_nagios_dashboard(auth_cookies, finish_install)
  end

  # Compares cookies obtained before and after authentication and modifies the
  # latter to remove cookies that may cause session timeouts
  #
  # @param pre_auth_cookies [String] Cookies obtained before authenticating to Nagios XI
  # @param auth_cookies [String] Cookies obtained while authenticating to Nagios XI
  # @return [String, nil] String containing the cookies required for authentication, stripped off unnecessary/unwanted cookies, nil if one or both of the parameters passed to this method are nil
  def clean_cookies(pre_auth_cookies, auth_cookies)
    if pre_auth_cookies.nil? || auth_cookies.nil?
      return nil
    end
    # Nagios XI may sometimes send the cookie `nagiosxi=deleted;` as part of the cookies after authentication.
    # This was observed when trying to access Nagios XI 5.3.0 when the license agreement had not been accepted yet.
    # The `nagiosxi=deleted;` cookie should be filtered out, since it may break authentication.
    if auth_cookies.include?('nagiosxi=deleted;')
      auth_cookies = auth_cookies.gsub('nagiosxi=deleted;', '').strip
    end

    # Remove duplicate cookies, necessary to make the next check work in case multiple
    # identical cookies were set (as observed on older Nagios versions)
    auth_cookies_array = auth_cookies.split(' ')
    auth_cookies_array.uniq!
    auth_cookies = auth_cookies_array.join(' ')

    # For newer Nagios XI versions, we need to remove the pre_auth cookies from the auth_cookies
    # string, otherwise the session will timeout. However, older Nagios XI versions use a single cookie
    # which has the same name both before and after authentication.
    unless pre_auth_cookies == auth_cookies
      if auth_cookies.include?(pre_auth_cookies)
        auth_cookies = auth_cookies.gsub(pre_auth_cookies, '').strip
      end
    end

    auth_cookies
  end

  # Performs an HTTP GET request to the Nagios XI backend to verify if authentication succeeded
  #
  # @param auth_cookies [String] Cookies required for authentication
  # @param finish_install [Boolean] Boolean indicating if the module should finish installing Nagios XI on target hosts if the installation hasn't been completed or the license agreement is not signed
  # @return [Array] Array containing a result code and an Array containing one of three possibilities: an HTTP response body and cookies if the Nagios XI backend was accessed successfully; cookies and the nsp token if the license agreement is not signed; otherwise an error message
  def visit_nagios_dashboard(auth_cookies, finish_install)
    # Visit the index page to verify we successfully authenticated
    res_index = send_request_cgi({
      'method' => 'GET',
      'uri' => nagios_xi_backend_url,
      'cookie' => auth_cookies
    })

    unless res_index
      return [1, ['Connection failed']]
    end

    unless res_index.code == 200 && res_index.body.include?('>Home Dashboard<')
      # Check if we need to sign the license agreement
      unless res_index.code == 302 && res_index.headers['Location'].end_with?('login.php?showlicense')
        return [2, ['Received unexpected reply while trying to access the NagiosXI home dashboard after authenticating.']]
      end

      print_warning('The Nagios XI license agreement has not yet been signed on the target.')
      unless finish_install
        return [2, ['You can let the module sign the Nagios XI license agreement by setting `FINISH_INSTALL` to true.']]
      end

      nsp = get_nsp(res_index)
      if nsp.nil?
        return [2, ['Failed to obtain the nsp token required for signing the license agreement.']]
      end

      return [5, [auth_cookies, nsp]]
    end

    # Return the HTTP response body and the authentication cookies.
    # The response body can be used to obtain the version number.
    # The cookies can be used by exploit modules to send authenticated requests.
    [0, [res_index.body, auth_cookies]]

  end

  # Grabs the auth_cookies value from an HTTP response and validate using regex
  #
  # @param res [Rex::Proto::Http::Response] HTTP response
  # @return [String, nil] auth_cookies value, nil if not found
  def extract_auth_cookies(res_array)
    auth_cookies = res_array[1]
    return auth_cookies if auth_cookies && /nagiosxi=[a-z0-9]+;/.match(auth_cookies)
  end

  # Grabs the nsp_str value from an HTTP response using regex
  #
  # @param res [Rex::Proto::Http::Response] HTTP response
  # @return [String, nil] nsp_str value, nil if not found
  def get_nsp(res)
    res = res.body if res.kind_of?(Rex::Proto::Http::Response)
    nsp = res.scan(/nsp_str = "([a-z0-9]+)/)&.flatten&.first
  end

  # Performs an authentication attempt. If the server does not return a response,
  # a second attempt is made after a delay of 5 seconds
  #
  # @param username [String] Username required for authentication
  # @param password [String] Password required for authentication
  # @param finish_install [Boolean] Boolean indicating if the module should finish installing Nagios XI on target hosts if the installation hasn't been completed or the license agreement is not signed
  # @return [Array] Array containing the HTTP response body and session cookies if the Nagios XI backend was accessed successfully, otherwise Array containing an error code and an error message
  def login_after_install_or_license(username, password, finish_install)
    # After installing Nagios XI or signing the license agreement, we sometimes don't receive a server response.
    # This loop ensures that at least 2 login attempts are performed if this happens, as the second one usually works.
    second_attempt = false
    while true
      login_result, error_message = nagios_xi_login(username, password, finish_install)

      break unless error_message == ['Connection failed']

      if second_attempt
        print_warning('The server is still not responding. If you wait a few seconds and rerun the module, it might still work.')
        break
      else
        print_warning('No response received from the server. This can happen after installing Nagios XI or signing the license agreement')
        print_status('The module will wait for 5 seconds and retry.')
        second_attempt = true
        sleep 5
      end
    end

    return [login_result, error_message]

  end
end