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.

AiTM vendor phish: attack chainInitial access: a compromised vendor mailbox sends a share notification with a PDF hosted on the vendor's own SharePoint. The PDF links to an AiTM proxy that relays the user's password and real MFA prompt to Microsoft. Microsoft issues a session cookie, the proxy keeps a copy, and the attacker replays it with MFA already satisfied. Inside the tenant, in one session on the same day, the attacker runs recon, staging, distribution, concealment and persistence. Distribution to the user's contacts starts the same chain again for the next ring of victims.Initial accessCompromisedvendor mailboxa real, trusted senderShare notificationPDF on the vendor'sown SharePointAiTM proxyrelays password andthe real MFA promptSession cookieissued by Microsoft,copied by the proxyCookie replayedfrom attacker infra,MFA already satisfiedIn our tenant: one session, same dayReconmailbox search,Sent ItemsStagingphishing PDF inOneDrive, sharedDistributiona couple hundredexternal contactsConcealmentinbox rulecreatedPersistenceenterprise appconsentEach recipient who falls for it becomes the next "compromised vendor" for their own contacts.
The chain end to end. Nothing in the first row touched our tenant’s controls: the theft happened on the attacker’s proxy.

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:

StageAttacker activityWhere it showed up
ReconSearched the mailbox through the API and browsed Sent Items to learn who the user talks toSearchQueryInitiatedExchange, MailItemsAccessed
StagingUploaded a credential-harvesting PDF to the user’s OneDrive and opened sharing on itFileUploaded, sharing operations
DistributionMass-mailed the share link to external contacts, plus a few targeted direct messagesSend, message trace
ConcealmentCreated an inbox rule in the user’s mailboxNew-InboxRule
PersistenceConsented to an enterprise application on the user’s behalfEntra 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.

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:

ActionWhy
Reset the passwordThe attacker captured it in cleartext at the proxy.
Re-register MFATreat the existing registration as untrusted until verified.
Remove sharing links, delete the staged PDF, purge it from the second-stage recycle binDeleting the file isn’t enough if it can be restored and the link resurrected. Sandbox a copy first.
Notify internal users and the vendorThe vendor still has a compromised account phishing everyone they know.
Notify external recipientsYour user’s address is now the lure. Give recipients something specific to look for.
Report the phishing domainRegistrar abuse contacts don’t always work. Ours had a full mailbox and a looping voicemail. Report through more than one channel.
Re-enable the accountOnly after the steps above, with the user signing in fresh.

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:

OptionPolicy IDWhat users can consent to
Do not allow user consentnone assignedNothing. Every app needs an admin.
Allow user consent for apps from verified publishers, for selected permissionsmicrosoft-user-default-lowOnly permissions you classify as low impact, and only for verified publishers or apps registered in your tenant.
Let Microsoft manage your consent settingsmicrosoft-user-default-recommendedMost 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 appsmicrosoft-user-default-legacyAny 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, FileUploaded or inbox rule operations from non-user IPs after containment.
  • Mailbox and apps: Get-InboxRule -IncludeHidden is 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 MailItemsAccessed to 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

  1. 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.
  2. MFA completed is not MFA protected. Push and code-based MFA can be proxied. Phishing-resistant methods can’t be.
  3. 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.
  4. Scope outward. The compromised account’s sent mail is the list of the next victims. Notifying them is part of containment.
  5. 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