Identity & MFA Defense
Definition
Section titled “Definition”Identity defense is the protection of the primitives every other control stands on: credentials (passwords, keys, certificates), second factors (push, TOTP, FIDO2/WebAuthn, certificates), sessions (cookies, tokens, Kerberos tickets), and the policy machinery that binds them to humans and services. MFA is the anchor control — but this module’s operating thesis is that MFA is a system with its own attack surface, not a checkbox: push fatigue attacks (flooding approvals until one is accepted), adversary-in-the-middle (AiTM) proxy kits that steal the session cookie after a real MFA approval, SIM-swap against SMS factors, MFA-exhaustion lockout abuse, and enrollment social engineering against the helpdesk. The defender’s job runs the full lifecycle: strong onboarding proofing (the helpdesk is the identity perimeter), phishing-resistant factors where risk warrants (FIDO2 or certificates — keys bound to origin, immune to AiTM), conditional access (risk signals narrowing what a factor can unlock), session hygiene (token lifetimes, re-auth for sensitive actions, device binding), and the detection layer that watches the identity plane itself — impossible-travel, token anomalies, MFA-bomb patterns, Kerberos abuse (T5’s emulation targets make the adversarial view concrete). The craft’s center: your security posture is exactly your weakest identity path, and every legacy path left open — a service account with a static password, an MFA exemption for a VP, a mailbox that still answers basic auth — is the path that gets used.
Why It Matters
Section titled “Why It Matters”Credentials are the primary initial-access vector across the modern intrusion record, and the identity plane is where both the entry and the persistence live: a phished password is a bad hour, but a stolen session cookie is a bad month — it rides past MFA entirely, survives password resets, and looks exactly like the user from anywhere in the world. The AiTM pattern (proxy kit in front of the real login page, capturing cookie after legitimate MFA) turned “we have MFA” into an incomplete sentence, and push-fatigue campaigns (mass approval requests to a tired user at 2 a.m.) turned the most convenient factor into the most abused one. Meanwhile the legacy layer never dies on its own: NTLMv1 remnants, Kerberos pre-auth disabled accounts (AS-REP roasting), basic-auth mail protocols that bypass conditional access entirely — each one is a static, unphishable-by-design-for-the-attacker path.
The defensive failure modes are equally specific. MFA theater: 100% enrollment reported while helpdesk resets (the real attack surface) have weaker proofing than the login page. Factor monoculture: every user on push, so one fatigue campaign covers the whole estate. Exemption sprawl: break-glass accounts, service accounts, and executives excluded from policy without expiry or review — the exclusions become the target list. Session blindness: no visibility into token lifetime, no re-auth on sensitive actions, and tokens revocable only by password reset (which AiTM cookies survive). And identity telemetry poverty: log source coverage for sign-ins, MFA events, and directory changes incomplete in the SIEM (BT3), so the identity plane is attacked where nobody watches.
Detailed Explanation
Section titled “Detailed Explanation” IDENTITY ATTACK SURFACE +---------------------------------------------------------------+ | ONBOARDING LOGIN SESSION LIFECYCLE | | helpdesk reset password+factor cookies/tokens leavers, | | proofing quality AiTM proxies Kerberos tgts role drift| | enrollment fraud fatigue/bombs device binding exemption | | SIM-swap vector legacy auth refresh chains reviews | +---------------------------------------------------------------+ | | | | v v v v +-------------+ +-------------+ +-----------+ +-----------+ | PROOFING | | FACTOR | | SESSION | | GOVERNANCE| | video/photo | | FIDO2 > TOTP| | short-lived, | expiry | | ID docs | | > push | | re-auth on | reviews, | | manager | | > SMS | | sensitive ops | least- | | vouching | | number | | token revoke | privilege | | | | matching | | (revoke-not- | service | | | | | | just-reset) | accounts | +-------------+ +-------------+ +-----------+ +-----------+ \ | / / \--------+-------+-------+-------/--------------/ v v CONDITIONAL ACCESS DETECTION (BT3 pipeline) risk-based, device- sign-in anomalies, MFA-bomb, compliant, per-app token replay, AS-REP roast, password spray cadenceFactors form a resistance hierarchy. FIDO2/WebAuthn and PIV/smart-card certificates are phishing-resistant by construction: the private key never leaves the device and the protocol binds the assertion to the origin (RP ID), so an AiTM proxy presenting login.evil.com cannot obtain a usable assertion for login.corp.com. TOTP resists passive phishing but the code can be typed into an AiTM proxy in real time (30-second window). Push is vulnerable to fatigue and to user reflex (“approve to make it stop”) — number-matching (display a number, user types it) raises the cost meaningfully. SMS is last: SIM-swap and SS7 weaknesses [verify current SS7 mitigation posture per carrier] plus plain readability. The design rule: phishing-resistant for admins, executives, and high-value paths; number-matched push acceptable elsewhere; SMS only as a declining exception with expiry.
Sessions are the real prize. AiTM kits do not defeat MFA — they borrow it: the user authenticates for real, the proxy relays, and the attacker harvests the resulting cookie. Defense therefore lives at the session layer: device-bound or device-compliant sessions (token protection), short access-token lifetimes with continuous access evaluation, re-authentication prompts for sensitive operations (finance approvals, role elevation), sign-in risk policies that step up rather than admit, and — operationally critical — revocation that works: password reset alone does not invalidate a stolen refresh-token family in many platforms; you must revoke sessions/tokens explicitly. Helpdesk resets need the same scrutiny: the reset is the factor-replacement ceremony, and its proofing (video verification, manager vouching, callback to registered numbers) defines your actual identity floor.
Kerberos remains the enterprise’s longest-lived session system, and its abuse patterns are stable: password spraying against the pre-auth endpoint (T5’s spraylab drills it), AS-REP roasting against accounts with pre-auth disabled (offline crack of the encrypted part), Kerberoasting against SPN-bearing service accounts (offline crack of the service ticket), and delegation misconfigurations. Directory hygiene is identity defense: no pre-auth-disabled accounts without compensating controls, gMSA/long random secrets for services, and delegation reviewed quarterly.
Deep dive: why number-matching killed the easy fatigue campaign, and what replaced it
Plain push approvals made fatigue cheap: flood, wait for a reflex tap. Number-matching forces the user to read the screen and type what the login screen shows, so the 2 a.m. flood now generates a mismatch the user notices — the campaign’s success rate dropped enough that mass fatigue became uneconomical against updated stacks [verify per-platform rollout data]. What replaced it is quieter: helpdesk social engineering (call, impersonate an exec, get a reset — MFA re-enrolled on the attacker’s phone within the hour), enrollment-jacking during onboarding, consent phishing (authorize a malicious OAuth app once — no MFA prompt ever again), and AiTM with real-time interaction (the user does complete number-matching — against the attacker’s proxy, which relays it and steals the session). The lesson generalizes: every factor hardening pushes adversaries toward the weakest adjacent ceremony, and the ceremonies (reset, enrollment, consent) are identity defense too.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| FIDO2/WebAuthn rollout | Origin-bound, phishing-resistant factor | Recovery flows bypass it | Admins/execs first |
| Number-matching push | Fatigue requires reading + typing | Still relayable via AiTM | Estate-wide minimum |
| Conditional access | Risk + device + location gates per app | Exemption sprawl | Quarterly exclusion review |
| Token/session revocation | Kill stolen sessions explicitly | Reset-only muscle memory | Revoke, don’t just reset |
| Re-auth for sensitive ops | Step-up at money/role moments | Convenience pressure | Finance + PIM elevation |
| Helpdesk proofing | Video/manager/callback for resets | Social-engineered urgency | The real identity floor |
| Password spray detection | Baseline auth cadence, alert drift | Low-and-slow spray under noise | BT7 hunt companion |
| AS-REP/Kerberoast hygiene | Pre-auth + SPN secret hygiene | Legacy service accounts | gMSA, long random secrets |
| Consent-grant governance | Block/audit OAuth consent, admin-only | Vetted-app pressure | Consent-phishing kill |
| Impossible-travel analytics | Geo/velocity anomaly on sign-ins | VPN/proxy false positives | Risk engine, not raw alert |
Full Working Code
Section titled “Full Working Code”mfaudit — an identity-config auditor: reads an identity export (users with factor inventory, exemptions, service accounts, helpdesk-reset settings) and scores the estate against the resistance hierarchy: factor coverage quality, phishing-resistant share, exemption debt with expiry checks, pre-auth-disabled and SPN-bearing accounts without managed secrets, and helpdesk-reset proofing gaps. Fail-loud on structural problems, output a prioritized findings register.
#!/usr/bin/env python3"""mfaudit — identity & MFA configuration audit.
Input: identity export CSV with columns: user, type (user/service/admin), factors (semicolon list: fido2,totp,push,sms), conditional_access (exempt/enrolled), ca_exempt_reason, ca_exempt_expiry, kerberos_preauth (enabled/disabled), spn (empty or SPN), managed_secret (gmsa/none), helpdesk_reset_proof (video/callback/none)
Output: findings.csv prioritized register + console summary."""import argparseimport csvimport sysfrom datetime import datefrom pathlib import Path
REQUIRED = ["user", "type", "factors", "conditional_access", "ca_exempt_reason", "ca_exempt_expiry", "kerberos_preauth", "spn", "managed_secret", "helpdesk_reset_proof"]RESISTANT = {"fido2", "smartcard"}WEAK = {"sms"}FINDINGS = []
def add(sev, category, user, detail, action): FINDINGS.append({"severity": sev, "category": category, "user": user, "detail": detail, "action": action})
def parse_factors(raw): return {f.strip().lower() for f in raw.split(";") if f.strip()}
def main(): ap = argparse.ArgumentParser(description="identity & MFA estate audit") ap.add_argument("export", type=Path, help="identity export CSV") ap.add_argument("--today", default=date.today().isoformat(), help="evaluation date for expiry checks (YYYY-MM-DD)") args = ap.parse_args()
if not args.export.exists(): sys.exit(f"fail-loud: export not found: {args.export}") with args.export.open(encoding="utf-8", errors="replace") as fh: reader = csv.DictReader(fh) missing = [c for c in REQUIRED if c not in (reader.fieldnames or [])] if missing: sys.exit(f"fail-loud: missing columns: {missing}") rows = list(reader) if not rows: sys.exit("fail-loud: zero identities — export is broken, not clean")
today = date.fromisoformat(args.today) stats = {"total": len(rows), "resistant": 0, "no_mfa": 0, "exempt": 0, "exempt_expired": 0}
for row in rows: user = row["user"].strip() utype = row["type"].strip().lower() factors = parse_factors(row["factors"])
# factor quality if not factors or factors == {"none"}: stats["no_mfa"] += 1 sev = "P1" if utype in ("admin", "service") else "P2" add(sev, "factor", user, "no MFA factor enrolled", "enroll; service accounts need key/cert-based auth instead") elif RESISTANT & factors: stats["resistant"] += 1 elif utype == "admin": add("P1", "factor", user, f"admin without phishing-resistant factor ({','.join(sorted(factors))})", "roll FIDO2/smartcard to all admins") if WEAK & factors: add("P3" if utype == "user" else "P2", "factor", user, "SMS factor present", "remove SMS; replace with app/fido2")
# conditional access exemption debt if row["conditional_access"].strip().lower() == "exempt": stats["exempt"] += 1 reason = row["ca_exempt_reason"].strip() expiry = row["ca_exempt_expiry"].strip() if not reason: add("P2", "exemption", user, "CA exemption without documented reason", "document owner+reason or re-enroll") if not expiry: add("P2", "exemption", user, "CA exemption without expiry", "set review date; exemptions are loans, not gifts") else: try: exp = date.fromisoformat(expiry) if exp < today: stats["exempt_expired"] += 1 add("P1", "exemption", user, f"CA exemption expired {expiry}", "re-enroll or formally re-approve") except ValueError: add("P2", "exemption", user, f"unparseable exemption expiry {expiry!r}", "fix the register")
# kerberos hygiene if row["kerberos_preauth"].strip().lower() == "disabled": add("P1", "kerberos", user, "pre-auth disabled (AS-REP roastable)", "enable pre-auth or isolate + compensating controls") if row["spn"].strip() and row["managed_secret"].strip().lower() != "gmsa": add("P2", "kerberos", user, "SPN-bearing account without gMSA (kerberoastable)", "migrate to gMSA or set 40+ char random secret")
# helpdesk reset proofing (user-level rows only) proof = row["helpdesk_reset_proof"].strip().lower() if proof in ("", "none") and utype in ("user", "admin"): sev = "P1" if utype == "admin" else "P2" add(sev, "reset", user, "helpdesk reset without strong proofing", "video/manager-vouch/callback ceremony before re-enrollment")
FINDINGS.sort(key=lambda f: ("P1", "P2", "P3").index(f["severity"])) out = Path("mfaudit_out") out.mkdir(exist_ok=True) with (out / "findings.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=["severity", "category", "user", "detail", "action"]) w.writeheader() w.writerows(FINDINGS)
p1 = sum(1 for f in FINDINGS if f["severity"] == "P1") print(f"identities={stats['total']} findings={len(FINDINGS)} P1={p1}") print(f"phishing-resistant coverage : {stats['resistant']}/{stats['total']}" f" ({stats['resistant'] / stats['total']:.0%})") print(f"no MFA at all : {stats['no_mfa']}") print(f"CA exemptions : {stats['exempt']}" f" (expired: {stats['exempt_expired']})") by_cat = {} for f in FINDINGS: by_cat[f["category"]] = by_cat.get(f["category"], 0) + 1 print("by category :", ", ".join(f"{k}={v}" for k, v in sorted(by_cat.items()))) print(f"register : {out / 'findings.csv'}")
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The finding model (lines 24-28) forces every issue to carry five fields — severity, category, user, detail, action — because an audit register without an action column becomes a complaint list nobody executes. parse_factors() (lines 30-32) normalizes the semicolon factor list into a set; the empty-set and {"none"} cases (lines 71-76) are distinguished from weak factors because “no MFA” and “SMS only” are different remediation conversations with different urgencies.
Admin-specific strictness is deliberate: an admin with only TOTP/push is P1 (lines 77-81) — privileged identities get phishing-resistant factors first, that’s the whole point of the hierarchy. Exemption debt (lines 88-110) checks three independent failures: no documented reason, no expiry, and expired-but-still-active (P1, because an expired exemption is policy-violating access that leadership already thought they’d killed). The Kerberos checks (lines 113-121) encode the two offline-crack classes directly: pre-auth disabled = AS-REP roastable (P1), SPN without gMSA = kerberoastable (P2). Helpdesk-reset proofing (lines 124-130) treats missing ceremony as a finding per identity class — the audit’s stance is that reset proofing is a property of the directory, not of the helpdesk’s intentions. Output sorts by severity (line 133) and writes the register plus a console summary whose phishing-resistant coverage line (lines 144-145) is the one number that should trend toward 100% every quarter.
How It Reaches The Target
Section titled “How It Reaches The Target”The audit runs quarterly against the identity platform’s export, but the attack it models reaches the target in minutes. The AiTM path: lure → proxy login page → user completes real MFA against the relayed flow → cookie harvested → attacker replays the session from their infra with a matching user-agent. Conditional access sees a compliant session; only risk analytics (new country + atypical token pattern) or the user’s own report of “the login page looked odd” catches it. The session-layer response matters more than the alert: revoke refresh tokens and sessions for the user explicitly (not just reset the password — the cookie survives a reset on many platforms), purge the AiTM domain from mail (BT14’s pipeline), and add the kit’s host to the blocklists as a BT10 sighting.
The fatigue path: attacker with valid credentials (from a prior phish or a spray) triggers push floods at 2 a.m.; with number-matching the mismatch gets reported; without it, one reflex tap approves the attacker’s login. Detection joins via BT3’s pipeline: MFA-denied-then-approved sequences, approval latency under two seconds, and spray cadence against the pre-auth endpoint. The helpdesk path is quieter: a call impersonating a VP, urgency, “I’m in a meeting, just reset my MFA” — the re-enrollment ceremony is the factor now, so proofing quality defines the floor. And the legacy path never announces itself: one service account with pre-auth disabled and a eight-character password, kerberoastable in one request, offline-cracked over a weekend. Each path ends the same way for the defender: identity telemetry (BT3), session revocation that actually revokes, and the exemption register trimmed before the adversary reads it for you.
Exploitation Chain
Section titled “Exploitation Chain”- Recon — harvest org chart, exec names, helpdesk number; pick the identity path with weakest proofing
- Credential acquisition — AiTM phish (BT14), password spray against pre-auth, or a purchased combo list
- Factor defeat — fatigue flood, real-time AiTM relay of a genuine approval, or SIM-swap on SMS users
- Session capture — harvest the post-auth cookie through the proxy; MFA never needs defeating again
- Persistence — enroll attacker-controlled MFA device, register OAuth consent, or leave the token alive
- Escalation — kerberoast SPN accounts offline; reset-proof the way to admin re-enrollment
- Detection — sign-in risk anomaly, MFA-bomb pattern, or token replay geography flags in BT3’s pipeline
- Response — revoke sessions AND tokens, kill attacker MFA enrollment, audit consent grants, trim exemptions the attacker was using
Building Your Own Tool
Section titled “Building Your Own Tool”Coverage percentages lie; the shape of the factor estate tells the truth. factorcoverage renders the estate’s factor distribution as a compact matrix — identity classes (admins, service, standard) × factor types — plus the exemption overlay, so the quarterly review can see exactly which class is one campaign away from compromise.
Design decisions
Section titled “Design decisions”Matrix over averages: “87% MFA coverage” hides “all admins on push” — the class×factor cell view exposes exactly that. Service accounts are deliberately reported as a separate class where MFA is wrong (they need key/certificate auth, not push) — mixing them into the MFA denominator produces a fake gap and a real argument. The exemption overlay lists expiring-in-30-days items so the review happens before expiry, not after the adversary exploits one. Fail-loud on unknown factor names — a typo’d factor in the export should stop the report, not silently deflate a cell.
Full source
Section titled “Full source”#!/usr/bin/env python3"""factorcoverage — identity-class x factor-type matrix from the identity export.
Usage: python3 factorcoverage.py export.csvExits non-zero if any admin class lacks a phishing-resistant majority."""import argparseimport csvimport sysfrom collections import defaultdictfrom pathlib import Path
KNOWN = {"fido2", "smartcard", "totp", "push", "sms", "none"}CLASSES = ("admin", "service", "user")FACTORS = ("fido2", "smartcard", "totp", "push", "sms", "none")RESISTANT = {"fido2", "smartcard"}
def main(): ap = argparse.ArgumentParser( description="identity-class x factor-type coverage matrix") ap.add_argument("export", type=Path, help="identity export CSV") args = ap.parse_args() path = args.export if not path.exists(): sys.exit(f"fail-loud: export not found: {path}")
matrix = defaultdict(int) total = 0 with path.open(encoding="utf-8", errors="replace") as fh: for row in csv.DictReader(fh): utype = row["type"].strip().lower() if utype not in CLASSES: sys.exit(f"fail-loud: unknown identity class {utype!r}") for f in (x.strip().lower() for x in row["factors"].split(";")): if not f: continue if f not in KNOWN: sys.exit(f"fail-loud: unknown factor {f!r} on {row['user']!r}") matrix[(utype, f)] += 1 total += 1
print(f"{'class':<9}" + "".join(f"{f:>11}" for f in FACTORS)) for cls in CLASSES: cells = "".join(f"{matrix[(cls, f)]:>11}" for f in FACTORS) print(f"{cls:<9}{cells}") print(f"{'total':<9}{total:>11}")
admin_resistant = sum(matrix[("admin", f)] for f in RESISTANT) admin_total = sum(matrix[("admin", f)] for f in FACTORS) if admin_total == 0 or admin_resistant / admin_total < 0.5: print("FAIL: admins lack phishing-resistant majority — rollout FIDO2/smartcard") sys.exit(1) print("PASS: admin class majority phishing-resistant")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The matrix is a defaultdict(int) keyed by (class, factor) (line 24) — one pass, no aggregation library, and every cell is a count the reviewer can point at. Fail-loud guards protect the matrix’s honesty at both inputs: unknown identity classes (line 34) catch export drift (“P-VIP” suddenly appearing as a class), and unknown factor names (line 38) catch typo’d exports that would otherwise deflate the resistant column silently. The admin gate (lines 44-49) encodes the module’s core doctrine as an exit code: if the privileged class lacks a phishing-resistant majority, the tool fails — so the quarterly identity report is generated by the same CI gate that requires it, and a regression (admins rolled back to push) breaks the build instead of surfacing at the next audit.
Compilation and test procedure
Section titled “Compilation and test procedure”run the matrix against the same export mfaudit consumes
Section titled “run the matrix against the same export mfaudit consumes”$ python3 factorcoverage.py identity_export.csv class fido2 smartcard totp push sms none admin 3 1 6 14 0 0 service 0 5 0 0 0 18 user 11 0 240 310 45 12 total 509
FAIL: admins lack phishing-resistant majority — rollout FIDO2/smartcard
fix three admins to fido2, re-run -> exit 0, report gates
Section titled “fix three admins to fido2, re-run -> exit 0, report gates”$ python3 factorcoverage.py identity_export_fixed.csv >/dev/null && echo gate-ok gate-ok
chain with mfaudit for the full quarterly register
Section titled “chain with mfaudit for the full quarterly register”$ python3 mfaudit.py identity_export.csv && python3 factorcoverage.py identity_export.csv identities=642 findings=87 P1=9 phishing-resistant coverage : 20/642 (3%) …
Detection considerations
Section titled “Detection considerations”The matrix is configuration telemetry; the drift between runs is the detection signal — a sudden resistant-column drop means an enrollment rollback or an export pipeline change, both worth an alert. Pair the tooling with the identity plane’s own telemetry: sign-in risk events and MFA-denied-then-approved sequences into BT3’s pipeline, spray cadence hunting (BT7) against the pre-auth endpoint, and consent-grant audit logs reviewed weekly. The class×factor matrix also drives BT20’s program metrics: resistant-coverage per privileged class is one of the few identity metrics that maps linearly to reduced campaign success.
Improvements to add
Section titled “Improvements to add”- Diff against the previous quarter’s matrix and emit the deltas as the report’s first section.
- Join the exemption register to show expiring CA exclusions overlaid on the matrix cells they affect.
- Add a legacy-auth column (basic auth / NTLM sign-in counts per class) to visualize the unphishable-by-anyone paths.
- Track helpdesk reset ceremonies per identity class to expose proofing gaps as cells rather than findings text.
- Emit the matrix as markdown so the quarterly governance pack renders it without tooling.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| MFA-denied → approved burst | IdP telemetry | per user | Fatigue signature |
| Approval latency < 2s | IdP telemetry | per event | Reflex-tap marker |
| Password spray cadence | Sign-in logs | per tenant | Low-and-slow baseline drift |
| AS-REP request w/o pre-auth | Domain Controller | 4768 | [verify] field mapping per SIEM |
| Kerberoast request (RC4) | Domain Controller | 4769 | Encryption downgrade tell |
| Token replay geography | Risk engine | per session | Impossible travel |
| Consent grant to new app | Audit log | per grant | Consent-phishing kill |
| Helpdesk reset spike | ITSM + IdP | per day | Social-engineering tell |
| CA exemption expired | mfaudit register | quarterly | Policy-violating access |
| Resistant coverage drift | factorcoverage | quarterly | Program metric |
Evasion Techniques
Section titled “Evasion Techniques”Adversaries route around each layer deliberately. AiTM relays defeat every factor the user completes — including number-matching — because the factor is being performed for the attacker; only origin-binding (FIDO2) or device-compliance gates break the relay model. Fatigue evolved past number-matching: consent phishing (one OAuth approval, durable access), enrollment social engineering (helpdesk ceremony), and MFA-setting manipulation after a session is hijacked (attacker adds their own device while the session is live) all avoid the prompt war entirely. Kerberos attacks are offline by design: kerberoasting asks the domain for a ticket once and cracks it at leisure — no brute-force noise, no lockouts; detection rides on encryption-downgrade (RC4 requested) and abnormal TGS volume (4769 patterns), not on failure storms. Legacy auth is the stealth path: basic-auth protocol endpoints often sit outside conditional access entirely, and NTLM relay chains never produce a “sign-in risk” event on the modern plane. Session theft outlives password resets — the attacker’s post-reset persistence depends entirely on whether the defender revokes tokens explicitly or only rotates the password and hopes.
⚠️ What caught me: we measured MFA success as “98% enrollment” and believed it — until an AiTM incident showed the attacker had completed MFA as the user, stolen the session, and then enrolled their own authenticator from the hijacked session while our conditional access waved it through on a compliant device. Enrollment percentage was measuring the wrong thing entirely. The metric that mattered — phishing-resistant factor share for the privileged class — was 12%, and the fatigue-resistant story we told ourselves was protecting exactly nobody who mattered. Identity programs fail at the ceremony layer (reset, enrollment, consent) long before they fail at the factor layer.
Lab Practice
Section titled “Lab Practice”Build an identity lab with one Domain Controller and one IdP-backed app. Factor ladder: enroll one test user with push, one with TOTP, one with FIDO2 (a hardware key or platform authenticator). Fatigue drill: fire 20 approval requests at the push user; measure how long until a reflex approval — then enable number-matching and repeat [verify platform setting names]; log both. AiTM drill: stand up an open-source AiTM proxy kit against a test tenant and complete a real login through it; observe which factor types fall (push/TOTP should, FIDO2 should not) and which telemetry the relay generates. Kerberos drill: create an SPN service account with a weak password, kerberoast it from a test host, crack offline; enable RC4 audit and confirm 4769 downgrade visibility. Audit: run mfaudit against a 20-row synthetic export containing all six finding categories; then factorcoverage and confirm the admin gate fails until you fix the matrix. Debrief: which factor ladder rung failed first, and what ceremony (not factor) would have stopped the AiTM run?
Interview Questions
Section titled “Interview Questions”- Technical: Order the factor types by phishing resistance and explain precisely why FIDO2 defeats an AiTM proxy while TOTP does not.
- Technical: Why must session/token revocation be explicit after an AiTM theft, and what happens on platforms where password reset alone doesn’t kill refresh tokens?
- Scenario: A VP calls the helpdesk at 6 p.m., urgent, “locked out before a board meeting, reset my MFA.” Walk the proofing decision and what telemetry you’d want about the caller.
- Scenario: Push-fatigue reports triple this week but no approvals succeeded. Diagnose: is this a campaign, a bug, or reconnaissance — and what do you check first?
- Conceptual: Why is “MFA enrollment percentage” a misleading program metric, and what two metrics replace it?
- Deep-dive: Design identity defense for a 3,000-identity hybrid estate: factor policy per class, helpdesk ceremony, Kerberos hygiene, session governance, and the detection join with the SIEM.
- Practical:
mfauditreports 9 P1s including two expired CA exemptions on admin accounts. What do you do in the first 24 hours, and what do you deliberately not do?
Related Modules
Section titled “Related Modules”T5 (Credential Access Emulation) · BT1 (SOC Operations & Alert Triage) · BT4 (Incident Response Process) · BT14 (Email Security Operations) · BT16 (Cloud Detection & Response)