The first thing a mailbox-takeover actor does is set up forwarding to an outside address, then delete the rule trace. Microsoft enforces "external forwarding blocked" by default in newer tenants, but inbox rules with redirect / forward to an external recipient still slip through, and the audit pass is essential.
Table of Contents
Why forwarding rules matter
BEC playbooks share three properties: silence (the mailbox owner does not see incoming finance email), persistence (the rule survives a password reset), and exfiltration (a copy goes to the attacker). All three are achieved with one well-crafted inbox rule.
Mailbox-level forwarding
This is the property set on the mailbox itself, not in a rule:
Connect-MgGraph -Scopes 'MailboxSettings.Read','User.Read.All'
Get-MgUser -All -Property Id,UserPrincipalName | ForEach-Object {
$s = Get-MgUserMailboxSetting -UserId $_.Id -ErrorAction SilentlyContinue
if ($s.AutomaticRepliesSetting -or $s.DelegateMeetingMessageDeliveryOptions) { }
if ($s.UserPurpose) { } # placeholder for future
}
For the equivalent of ForwardingSmtpAddress, the Graph SDK still relies on Exchange Online cmdlets โ Get-Mailbox | Select ForwardingSmtpAddress, ForwardingAddress, DeliverToMailboxAndForward. Use both modules together until Graph parity lands.
Inbox rules
Get-MgUser -All -Property Id,UserPrincipalName | ForEach-Object {
$u = $_
Get-MgUserMessageRule -UserId $u.Id -ErrorAction SilentlyContinue |
Where-Object { $_.Actions.ForwardTo -or $_.Actions.RedirectTo -or $_.Actions.ForwardAsAttachmentTo } |
ForEach-Object {
[pscustomobject]@{
UPN = $u.UserPrincipalName
Rule = $_.DisplayName
Forward = ($_.Actions.ForwardTo + $_.Actions.RedirectTo + $_.Actions.ForwardAsAttachmentTo).EmailAddress.Address -join ','
Enabled = $_.IsEnabled
}
}
}
Detect external recipients
$internalDomains = @('contoso.com','contoso.onmicrosoft.com')
$above | Where-Object {
$hits = $_.Forward -split ','
($hits | Where-Object { $d = ($_ -split '@')[1]; $internalDomains -notcontains $d }).Count -gt 0
}
Any rule that forwards to an address outside your accepted domains is the actionable list.
Remediate safely
- Snapshot the rule (export to JSON for the IR record).
- Disable the rule (do not delete yet).
- Force a password reset and revoke refresh tokens.
- Audit sign-in logs for the same user โ country, device, IP.
- Open an IR ticket if anything looks off.
Continuous detection
The Microsoft Defender for Office 365 alert "Suspicious inbox forwarding rule" covers some cases. Pair with a weekly cron of the script above (logged to a SIEM) so nothing slips between alerts.
Audit checklist
- Tenant external forwarding policy = Disabled or Restricted (1 pt)
- Zero mailboxes with ForwardingSmtpAddress to external (1 pt)
- Zero inbox rules forwarding to external (1 pt)
- Defender alert "Suspicious inbox forwarding" enabled (1 pt)
- Weekly cron of audit script with SIEM ingest (1 pt)
Detection across all forwarding mechanisms
Attackers create forwards in three places, and a complete audit checks all three: mailbox-level ForwardingSmtpAddress, inbox rules, and transport rules. A script that only checks one is the security equivalent of locking the front door and leaving the patio open.
Mailbox-level forwarding
The classic exfiltration channel. ForwardingSmtpAddress sends a copy of every inbound message to an external address. Pull every mailbox and flag any non-empty value that points outside your accepted domains.
$accepted = (Get-AcceptedDomain).DomainName # via ExchangeOnlineManagement
$mbx = Get-EXOMailbox -ResultSize Unlimited `
-Properties ForwardingSmtpAddress, DeliverToMailboxAndForward |
Where-Object ForwardingSmtpAddress
$external = $mbx | Where-Object {
$domain = ($_.ForwardingSmtpAddress -split '@')[-1].TrimEnd(';')
$accepted -notcontains $domain
}
$external | Select DisplayName, PrimarySmtpAddress, ForwardingSmtpAddress |
Export-Csv -Path 'fwd-mailbox-external.csv' -NoTypeInformation
Inbox rules per user via Graph
Inbox rules are user-owned and survive password resets. The Graph endpoint users/{id}/mailFolders/inbox/messageRules exposes them โ iterate every user, every rule, every action.
$users = Get-MgUser -All -Property Id, UserPrincipalName
$flagged = foreach ($u in $users) {
try {
$rules = Get-MgUserMailFolderMessageRule -UserId $u.Id -MailFolderId Inbox -All
} catch { continue } # mailbox not provisioned, skip
foreach ($r in $rules | Where-Object IsEnabled) {
$a = $r.Actions
if ($a.ForwardTo -or $a.RedirectTo -or $a.ForwardAsAttachmentTo -or $a.MoveToFolder -eq 'deleteditems') {
[pscustomobject]@{
User = $u.UserPrincipalName
Rule = $r.DisplayName
Forward = ($a.ForwardTo + $a.RedirectTo + $a.ForwardAsAttachmentTo |
ForEach-Object { $_.EmailAddress.Address }) -join '; '
Actions = ($r.Actions.PSObject.Properties | Where-Object Value).Name -join ','
}
}
}
}
Transport rules: tenant-wide forwarding
A malicious admin can create a transport rule that forwards every message from a specific user, mailbox or pattern. Pull them with Get-TransportRule and verify every RedirectMessageTo and BlindCopyTo action.
Common pitfalls
- Trusting
RemoteDomainAutoForwardEnabled. Tenant-wide block helps but does not stop inbox rules usingredirect. Check rules explicitly. - Forgetting shared mailboxes. Shared mailboxes have inbox rules too and are a frequent attacker target precisely because they are unmonitored.
- Allowing partner domains as exceptions without review. Every exception ages โ review the partner-domain allow-list quarterly.
- No alerting on rule creation. Audit-after-the-fact catches forwarders eventually. An alert on
New-InboxRulein the Unified Audit Log catches them in minutes. - Removing rules without preserving evidence. Export the rule definition before deletion. The body of the rule is part of the incident record.
Incident-response playbook when an external forward is found
Finding an unauthorised external forwarding rule is a confirmed compromise indicator until proven otherwise. The 60-minute response below contains the blast radius, preserves evidence, and starts the formal investigation.
- Minute 0โ5 โ preserve. Export the rule body before doing anything else:
Get-InboxRule -Mailbox $u -Identity $r | Export-Clixml ir-evidence-$(Get-Date -f yyyyMMddHHmm).xml. Without the original definition you lose the indicator. - Minute 5โ15 โ contain. Disable the rule (do not delete yet). Block the destination domain at the transport layer. Reset the user's credential and revoke all existing sessions:
Revoke-MgUserSignInSession -UserId $u. - Minute 15โ30 โ scope. Pull the user's sign-in log for the last 30 days. Look for impossible-travel events, sign-ins from new IPs, and any successful authentication after the rule's CreatedDateTime. Any anomaly expands the investigation to that endpoint.
- Minute 30โ45 โ broaden the audit. Re-run the forwarding audit across the entire tenant filtered to rules created within ยฑ48 hours of the indicator timestamp. Attackers create rules in batches; the first one is rarely the only one.
- Minute 45โ60 โ communicate. Notify the user (without details if you suspect insider). Notify their manager. Open the formal IR ticket with the evidence file attached. Schedule the 24-hour follow-up review.
The 24-hour review checks: did the destination domain receive any messages before containment? Did the user create other rules elsewhere (Power Automate, Teams)? Are there sign-in events on adjacent accounts from the same source IP? Each yes expands the investigation; each no shrinks it.
Document the whole sequence in a runbook stored next to the audit script. The next analyst should be able to execute it without rediscovering the steps under pressure.
Continuous monitoring: catching the next forward in minutes, not weeks
A nightly audit catches the forwarding rule that an attacker created the previous day. A real-time monitoring pipeline catches it within minutes โ long before the attacker has read enough mail to do meaningful damage. Building the pipeline takes a single afternoon and pays for itself the first time it fires.
The foundation is the Unified Audit Log. Every inbox-rule creation, mailbox-forwarding configuration change, and transport-rule edit writes a structured event with the actor, target, parameters and timestamp. Stream the audit log into your SIEM and write three rules: alert on any new inbox rule with a forwarding action; alert on any change to mailbox ForwardingSmtpAddress; alert on any new transport rule with RedirectMessageTo or BlindCopyTo.
Tune for noise. Most legitimate inbox rules โ file an email, mark as read โ are not forwarding rules. The first week of monitoring will surface a handful of legitimate forwards (an out-of-office alternate, an executive's assistant). Whitelist them by user and rule name, and the alert volume drops to near-zero. From then on, every fire is worth investigating.
Pair the alerts with a weekly trend report: count of forwarding rules created, count by user, count of external destinations. Trends matter: a single rule is a finding; ten rules from ten users in one day is a campaign. Both deserve different responses, and the chart makes the difference visible.
For tenants serious about exfiltration prevention, layer in Defender for Office 365 alerting. The Suspicious Email Forwarding alert detects rules that match attacker patterns (forwarding to recently-created free-mail accounts, forwarding messages containing "invoice" or "wire") and fires before the rule has done meaningful damage. Combined with the audit-log monitoring, the two cover both novel and pattern-matched threats.
Document the response procedure once and route every alert through the same playbook. Consistency makes the response faster and the evidence collection cleaner; ad-hoc responses lose detail every time and slow the post-incident review.
FAQ
Why use Graph and not just Exchange Online cmdlets?
Graph is faster at scale and supports the same app-only auth pattern as the rest of your audit. Use EXO where Graph parity is missing โ but converge on Graph for the rest.
Does the audit catch hidden rules?
It catches every messageRule. Truly hidden rules require the EXO Get-InboxRule -IncludeHidden cmdlet โ combine both for full coverage.
What about Outlook desktop client rules?
Server-side rules (the kind we audit) sync to all clients. Client-only rules do not exfiltrate to external recipients โ they cannot run without the client open.
How do I prevent users from creating new external forwards?
Set the outbound spam filter to disable automatic forwarding. Combined with the audit, this is the standard belt-and-braces configuration.
Will this catch forwarding via Power Automate?
No โ Power Automate uses its own connectors and is invisible to mailbox-level audit. Audit Power Automate flows separately via the Power Platform admin centre.
What permission does the audit script need?
MailboxSettings.Read for the Graph calls and the Exchange role View-Only Recipients + View-Only Configuration for the EXO cmdlets. Application permissions, not delegated.
Can a user see that I ran this audit?
No. The audit is read-only and does not generate user-visible artifacts. The Unified Audit Log records that an admin ran it, which is the appropriate paper trail.