Checking for compromised email accounts
Yesterday, I participated in an escalation for a customer where one or more users had been successfully phished and had given up their credentials. While we were walking through some remediation steps, we started a discussion about data exfiltration attempts.
Many moons ago, I put together a few scripts that can be used to check mailbox forwarding and transport rule forwarding configurations, specifically looking for actions that send mail (forward, redirect, bcc) to recipients outside of the domains verified in your tenant. You can see those here:
- Audit Mailbox Rules (https://gallery.technet.microsoft.com/Audit-Mailbox-Rules-to-60710f28): The idea of this script is to check your user's mailbox rules for actions that can relay mail to external users. It produces a report.
- Audit Transport Rules (https://gallery.technet.microsoft.com/Audit-Transport-Rules-to-1dd8acee): This does a similar thing, only checking transport rules for similar types of activities. And, what would an audit be without a report?
Those can be great for looking at the current state of things. One of the drawbacks of the Get-InboxRules cmdlet is that it doesn't reveal when a rule was created.
If you have turned on all of your tenant auditing (which I definitely recommend you do), I'd recommend scouring the audit logs for entries regarding new rules as well. This may help you in pinpointing new activity or identifying further compromised accounts.
$RuleLogs = Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-90) -EndDate (Get-Date) -Operations @('New-InboxRule', 'Set-InboxRule')
[array]$entries = @()
foreach ($entry in $RuleLogs)
{
$entry | `
Select CreationDate, UserIds, Operations, @{
l = 'Rule'; `
e = { (($entry.AuditData | ConvertFrom-Json).Parameters | ? { $_.Name -eq "Name" }).Value }
} | `
Export-Csv .\90DayRules.csv -append -notypeinformation
}
The output is a simple CSV showing you the user date, user ID, Operation (New-InboxRule, Set-InboxRule) and what the name of the rule is.
I also put together another script that I'm still tidying up before I put it on the gallery. It uses the haveibeenpwned.com API to get a list of accounts whose addresses have shown up in various breach notifications. It's a little rough at the moment, but you can use it against both Office 365 accounts and local Active Directory.
<#
.SYNOPSIS
Check accounts in Active Directory and Office 365 against
haveibeenpwned.com database
.PARAMETER ActiveDirectory
Choose to run against Active Directory
.PARAMETER BreachedAccountOutput
CSV filename for any potentially breached accounts
.PARAMETER IncludeGuests
If querying Office 365, choose if you want to include external guests. Otherwise
only objects with type MEMBER are selected.
.PARAMETER InstallModules
Choose if you want to install MSOnline and supporting modules.
.PARAMETER Logfile
Output log file name.
.PARAMETER Office 365
Choose to run against Office 365.
#>
param (
# Credentials
[System.Management.Automation.PSCredential]$Credential,
[switch]$ActiveDirectory,
[string]$BreachedAccountOutput = (Get-Date -Format yyyy-MM-dd) + "_BreachedAccounts.csv",
[switch]$IncludeGuests,
[switch]$InstallModules,
[string]$Logfile = (Get-Date -Format yyyy-MM-dd) + "_pwncheck.txt",
[switch]$Office365
)
## Functions
# Logging function
function Write-Log([string[]]$Message, [string]$LogFile = $Script:LogFile, [switch]$ConsoleOutput, [ValidateSet("SUCCESS", "INFO", "WARN", "ERROR", "DEBUG")][string]$LogLevel)
{
$Message = $Message + $Input
If (!$LogLevel) { $LogLevel = "INFO" }
switch ($LogLevel)
{
SUCCESS { $Color = "Green" }
INFO { $Color = "White" }
WARN { $Color = "Yellow" }
ERROR { $Color = "Red" }
DEBUG { $Color = "Gray" }
}
if ($Message -ne $null -and $Message.Length -gt 0)
{
$TimeStamp = [System.DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss")
if ($LogFile -ne $null -and $LogFile -ne [System.String]::Empty)
{
Out-File -Append -FilePath $LogFile -InputObject "[$TimeStamp] [$LogLevel] $Message"
}
if ($ConsoleOutput -eq $true)
{
Write-Host "[$TimeStamp] [$LogLevel] :: $Message" -ForegroundColor $Color
}
}
}
function MSOnline
{
Write-Log -LogFile $Logfile -LogLevel INFO -Message "Checking Microsoft Online Services Module."
If (!(Get-Module -ListAvailable MSOnline -ea silentlycontinue) -and $InstallModules)
{
# Check if Elevated
$wid = [system.security.principal.windowsidentity]::GetCurrent()
$prp = New-Object System.Security.Principal.WindowsPrincipal($wid)
$adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator
if ($prp.IsInRole($adm))
{
Write-Log -LogFile $Logfile -LogLevel SUCCESS -ConsoleOutput -Message "Elevated PowerShell session detected. Continuing."
}
else
{
Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "This application/script must be run in an elevated PowerShell window. Please launch an elevated session and try again."
$ErrorCount++
Break
}
Write-Log -LogFile $Logfile -LogLevel INFO -ConsoleOutput -Message "This requires the Microsoft Online Services Module. Attempting to download and install."
wget https://download.microsoft.com/download/5/0/1/5017D39B-8E29-48C8-91A8-8D0E4968E6D4/en/msoidcli_64.msi -OutFile $env:TEMP\msoidcli_64.msi
If (!(Get-Command Install-Module))
{
wget https://download.microsoft.com/download/C/4/1/C41378D4-7F41-4BBE-9D0D-0E4F98585C61/PackageManagement_x64.msi -OutFile $env:TEMP\PackageManagement_x64.msi
}
If ($DebugLogging) { Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Installing Sign-On Assistant." }
msiexec /i $env:TEMP\msoidcli_64.msi /quiet /passive
If ($DebugLogging) { Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Installing PowerShell Get Supporting Libraries." }
msiexec /i $env:TEMP\PackageManagement_x64.msi /qn
If ($DebugLogging) { Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Installing PowerShell Get Supporting Libraries (NuGet)." }
Install-PackageProvider -Name Nuget -MinimumVersion 2.8.5.201 -Force -Confirm:$false
If ($DebugLogging) { Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Installing Microsoft Online Services Module." }
Install-Module MSOnline -Confirm:$false -Force
If (!(Get-Module -ListAvailable MSOnline))
{
Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "This Configuration requires the MSOnline Module. Please download from https://connect.microsoft.com/site1164/Downloads/DownloadDetails.aspx?DownloadID=59185 and try again."
$ErrorCount++
Break
}
}
If (Get-Module -ListAvailable MSOnline) { Import-Module MSOnline -Force }
Else { Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "This Configuration requires the MSOnline Module. Please download from https://connect.microsoft.com/site1164/Downloads/DownloadDetails.aspx?DownloadID=59185 and try again." }
Write-Log -LogFile $Logfile -LogLevel INFO -Message "Finished Microsoft Online Service Module check."
} # End Function MSOnline
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ForestFQDN = (Get-ChildItem Env:\USERDNSDOMAIN).Value.ToString()
# Build header and parameter functions
$Excluded = @(
'Credential',
'ForestFQDN',
'InstallModules',
'IncludeGuests',
'Logfile,'
'BreachedAccountOutput')
[regex]$ParametersToExclude = '(?i)^(\b' + (($Excluded | foreach { [regex]::escape($_) }) –join "\b|\b") + '\b)$'
$Params = $PSBoundParameters.Keys | ? { $_ -notmatch $ParametersToExclude }
[System.Text.StringBuilder]$UserAgentString = "Compromised User Account Check -"
# If no parameters are listed, assume Office 365
If (!($Params -match "ActiveDirectory|Office365" )) { $Params = "Office365"}
# Collect users
[array]$global:users = @()
[array]$ADUsers = @()
[array]$MSOLUsers = @()
switch ($Params)
{
# Get users from Active Directory
ActiveDirectory {
$UserAgentString.Append(" Active Directory Forest $($ForestFQDN)") | Out-Null
[array]$ADusers = Get-ADUser -prop guid, enabled, displayname, userprincipalname, proxyAddresses, PasswordNeverExpires, PasswordLastSet, whenCreated | select @{ N = "ObjectId"; E = { $_.Guid.ToString() } }, DisplayName, UserPrincipalName, @{ N = "LogonStatus"; E = { if ($_.Enabled -eq $True) { "Enabled" } else { "Disabled" } } }, @{ N = "LastPasswordChange"; E = { $_.PasswordLastSet } }, @{ N = "StsRefreshTokensValidFrom"; E= { "NotValidForADUsers" } },proxyAddresses, PasswordNeverExpires, WhenCreated }
# Get users from Office 365
Office365 {
Try { Get-MsolCompanyInformation -ea Stop | Out-Null }
Catch
{
# Check for MSOnline Module
If (!(Get-Module -ListAvailable MSOnline))
{
#
If ($InstallModules) { MSOnline }
If (Get-Module -List MSOnline)
{
Import-Module MSOnline
Connect-MsolService -Credential $Credential
}
Else
{
Write-Log -LogFile $Logfile -Message "You must install the MSOnline module to continue." -LogLevel ERROR -ConsoleOutput
Break
}
# Check for credential
If (!($Credential)) { $Credential = Get-Credential }
Import-Module MSOnline -Force
Connect-MsolService -Credential $Credential
}
}
If (Get-Module -List MSOnline)
{
Import-Module MSOnline -Force
If (!($Credential)) { $Credential = Get-Credential }
Connect-MsolService -Credential $Credential
$TenantDisplay = (Get-MsolCompanyInformation).DisplayName
$UserAgentString.Append(" Office 365 Tenant - $($DisplayName)") | Out-Null
[array]$MSOLUsers = Get-MsolUser -All | select ObjectId, DisplayName, UserPrincipalName, ProxyAddresses, StsRefreshTokensValidFrom, @{N = "PasswordNeverExpires"; e= { $_.PasswordNeverExpires.ToString() } }, @{ N = "LastPasswordChange"; e = { $_.LastPasswordChangeTimestamp } }, LastDirSyncTime, WhenCreated, UserType
If (!($IncludeGuests)) { $MSOLUsers = $MSOLusers | ? { $_.UserType -eq "Member" } }
}
}
}
$headers = @{
"User-Agent" = $UserAgentString.ToString()
"api-version" = 2
}
$baseUri = "https://haveibeenpwned.com/api"
$users += $ADUsers
$users += $MSOLUsers
if ($users.count -ge 1)
{
foreach ($user in $users)
{
# get all proxy addresses for users and add to an array
[array]$addresses = $user.proxyaddresses | Where-Object { $_ -imatch "smtp:" }
# trim smtp: and SMTP: from proxy array
$addresses = $addresses.SubString(5)
# add the user UPNs. This can potentially be important if:
# - query is being done against Active Directory and only UPNs were gathered
# - customer is using alternate ID to log on to Office 365 and accounts may
$addresses += $user.userprincipalname
# if guest accounts were selected, their email addresses will show up as
# under proxyAddresses, but their UPNs will have #EXT# in them, so we're
# going to take those out
[array]$global:Errors = @()
$addresses = $addresses -notmatch "\#EXT\#\@" | Sort -Unique
foreach ($mail in $addresses)
{
#$addresses | ForEach-Object {
#$mail = $_
$uriEncodeEmail = [uri]::EscapeDataString($mail)
$uri = "$baseUri/breachedaccount/$uriEncodeEmail"
$Result = $null
try
{
[array]$Result = Invoke-RestMethod -Uri $uri -Headers $headers -ErrorAction SilentlyContinue
}
catch
{
$global:Errors += $error
if ($error[0].Exception.response.StatusCode -match "NotFound" -or $error[0].Exception -match "404")
{
Write-Log -LogFile $Logfile -LogLevel INFO -Message "No Breach detected for $mail" -ConsoleOutput
}
if ($error[0].Exception -match "429" -or $error[0].Exception -match "503")
{
Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Rate limiting is in effect. See https://haveibeenpwned.com/API/v2 for rate limit details."
}
}
if ($Result)
{
foreach ($obj in $Result)
{
$RecordData = [ordered]@{
EmailAddress = $mail
UserPrincipalName = $user.UserPrincipalName
LastPasswordChange = $user.LastPasswordChange
StsRefreshTokensValidFrom = $user.StsRefreshTokensValidFrom
PasswordNeverExpires = $user.PasswordNeverExpires
UserAccountEnabled = $user.LogonStatus
BreachName = $obj.Name
BreachTitle = $obj.Title
BreachDate = $obj.BreachDate
BreachAdded = $obj.AddedDate
BreachDescription = $obj.Description
BreachDataClasses = ($obj.dataclasses -join ", ")
IsVerified = $obj.IsVerified
IsFabricated = $obj.IsFabricated
IsActive = $obj.IsActive
IsRetired = $obj.IsRetired
IsSpamList = $obj.IsSpamList
}
$Record = New-Object PSobject -Property $RecordData
$Record | Export-csv $BreachedAccountOutput -NoTypeInformation -Append
Write-Log -LogFile $Logfile -Message "Possible breach detected for $mail - $($obj.Name) on $($obj.BreachDate)" -LogLevel WARN -ConsoleOutput
}
}
Sleep -Seconds 2
}
}
}
When you run it, it will show you data that looks like this:
Happy hunting!