# -*- coding: binary -*-

module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes
  include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo
  include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs

  # Performs a ManageEngine ADAudit Plus login.
  #
  # @param auth_domain [String] The authentication domain to use to log in.
  # @param user [String] The username to log in as.
  # @param pass [String] The password to log in with.
  # @param only_get_cookie [Boolean] If this is set to true, then this method will only try to obtain an
  #   'adapcsrf' cookie that is required to perform API calls.
  # @return [Hash] Hash containing a `status` key, which is used to hold a
  #   status value as an Integer value, a `message` key, which is used
  #   to hold a message associated with the status value as a String. May optionally
  #   contain an `adapcsrf_cookie` key which maps to a String containing the
  #   adapcsrf cookie to be used for authentication purposes, and/or a
  #   `configured_domains` key which maps to an Array of Strings,
  #   each containing a domain name that has been configured to be used by
  #   the ManageEngine ADAudit Plus target.
  def adaudit_plus_login(auth_domain, user = '', pass = '', only_get_cookie = false)
    cookie_jar.clear # let's start fresh

    # Visit the default homepage to retrieve some of the baseline cookies needed to authenticate.
    res_initial_cookies = send_request_cgi({
      'uri' => normalize_uri(target_uri.path),
      'method' => 'GET',
      'keep_cookies' => true
    })

    unless res_initial_cookies
      return {
        'status' => adaudit_plus_status::CONNECTION_FAILED,
        'message' => 'Connection failed.'
      }
    end

    # Make sure the target is actually ManageEngine ADAudit Plus
    unless res_initial_cookies.code == 200 && res_initial_cookies.body =~ /<title>ADAudit Plus/
      return {
        'status' => adaudit_plus_status::UNEXPECTED_REPLY,
        'message' => 'Target does not seem to be ADAudit Plus.'
      }
    end

    # Check if we have an initial adapcsrf cookie with the expected format
    unless res_initial_cookies.headers.include?('Set-Cookie') && res_initial_cookies.get_cookies =~ /adapcsrf=[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/
      return {
        'status' => adaudit_plus_status::UNEXPECTED_REPLY,
        'message' => 'Failed to obtain the baseline cookies needed to proceed with authentication.'
      }
    end

    # Visit the adaudit_plus_jump_to_js_uri page to grab more cookies needed for authentication.
    vprint_status('Attempting to obtain the required cookies for authentication')

    res_extra_cookies = send_request_cgi({
      'uri' => adaudit_plus_jump_to_js_uri,
      'method' => 'GET',
      'keep_cookies' => true
    })

    unless res_extra_cookies
      return {
        'status' => adaudit_plus_status::CONNECTION_FAILED,
        'message' => 'Connection failed.'
      }
    end

    # check if we have a new adapcsrf cookie with the expected format, which is different
    # from the initial adapcsrf cookie format that we got before visiting the adaudit_plus_jump_to_js_uri URI.
    unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-f0-9]{128}/
      return {
        'status' => adaudit_plus_status::UNEXPECTED_REPLY,
        'message' => 'Failed to obtain the jump_to_js cookies required for authentication.'
      }
    end

    vprint_status('Trying to authenticate...')
    post_vars = {
      'forChecking' => '',
      'j_username' => user.to_s,
      'j_password' => pass.to_s,
      'domainName' => auth_domain.to_s,
      'AUTHRULE_NAME' => 'Authenticator'
    }

    res_login = send_request_cgi({
      'uri' => adaudit_plus_login_uri,
      'method' => 'POST',
      'keep_cookies' => true,
      'vars_post' => post_vars
    })

    # Check to see if the connection succeeded.
    return {
      'status' => adaudit_plus_status::CONNECTION_FAILED,
      'message' => 'Connection failed'
    } unless res_login

    # Check to see if we got the right response code and the expected cookies.
    unless res_login.code == 303 && res_login.headers.include?('Set-Cookie')
      # Matches something like JSESSIONIDADAP=50E42FBF96E820A6099A1F38FA5A4854; JSESSIONIDADAPSSO=7EB091F6BB9A7A4C4476419DFC11E2A1;
      # Or this JSESSIONIDADAP=50E42FBF96E820A6099A1F38FA5A4854; JSESSIONIDSSO=7EB091F6BB9A7A4C4476419DFC11E2A1;
      # Or even this JSESSIONIDADAP=50E42FBF96E820A6099A1F38FA5A4854; JSESSIONIDSSO=7EB091F6BB9A7A4C4476419DFC11E2A1
      unless res_login.get_cookies =~ /(?:JSESSIONID[A-Z].*?=[0-9A-Z]{32};{0,1} {0,1}){2}/
        return {
          'status' => adaudit_plus_status::NO_ACCESS,
          'message' => 'Failed to authenticate.'
        }
      end
    end

    # Check if we are actually logged in by visiting the home page.
    res_post_auth = send_request_cgi({
      'uri' => normalize_uri(target_uri.path),
      'method' => 'GET',
      'keep_cookies' => true
    })

    return {
      'status' => adaudit_plus_status::CONNECTION_FAILED,
      'message' => 'Connection failed'
    } unless res_post_auth

    unless res_post_auth.code == 200 && res_post_auth.body.include?('ManageEngine ADAudit Plus web client is initializing')
      return {
        'status' => adaudit_plus_status::NO_ACCESS,
        'message' => 'The web app failed to load after authenticating'
      }
    end

    # Return the value of the adapcsrf cookie, which will be required for later actions.
    adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first
    if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank?
      return {
        'status' => adaudit_plus_status::NO_ACCESS,
        'message' => 'Failed to obtain the required adapcsrf cookie'
      }
    end

    # In order to get a cookie we can actually use, we need to obtain the configured domains via the API,
    # so we will call adaudit_plus_grab_configured_domains to retrieve this information for us.
    # Note that adaudit_plus_obtain_configured_domains uses the same return format as this method.
    adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, only_get_cookie)
  end
end
