Checking for compromised email accounts

Checking for compromised email accounts

5/5 - (4 votes)

UPDATE: I have posted the script to check against at the bottom in the TechNet Gallery.

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:

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 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.

Check accounts in Active Directory and Office 365 against 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.

Output log file name.

.PARAMETER Office 365
Choose to run against Office 365.

param (
 # Credentials
 [string]$BreachedAccountOutput = (Get-Date -Format yyyy-MM-dd) + "_BreachedAccounts.csv",
 [string]$Logfile = (Get-Date -Format yyyy-MM-dd) + "_pwncheck.txt",

## 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 = []::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."
   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."
  Write-Log -LogFile $Logfile -LogLevel INFO -ConsoleOutput -Message "This requires the Microsoft Online Services Module. Attempting to download and install."
  wget -OutFile $env:TEMP\msoidcli_64.msi
  If (!(Get-Command Install-Module))
   wget -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 -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 and try again."
 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 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 = @(
[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 }
   # Check for MSOnline Module
   If (!(Get-Module -ListAvailable MSOnline))
    If ($InstallModules) { MSOnline }
    If (Get-Module -List MSOnline)
     Import-Module MSOnline
     Connect-MsolService -Credential $Credential
     Write-Log -LogFile $Logfile -Message "You must install the MSOnline module to continue." -LogLevel ERROR -ConsoleOutput
    # 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 = ""

$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
    [array]$Result = Invoke-RestMethod -Uri $uri -Headers $headers -ErrorAction SilentlyContinue
    $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 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!

Published by Aaron Guilmette

Helping companies conquer inferior technology since 1997. I spend my time developing and implementing technology solutions so people can spend less time with technology. Specialties: Active Directory and Exchange consulting and deployment, Virtualization, Disaster Recovery, Office 365, datacenter migration/consolidation, cheese.

Reader Comments

  1. Pingback: Homepage

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.