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:

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!