The situation
A user’s account started sending a shared-file invitation to a couple hundred external contacts. Nobody in our tenant had been targeted by a stranger. The lure came from a vendor the user corresponded with regularly, from that vendor’s real mailbox, as a standard Microsoft 365 “is inviting you to collaborate” share notification. The shared file lived on the vendor’s own SharePoint. Every reputation check along the way passed, because everything was legitimate right up to the link inside the PDF.
The attacker never needed to break our controls; they borrowed a trusted relationship and walked through them.
Investigation
Step 1: Find the lure
A 7-day message trace on the vendor’s sender address showed the share notification arriving for our user and a handful of other internal recipients. Get-MessageTrace was deprecated in September 2025, so use the V2 cmdlets (Exchange Online module 3.7.0 or later). V2 returns at most 10 days per query.
Connect-ExchangeOnline
# Everything the compromised vendor account sent into the tenant
Get-MessageTraceV2 -SenderAddress 'vendor.user@example.com' `
-StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) |
Select-Object Received, SenderAddress, RecipientAddress, Subject, Status |
Export-Csv .\lure-trace.csv -NoTypeInformation
That gave us the full internal recipient list. Each of those users was contacted, their sign-in logs were checked for anything unusual, and they reset their passwords as a precaution while the investigation continued.
Step 2: Confirm the session theft in the sign-in logs
The compromised user’s sign-in log is where the attacker’s IPs come from. In an AiTM compromise, the sign-in the user completed is relayed through the proxy, and the attacker’s follow-on sessions show up from infrastructure the user has never used, with MFA already satisfied.
The MFA detail fields (AuthenticationRequirement, AuthenticationDetails) only exist on the beta sign-in resource. The v1.0 Get-MgAuditLogSignIn returns the rows without them, so use the beta cmdlet:
# Install-Module Microsoft.Graph.Beta.Reports
Connect-MgGraph -Scopes 'AuditLog.Read.All','Directory.Read.All'
$start = (Get-Date).ToUniversalTime().AddDays(-7).ToString('yyyy-MM-ddTHH:mm:ssZ')
Get-MgBetaAuditLogSignIn -All -Filter "userPrincipalName eq 'user@contoso.com' and createdDateTime ge $start" |
Select-Object CreatedDateTime, IpAddress, AppDisplayName, ClientAppUsed,
@{n='Country';e={$_.Location.CountryOrRegion}},
AuthenticationRequirement,
@{n='MfaDetail';e={$_.AuthenticationDetails.AuthenticationStepResultDetail -join '; '}} |
Export-Csv .\signins.csv -NoTypeInformation
Look for sign-ins where MFA shows as satisfied by a claim in the token instead of an interactive prompt, coming from an IP or ASN the user has never used. Once you have the attacker IPs, run the same query tenant-wide with ipAddress eq '<ip>' to make sure no one else is affected.
Step 3: Reconstruct what they did
The unified audit log filled in the attacker’s activity. Filter on the operations that matter for this pattern instead of pulling everything:
$ops = @(
'MailItemsAccessed', # what they read (now logged for Audit Standard too)
'SearchQueryInitiatedExchange', # mailbox searches (Audit Premium, enabled per mailbox)
'Send', # what they sent
'New-InboxRule','Set-InboxRule','UpdateInboxRules',
'FileUploaded', # staging in OneDrive
'SharingSet','AnonymousLinkCreated','CompanyLinkCreated','SecureLinkCreated'
)
Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) `
-UserIds 'user@contoso.com' -Operations $ops -ResultSize 5000 |
Select-Object CreationDate, Operations, AuditData |
Export-Csv .\ual.csv -NoTypeInformation
SearchQueryInitiatedExchange is an Audit (Premium) event, and it is only logged once it has been switched on for the mailbox with Set-Mailbox <user> -AuditOwner @{Add="SearchQueryInitiated"}. If it was never enabled, an empty result means no data, not no searches.
The sequence, all in a single session on the same day:
| Stage | Attacker activity | Where it showed up |
|---|---|---|
| Recon | Searched the mailbox through the API and browsed Sent Items to learn who the user talks to | SearchQueryInitiatedExchange, MailItemsAccessed |
| Staging | Uploaded a credential-harvesting PDF to the user’s OneDrive and opened sharing on it | FileUploaded, sharing operations |
| Distribution | Mass-mailed the share link to external contacts, plus a few targeted direct messages | Send, message trace |
| Concealment | Created an inbox rule in the user’s mailbox | New-InboxRule |
| Persistence | Consented to an enterprise application on the user’s behalf | Entra audit log, consent operations |
What was not there mattered too. No sign of data exfiltration, business disruption or movement into other accounts or endpoints. This account was a launch pad, not a target: the attacker wanted the user’s address book and their reputation, and nothing else.
Step 4: Scope the outbound blast
A Purview compliance search against the user’s mailbox counted the outbound phishing messages: a couple hundred, nearly all external, with a couple sent internally. Keep these messages until notifications are done. They are your recipient list.
Connect-IPPSSession
New-ComplianceSearch -Name 'IR-Outbound-Phish' `
-ExchangeLocation 'user@contoso.com' `
-ContentMatchQuery 'subject:"is inviting you to collaborate" AND sent>=2026-01-01' # set to the incident window
Start-ComplianceSearch -Identity 'IR-Outbound-Phish'
Get-ComplianceSearch -Identity 'IR-Outbound-Phish' | Select-Object Status, Items
Pair it with a message trace on the user as sender, export the external recipient domains, and hand that list to whoever owns outside communication.
Step 5: Detonate the link
The staged PDF was pulled into a sandbox and the embedded link was opened there. The page behaved like a textbook AiTM kit: it proxied the real Microsoft sign-in, relayed the username and password to Microsoft, presented the real MFA prompt, and captured the session cookie that came back. The landing domain was a recently created throwaway with a long list of subdomains, several of them named after other organizations. We were not the only victim.
We still ran a full EDR scan on the user’s machine and reviewed recently installed applications, because you have to rule out an infostealer. With AiTM, the theft happens on the attacker’s server, so the user’s machine is usually a dead end. Check it, but don’t let it hold up the cloud-side containment.
Root cause
The user completed a real Microsoft sign-in, including MFA, through a reverse proxy. Microsoft issued a valid session cookie, the proxy kept a copy, and the attacker replayed it from their own infrastructure. Microsoft is explicit that this is not a vulnerability in MFA. Once the cookie is stolen, the attacker is authenticated regardless of how the user signed in.
The vendor angle is what got the user to click. Microsoft has documented the same supplier-to-supplier chain: a compromised vendor shares a file from a legitimate SharePoint, victims are phished through it, and each new victim’s mailbox is used to phish the next ring of contacts.
Remediation
Order matters. Contain first, then remove what the attacker left behind, then reset trust.
Connect-MgGraph -Scopes 'User.ReadWrite.All','UserAuthenticationMethod.ReadWrite.All',
'DelegatedPermissionGrant.ReadWrite.All','Directory.ReadWrite.All'
$u = Get-MgUser -UserId 'user@contoso.com'
# 1. Contain: block sign-in if the business can tolerate it, then kill refresh tokens
Update-MgUser -UserId $u.Id -AccountEnabled:$false
Revoke-MgUserSignInSession -UserId $u.Id
# 2. Review registered MFA methods; remove anything the user didn't set up
Get-MgUserAuthenticationMethod -UserId $u.Id |
Select-Object Id, @{n='Type';e={$_.AdditionalProperties.'@odata.type'}}
# 3. Find delegated consent grants made by this user
Get-MgOauth2PermissionGrant -All -Filter "principalId eq '$($u.Id)'" |
Select-Object Id, ClientId, Scope, ConsentType
# Map ClientId to an app name with Get-MgServicePrincipal -ServicePrincipalId <ClientId>, then:
# Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId <Id>
# 4. Inbox rules: -IncludeHidden catches rules that don't show in Outlook
Get-InboxRule -Mailbox 'user@contoso.com' -IncludeHidden |
Select-Object Name, Enabled, From, SubjectOrBodyContainsWords,
MoveToFolder, MarkAsRead, DeleteMessage, ForwardTo, RedirectTo
# Remove-InboxRule -Mailbox 'user@contoso.com' -Identity '<rule>'
Then the rest:
| Action | Why |
|---|---|
| Reset the password | The attacker captured it in cleartext at the proxy. |
| Re-register MFA | Treat the existing registration as untrusted until verified. |
| Remove sharing links, delete the staged PDF, purge it from the second-stage recycle bin | Deleting the file isn’t enough if it can be restored and the link resurrected. Sandbox a copy first. |
| Notify internal users and the vendor | The vendor still has a compromised account phishing everyone they know. |
| Notify external recipients | Your user’s address is now the lure. Give recipients something specific to look for. |
| Report the phishing domain | Registrar abuse contacts don’t always work. Ours had a full mailbox and a looping voicemail. Report through more than one channel. |
| Re-enable the account | Only after the steps above, with the user signing in fresh. |
Locking down user consent
Removing the malicious app grant fixes this account. The tenant setting that allowed the grant in the first place is what keeps it from happening on the next compromise. If a user can consent to an app reading their mail, so can anyone holding that user’s session.
The setting lives at Entra ID › Enterprise apps › Consent and permissions › User consent settings. Here are your options, from tightest to loosest:
| Option | Policy ID | What users can consent to |
|---|---|---|
| Do not allow user consent | none assigned | Nothing. Every app needs an admin. |
| Allow user consent for apps from verified publishers, for selected permissions | microsoft-user-default-low | Only permissions you classify as low impact, and only for verified publishers or apps registered in your tenant. |
| Let Microsoft manage your consent settings | microsoft-user-default-recommended | Most delegated permissions, excluding mail, files, sites, calendars, chat, contacts and legacy protocol access such as EWS and IMAP. Microsoft updates the list over time. This is the default for new tenants. |
| Allow user consent for apps | microsoft-user-default-legacy | Any permission that doesn’t require admin consent, from any publisher. Move off this one. |
For a small tenant, I’d go with Do not allow user consent, paired with the admin consent workflow (Entra ID › Enterprise apps › Consent and permissions › Admin consent settings). With the workflow on, users who hit a blocked app can request it instead of shadow-IT’ing around it, and your reviewers get an email. This is CIS Microsoft 365 Foundations 5.1.5.2. Two details to know: designating a reviewer doesn’t grant them any privileges, so pick people who already hold an app admin role, and only a Global Administrator can approve requests for apps asking for Microsoft Graph application permissions. If blocking all consent is too much friction for a client, the Microsoft-managed option is the minimum. It blocks exactly the mail and file permissions a mailbox-hijacking attacker wants.
To check and change it in PowerShell:
Connect-MgGraph -Scopes 'Policy.ReadWrite.Authorization' # sign in as Privileged Role Administrator
# managePermissionGrantsForSelf.* is the user consent policy
$current = (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions.PermissionGrantPoliciesAssigned
$current
# Drop the user consent policy, but keep any managePermissionGrantsForOwnedResource.* entries
$keep = @($current | Where-Object { $_ -notlike 'managePermissionGrantsForSelf.*' })
# Uncomment to use the Microsoft-managed policy instead of blocking consent outright
# $keep += 'managePermissionGrantsForSelf.microsoft-user-default-recommended'
Update-MgPolicyAuthorizationPolicy -BodyParameter @{
defaultUserRolePermissions = @{ permissionGrantPoliciesAssigned = $keep }
}
# Confirm the result
(Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Format-List
This change only applies to future consents. Grants that already exist stay in place, so run the Get-MgOauth2PermissionGrant review from the remediation step tenant-wide, not just on the compromised user.
Verification
- Sign-ins: re-run the sign-in query. Nothing from the attacker IPs after the revocation, and the user’s next sign-in shows a fresh interactive MFA.
- Tenant-wide IP sweep: the attacker IPs appear on no other account.
- Audit log: no
Send,FileUploadedor inbox rule operations from non-user IPs after containment. - Mailbox and apps:
Get-InboxRule -IncludeHiddenis clean, and the user has no consent grants you can’t explain. - Mail flow: message trace shows no further outbound sends matching the campaign.
- Reproduce the technique: the sandbox detonation is your proof of mechanism. If your controls are in place (see below), replaying the same flow against a test account should now fail.
Caveats
Compliance impact. The attacker read mail. Whether that triggers a notification obligation depends on what was in it:
- HIPAA: use
MailItemsAccessedto determine which messages were opened, then complete the breach risk assessment for any PHI. - CMMC / DFARS 252.204-7012: if CUI was in the mailbox or OneDrive, the 72-hour reporting clock applies.
- PCI DSS and SOC 2: document the incident against your IR plan and the related control (Requirement 12.10, CC7.3–CC7.5), including how you scoped the access.
MailItemsAccessed is throttled when a mailbox generates very high access volume, so treat an absence of records as “unknown,” not “untouched.”
Fixes have limits too. Token Protection binds sign-in session tokens to a device, but it covers native applications, not browser sessions in general, which is exactly what this attack used. The control that breaks AiTM at the source is phishing-resistant authentication: FIDO2 security keys, passkeys, Windows Hello for Business or certificate-based authentication, enforced with a Conditional Access authentication strength. A FIDO2 authenticator won’t complete a challenge for the proxy’s origin. If a full rollout isn’t feasible, start with admins and the people vendors email most: finance, AP and executives. Pair it with a Conditional Access policy requiring a compliant or registered device, so a cookie replayed from attacker infrastructure has nothing to satisfy.
Key takeaways
- Trusted senders are the delivery mechanism. A vendor’s real mailbox and real SharePoint will pass every reputation check. Train users that “shared a file” from a known contact still warrants a check when it’s unexpected.
- MFA completed is not MFA protected. Push and code-based MFA can be proxied. Phishing-resistant methods can’t be.
- A password reset is the start of cleanup, not the end. Revoke sessions, re-register MFA, and remove inbox rules (with
-IncludeHidden) and OAuth grants every time. - Scope outward. The compromised account’s sent mail is the list of the next victims. Notifying them is part of containment.
- Hunt by IP across the tenant. The attacker’s infrastructure is usually shared across victims. Check whether it touched anyone else before you close the ticket.
References
- Microsoft Security Blog: From cookie theft to BEC: Attackers use AiTM phishing sites as entry point to further financial fraud
- Microsoft Security Blog: Detecting and mitigating a multi-stage AiTM phishing and BEC campaign
- Microsoft Learn: Revoke user access in Microsoft Entra ID
- Microsoft Learn: Continuous access evaluation
- Microsoft Learn: Token Protection in Conditional Access
- Microsoft Learn: Conditional Access authentication strengths
- Microsoft Learn: Get-InboxRule
- Microsoft Learn: Set up Audit (Premium)
- Microsoft Learn: Get-MessageTraceV2
- Microsoft Learn: Detect and remediate illicit consent grants
- Microsoft Learn: Configure how users consent to applications
- Microsoft Learn: Manage app consent policies
- Microsoft Learn: Configure the admin consent workflow
- CIS Microsoft 365 Foundations Benchmark, 5.1.5.2: Ensure the admin consent workflow is enabled
- MITRE ATT&CK: T1586.002 Compromise Accounts: Email Accounts, T1566.002 Spearphishing Link, T1557 Adversary-in-the-Middle, T1539 Steal Web Session Cookie, T1550.004 Web Session Cookie, T1114.002 Remote Email Collection, T1608.005 Link Target, T1564.008 Email Hiding Rules, T1534 Internal Spearphishing