presenting ←→ nav F fullscreen Esc exit 1 / 23
CASE FILE ◆ M365-IR-001 ◆ CONFIDENTIAL
01 / 23 Certified Made in K12 badge
Docketed · BSides Knoxville 2026

Password reset is not incident response.

Inadequate Re-open file

Hunting OAuth persistence in Microsoft 365 — and the cleanup steps that actually evict the attacker.

Read the playbook

Companion reading Phishing Persistence: 10 Steps to Securing a Compromised M365 Account ↗

Filed by
Andy Lombardo
Issued
2026
Audience
K12 security teams
Status
ACTIVE · ONGOING
Scan for the references QR code linking to oauth.bardsec.com oauth.bardsec.com
Investigator on file
Andy Lombardo AL

Andy Lombardo

K12 Tech Director · K12 Security Information Exchange (K12 SIX) · SupportStudioK12 Founder · Security Awareness Content Creator. I publish at these sites — pick whichever matches your itch.

02 / 23
Audience participation · optional

Wanna give me some dummy data for our demo later?

Send an email to…

Sending an email to this address may involve your email address and the contents of your email showing up on the screen (and subsequent recording).

03 / 23
Operating context · the targeting climate

Why K12 draws disproportionate fire.

  • Volume

    6,500 users.

    A single district's baseline, sustained. Most are blocked at the door — but at that volume, the ones that slip through are what we triage.

  • Transparency

    K12 is a high-transparency industry by mandate.

    Board minutes, staff directories, calendars, contract awards — all public. Targeting and pretexting OSINT is one search away.

  • Staffing

    Two districts in Tennessee, to my knowledge, have a dedicated full-time cybersecurity FTE.

    Many "departments" are one or two people supporting thousands of accounts and devices. The attacker is full-time. The defender often isn't.

04 / 23
Standing finding

The familiar script: disable, reset, MFA, move on.

That playbook is incomplete. Modern attackers don't rely on stolen passwords for long — they rely on stolen trust. And trust survives password resets.

05 / 23
Amended finding

The foothold is the password.

The foothold is delegated trust.

Refresh tokens, consented apps, mailbox rules, forwarders — none of these care whether the password changed.

06 / 23
Findings to take with you

Five things that change how you triage.

i.

How OAuth apps maintain access after credential resets.

ii.

What refresh tokens and offline_access actually enable.

iii.

Where to look in Entra ID and audit logs for non-interactive persistence.

iv.

How to revoke sessions and consent properly.

v.

How to reduce tenant-wide exposure with consent policies and governance.

07 / 23
Procedure log

Ten steps, in order.

Each step closes a different door. Skipping the middle ones is how attackers stay in.

01

Disable the account.

block sign-in first
02

Revoke sessions.

kills refresh-token reuse
03

Reset the password.

don't forget on-prem AD
04

Audit MFA methods — do not require re-registration yet.

attacker may still be in control
05

Audit OAuth apps & revoke refresh tokens.

the deep dive begins here
06

Delegate mailbox access for forensic investigation.

don't muddy the audit trail
07

Audit inbox rules.

watch for rules named "."
08

Audit forwarding rules.

block external forwarding tenant-wide
09

Audit mail folders (incl. Archive, Drafts, RSS).

attackers hide conversations
10

Audit sign-in logs — esp. non-interactive.

pivot on ASN, user agent
08 / 23
Steps 01 – 04 · initial response

The obvious moves — and the one trap inside them.

Run these four in order. The trap is in Step 04.

01

Block sign-in first so you can do the rest without racing the attacker. If you sync from on-prem AD, disable there too — otherwise the next sync re-enables.

~30 sec
02

Despite the name, "Revoke sessions" also invalidates refresh-token reuse. Without this, a fresh password is irrelevant — the attacker keeps minting access tokens.

Continuous Access Evaluation (CAE) makes this near-real-time for CAE-aware resources (Exchange, SharePoint, Teams). Other apps wait for the access token to expire.

propagation: minutes
03

Reset the password.

Least effective step — but necessary so the attacker can't sign in interactively later.

routine
04

— do not require re-registration yet.

If the attacker added a passwordless method or a Temporary Access Pass, requiring re-registration lets them re-register. Inspect the list. Delete any method that wasn't added by the user. Then unblock.

the trap
09 / 23
Background · setting the term

What's an OAuth app?

A third-party application — registered with Microsoft Entra or Google Workspace — that asks for, and is granted, permission to access data on a user's behalf.

  1. i
    It's not the user. When the user , the app gets its own identity in your tenant — a service principal — distinct from any user account.
  2. ii
    It holds permissions, not a password. The grant is delegated permission to call Microsoft Graph or Exchange Online directly — read mail, read calendar, whatever scopes the user consented to.
  3. iii
    It outlives the sign-in. Once consented, the app keeps working — using refresh tokens — even when the user isn't actively signed in. That's the feature. It's also the persistence vector.
10 / 23
Step 05 · the one that gets missed

OAuth persistence is not a credential problem.

It's a consent problem. The attacker doesn't need the password — they need a delegated permission grant that outlives every step you just took.

  • A malicious or over-permissive app holds a refresh token, not a credential.
  • offline_access means the app keeps minting access even when the user isn't signed in.
  • A tenant-wide consent grant survives the user's entire identity. Even deletion.
  • Revoking sessions kills the token. It does not remove the consent grant — that's a separate action.

If you stop after revoking sessions, you've turned an attacker into a patient one.

11 / 23
Glossary of evidence

What people call "OAuth tokens" — broken into three things.

None of these are revoked by a password change.

  1. i
    Access token Short-lived (~1 hour). The "proof" presented to Exchange Online or Microsoft Graph. Useful for an attacker right now — expires fast.
  2. ii
    Refresh token Mints new access tokens without the user signing in again. This is what keeps Outlook seamless across the day — and what keeps an attacker in long after a reset. Rotation: Microsoft rotates these periodically, shrinking the reuse window — but it doesn't eliminate the threat if the attacker is still riding the lifecycle, or if malicious consent is in place. By default, rotation is time-based.
  3. iii
    Refresh token with offline_access The dangerous variant. Lets an app keep working even when the user isn't actively signed in. Background sync. Long-running API access. Classic persistence.
13 / 23
Step 05 · the fix

Restrict user consent. Tenant-wide.

Three settings, one Entra page. Close the foothold before the next phish lands.

Recommendation · Entra ID → Enterprise applications → Consent & permissions
  1. 01 · Restrict

    Set to "Do not allow user consent" — or, at minimum, "Allow user consent for verified publishers, for selected permissions."

  2. 02 · Route

    Enable the admin-consent request workflow so users have a path to ask for approval — instead of finding one around you.

  3. 03 · Review

    By default, offline_access ships in the low-risk bucket. Reclassify before you allow user consent to anything.

Yes, this generates help-desk tickets when staff want to authorize a new app. Yes, it's worth it. Most K12 tenants don't need users authorizing third-party apps unilaterally.

14 / 23
Step 05 · where to search

Four places to hunt for OAuth abuse.

User-level, tenant-level, the broader behavioral view if you're licensed for it — and one more, coming up next.

filed locations
Microsoft Defender Cloud Apps — OAuth apps view filter: rare + high perms Entra ID Users → user → Applications — user-consented Enterprise applications — tenant-wide → Permissions → Granted by (user | admin)

The best investigative view — if licensed.

Microsoft Defender → Cloud Apps → OAuth apps. The killer filter is "Rare apps with high permissions" — it surfaces the consent grants you've never seen anywhere else in the tenant.

The user-scoped view.

Shows Enterprise Applications the specific user consented to. Not the same as App Registrations a developer created. Match the consent timestamps against your suspected compromise window.

The tenant-wide view — biggest blast radius.

Apps granted admin consent persist beyond any one user. A malicious service principal with tenant-wide permissions is a much larger problem than a single compromised mailbox. Audit Permissions and "Granted by" for every recent change.

The forensic trail.

Filter for Consent to application and Add service principal events. The illicit-consent pattern is a single unusual consent event followed by persistent quiet access.

15 / 23
Tool · attached exhibit

The Step 05 audit, tooled.

OAuth-app auditing is the kind of work that gets skipped because it's tedious. App Hunter is the open-source tool I built so it doesn't have to be.

app-hunter.bardsec.com / apps / risk = HIGH LIVE DEMO
App Hunter — Entra App Auditor app inventory, filtered to high-risk apps
  • What A Flask web app that ingests your tenant's OAuth apps and surfaces what your eyes would miss — RiskLevel.HIGH, unowned apps, expired credentials, high-impact scopes.
  • Why Because the Entra portal makes you click through every app one at a time. App Hunter gives you the whole inventory at a glance — filterable by risk, audience, owner, and permissions.
  • License Open source. Self-hostable. K12-friendly.
16 / 23
Indicators · index of suspicion

Five flags. One earns scrutiny. Two or more is your answer.

  • Flag

    Generic, misleading, or impersonating name — "Microsoft Support Service," "SSO Upgrade Tool."

    Most legitimate publishers don't pick names that sound like internal tooling. Loose naming in your own tenant is what makes this hard — fix that too.

  • Flag

    Consent timestamp aligns with your suspected compromise window.

    Illicit consent grant attacks present as one unusual consent event followed by persistent quiet access. Time-correlate against the sign-in logs.

  • Flag

    High-impact Graph scopes — Mail.ReadWrite, Mail.Send, Files.ReadWrite.All, offline_access.

    High scopes aren't automatically malicious — but during an investigation window, they're strong indicators.

  • Flag

    "Granted by" = the compromised user. Especially with tenant-wide scope.

    A user consented to something with tenant-level reach during a compromise window. That is the primary signal you needed.

  • Flag

    No verified publisher, no documentation, no internal owner.

    If you can't find the person in your district who owns it, treat as malicious until proven otherwise. Most K12 tenants benefit from restricting user consent to verified publishers.

17 / 23
Evidence reel B · pre-recorded

OAuth persistence inside Entra ID.

Watch the consent grant survive a password reset — then watch the proper eviction make it stop.

attached video · oauth persistence walk-through REEL B · EVIDENCE
  • 0:00 Attacker tricks user into consenting to a "Microsoft Support Service" app with Mail.ReadWrite and offline_access.
  • 1:38 Attacker uses the refresh token to read mailbox via Graph from their own infrastructure. No interactive login.
  • 2:25 Defender removes the Enterprise Application. Service principal gone. Access fully evicted.
18 / 23
Step 05 · eviction procedure

Three actions that actually remove OAuth persistence.

Do them all. Skipping any one leaves the door open.

eviction.ps1 — Microsoft Graph PowerShell
# 1. Disable or delete the suspicious Enterprise Application
#    Entra ID → Enterprise Applications → [app] → Properties
#    "Enabled for users to sign in?" → No   (then delete once validated)

# 2. Revoke the compromised user's sessions (also invalidates refresh token reuse)
Revoke-MgUserSignInSession -UserId <user@yourdistrict.org>

# 3. Remove the OAuth grant/consent so it can't be re-used
Get-MgOauth2PermissionGrant -Filter "clientId eq '<servicePrincipalId>'" |
  Remove-MgOauth2PermissionGrant

# Bonus: verify there are no app role assignments left over
Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId <sp-id>

Revoking sessions without removing consent leaves a malicious service principal alive in the tenant. It will be ready the next time someone consents.

19 / 23
Step 05 · portal version

If you'd rather click than type.

The same three actions, every step through the Entra portal — for the days when PowerShell isn't an option.

01

Disable, then delete the Enterprise Application.

Entra ID → Enterprise applications → [the app] → Properties → Enabled for users to sign in? → No. Validate, then return to the app → Delete.

removes service principal + consent grants
02

Revoke the user's sign-in sessions.

Entra ID → Users → [the user] → Revoke sessions.

kills refresh-token reuse
03

Check for leftover OAuth grants.

Entra ID → Enterprise applications → [any app you didn't delete] → Permissions → Review permissions granted to this application.

cleanup pass

Deleting the application is the cleanest single action — it removes the service principal and all its consent grants in one move. Revoking sessions is the only step you'll do from the User blade.

20 / 23
Steps 06 – 10 · trace evidence

Mailbox forensics, after the door is closed.

What an attacker leaves behind is often where you'll learn what they were really after.

What to check

  • Delegated mailbox access for investigationRead & Manage in Exchange Online — clean audit trail. Remove when done.
  • Inbox rules + delegate permissionsalso audit who else has access to this mailbox
  • Forwarding rules (incl. external)EAC → Reports → Mail Flow → Auto-forwarded report
  • Mail folders — Archive, Drafts, Sent, Deletedattackers stash convos in obscure folders
  • Sign-in logs, especially non-interactivenon-interactive = token-based, not browser-based

Patterns to flag

  • Rule named "." (single period)classic obfuscation rule name
  • Forwarding to Gmail / other externalblock at tenant level if you can
  • Conversations in the RSS Subscriptions folderattackers run convos with banks / vendors here
  • Python user agents on non-standard endpointse.g., user trying to hit Azure Admin portal
  • Sign-ins from M246 (ASN 9009) or other rare ASNspivot on ASN to find other affected accounts
21 / 23
Live demo · process integration

Embed the runbook in your process.

A 10-step playbook is only as good as the workflow that forces you to use it. Here's what it looks like when the runbook is wired straight into the helpdesk.

live · supportstudiok12.com / incident-log LIVE · UNCAPTURED
↗ Switching to Safari

SupportStudioK12 · Incident Log

Compromised-account runbook, wired into the ticket. Every action — disable, revoke, reset, hunt, evict — writes its own timeline entry with the operator and the timestamp.

  • Watch for The runbook lives inside the incident — open a ticket, the next-action prompt is right there.
  • Watch for Auto-documentation: each completed step writes its own timeline entry with the operator and the timestamp.
  • Watch for Hand-off ready: another tech can pick up the ticket and immediately see what's been done, by whom, and what's next.
22 / 23
Bonus reel · if time allows

Token theft with AitM.

The phish that survives MFA. Evilginx-style reverse proxy, real Microsoft login, real MFA prompt — token siphoned in the middle.

attached video · evilginx walk-through REEL A · EVIDENCE
  • 0:00 Phish email lands. Link looks legitimate — different TLD, but plausible.
  • 0:25 User clicks. Reverse proxy serves the real Microsoft login page. URL is the attacker's — UI is Microsoft's.
  • 0:55 User enters credentials. MFA prompt appears on their phone. User approves — it is the real login.
  • 1:20 Authentication completes. Attacker now holds the user's session cookie and a fresh refresh token.
  • 2:00 Attacker replays the cookie from a different country. Already in. MFA was satisfied — for them.
23 / 23
Case closed Finding · for the record

Treat identity like infrastructure.

Verify Evict Monitor

Compromise recovery isn't about changing passwords. It's about removing persistence. The goal is not just regaining control — it's ensuring no delegated access, no forwarding path, and no quiet rule is still siphoning data when you walk away.

Appendix · resources & references
Annex A · full playbook

Ten steps, fully written out.

For after the talk — bookmark, copy, share with your incident-response runbook.

  1. Disable the account

    Blocking sign-in is the top priority — it gives you time to do everything else without racing the attacker. Disable the account does not, on its own, evict an attacker who's already established persistence below.

    If you sync identities from on-prem AD, disable the account locally too — otherwise AD's next sync will re-enable it.

  2. Revoke sessions

    "Revoke sessions" in Entra ID does double duty: it kills active sign-in sessions and invalidates refresh-token reuse, forcing reauthentication across apps. Without this, your password reset is theatrical — the attacker keeps minting fresh access tokens.

    Propagation across M365 services can take several minutes.

  3. Change the password

    The least effective single step, but still necessary so the attacker can't sign in interactively later. Remember to disable on-prem AD if you sync identities, otherwise the next sync may re-enable.

  4. Audit MFA methods (and do NOT require re-registration yet)

    Verify each authentication method is one the user owns. Check for newly added passwordless methods or suspicious Temporary Access Pass issuance — these are how attackers regain access immediately after the account is re-enabled.

    If an attacker added an MFA method and you require re-registration, they may be the one who completes it.

  5. Audit app registrations & revoke OAuth refresh tokens

    This is the step that's most often skipped — and the one that turns a recovered account into a still-compromised one. Audit Defender for Cloud Apps (if licensed) for "Rare apps with high permissions." Check Entra ID → Users → [user] → Applications, and Enterprise Applications for tenant-wide consent.

    Disable or delete suspicious apps. Run Revoke-MgUserSignInSession. Remove the underlying consent grant — revoking tokens alone is temporary if the consent remains.

  6. Delegate mailbox access for investigation

    Delegate Read & Manage in Exchange Online so you can investigate without generating misleading "user logged in" entries in the audit logs. While you're there, check existing delegate permissions for anything illegitimate.

    Remove your delegated access when the investigation is complete.

  7. Audit inbox rules

    Inbox rules are persistence-light. A forwarding rule to a Gmail address keeps the attacker reading mail even when their other access is gone. Rules that send certain conversations to obscure folders (RSS Subscriptions is common) are used to hide vendor / bank impersonation threads.

    The classic malicious rule name is a single period: .

  8. Audit forwarding rules

    Block external forwarding tenant-wide if you can. If not, alert when new forwarding is configured (Blumira offers a free SIEM for M365 if you don't already have one). The Exchange Admin Center's Reports → Mail Flow → Auto-forwarded report shows current external forwarding.

  9. Audit mail folders

    Search Archive, Drafts, Sent, Deleted, and unusual subfolders for conversations the user doesn't recognize — particularly anything from vendors, banks, or finance addresses. The attacker often carries on a conversation in parallel using the compromised identity.

  10. Audit sign-in logs

    Pay special attention to non-interactive sign-ins, which usually indicate token-based access rather than a normal user login. Pivot on IP, user agent, app, and ASN. A user agent like Python-requests trying to hit the Azure portal is rarely your user's behavior.

    If an ASN is unusual (e.g., M246 / ASN 9009), look for the same ASN in other users' sign-in logs to find related compromises.

§ Broader cleanup · after the mailbox is secured

  • Verify the user wasn't added to privileged roles or groups.
  • Review Conditional Access and security policy changes for weakening or new exclusions.
  • Ensure the user's primary device is clean (infostealers cause immediate re-compromise).
  • Quarantine or retract malicious emails sent from the account.
  • Check OneDrive / SharePoint for unusual downloads, sharing links, or data staging.
  • Validate connected third-party accounts that rely on the same identity or SSO.
  • Increase monitoring for 1 – 2 weeks and tune alerts based on what you learned.
Annex B · references

Take this with you.

The articles, the docs, and the tools you'll actually reach for the next time a mailbox lights up.

§ Source articles

article · main

Phishing Persistence: 10 Steps to Securing a Compromised M365 Account

The full 10-step checklist with screenshots, written out at length. The basis for most of this talk.

edtechirl.com →
article · deep dive

M365 Compromised Account Triage: OAuth Persistence

The Step 05 deep dive — token types, where to look, what to revoke, why password reset alone is incomplete.

edtechirl.com →

§ Microsoft documentation

docs

Revoke user access in Microsoft Entra ID

The official walkthrough for revoking sessions and refresh tokens — both portal and PowerShell.

learn.microsoft.com →
docs · graph

Revoke-MgUserSignInSession (Graph PowerShell)

The modern replacement for the deprecated AzureAD module. Use this for scripted eviction.

graph powershell reference →
docs · oauth

OAuth app investigations in Defender for Cloud Apps

How to use the Cloud Apps view to surface rare apps with high permissions, plus consent-grant alerting.

defender docs →
docs · consent

Configure user consent settings

How to restrict user consent to verified publishers or require admin approval — the proactive control most K12 tenants want.

configure user consent →

§ Frameworks & community

framework · k12

K12SIX Essentials Series

K12SIX's compromised-account response framework. This deck complements that with the Microsoft 365–specific tactical details.

k12six.org →
tool · free

Blumira — free SIEM for M365

Free tier ingests M365 logs and can alert on new inbox rules and forwarding changes. If you don't have a SIEM, start here.

blumira.com →
tool · open source

App Hunter — Entra OAuth app auditor

The tool from slide 11. Surfaces high-risk OAuth apps, unowned apps, expired credentials, and high-impact Graph scopes from your Entra tenant. Self-hostable.

try the demo → github →

§ Glossary

access token
Short-lived (~1 hr) credential a client presents to Microsoft Graph or Exchange Online. Expires fast — not useful for long-term persistence on its own.
refresh token
Longer-lived token used to mint new access tokens without the user signing in again. The real prize for an attacker.
offline_access
An OAuth scope that allows apps to maintain access even when the user isn't actively signed in. Foundational for legitimate background sync — and for malicious persistence.
service principal
The tenant-local representation of an application. Permissions are bound here. Removing it is what fully evicts a malicious app.
illicit consent grant
An attack pattern where the attacker tricks the user into authorizing a malicious OAuth app rather than stealing credentials directly.
non-interactive sign-in
A sign-in performed by a client using a token (not a browser session). Common indicator of token-based persistence rather than password reuse.
AitM (Adversary-in-the-Middle)
A reverse-proxy phishing technique that intercepts the user's session cookie and token during a real Microsoft login — bypassing MFA.
TAP (Temporary Access Pass)
A short-lived passcode an admin can issue. If an attacker can issue or use one, they may regain access without needing the user's MFA.
Annex C · clarifying questions

The FAQ.

Things that come up after every talk on this — answered up front.

If I revoke sessions, why do I still need to remove the consent?
Revoking sessions invalidates the current refresh-token lineage for the user. It does not delete the OAuth consent record on the service principal. If the same user (or any other compromised user) ever signs in to the app again, fresh tokens will be issued and the persistence resumes. Remove the consent and disable / delete the Enterprise Application to fully close the door.
Why shouldn't I require MFA re-registration immediately?
Because if the attacker still has any path back into the account — including via a passwordless method they added — they may be the one who registers the new MFA method. Disable, audit, clean the methods, then require re-registration when you've confirmed only legitimate methods remain.
Is the AzureAD PowerShell module still okay to use?
No. The AzureAD module is deprecated in favor of Microsoft Graph PowerShell. Use Revoke-MgUserSignInSession, Get-MgOauth2PermissionGrant, and related Graph cmdlets. Microsoft has been clear that AzureAD module won't be maintained.
What if we don't have Defender for Cloud Apps?
You can still do most of this from Entra ID directly: Users → [user] → Applications for user-consented apps, and Enterprise Applications for tenant-wide. Defender for Cloud Apps adds behavioral signals (rare apps, high-permission flags, anomalous consent) that make hunting much faster — but it's not strictly required.
How do I block external forwarding tenant-wide?
Exchange Admin Center → Mail Flow → Remote Domains → "*" → Allow forwarding = No. Also configure an Outbound Anti-Spam policy with "Automatic forwarding rules" set to Off. Both together close the most common forwarding paths.
What's a reasonable "user consent" setting for K12?
For most K12 tenants: restrict user consent to "Verified publishers only" with low-risk permissions, or require admin approval for everything. Yes, you'll get more help-desk tickets. You'll also stop the illicit-consent grant pattern cold.
How long after revoking sessions before the attacker really loses access?
Propagation across Microsoft 365 services can take several minutes — sometimes more for OneDrive / SharePoint. Don't assume the revoke is instant. If the situation is hot, keep the account disabled while everything else stabilizes.
Why do attackers stash conversations in the RSS Subscriptions folder?
Because virtually no one looks there. Once they have an inbox rule that diverts certain conversations there, they can keep entire vendor / bank threads alive while the user sees a completely clean inbox. Audit Archive, Drafts, Sent, Deleted, and unusual subfolders.
Colophon

About this file.

One HTML file. No build step. Press F from anywhere to open the case file.

The deck and the reference material live in the same page. Built on the same single-file chassis as Vibe Code Live, restyled as a case-file dossier for incident response.


type   Newsreader · IBM Plex Mono
palette   manila cream, ink black, evidence red, tobacco yellow
stack   one HTML file, no dependencies, no build step
presentation mode   press F from any section