Sooua
登录
返回文章列表
身份安全··14 分钟阅读

用户在微软官网完成 MFA,却把令牌交给了攻击者:Entra ID 设备码钓鱼的检测与治理

设备码钓鱼把 OAuth 2.0 合法授权流变成绕过 MFA 的令牌窃取通道。本文先复核 RFC 8628 的安全假设,再用公开端点实测证明协议本身没漏洞,然后给出 Entra ID SignInLogs 检测信号、误报来源与 Conditional Access 治理路径。

用户在微软官网完成 MFA,却把令牌交给了攻击者:Entra ID 设备码钓鱼的检测与治理

1. 核心观点

设备码钓鱼(Device Code Phishing)不是“伪造微软登录页”,而是把 RFC 8628 描述的 OAuth 2.0 设备授权流(Device Authorization Grant)反过来用:攻击者扮演“受限设备/客户端”,受害者扮演“显示设备”。受害者在真正的 microsoft.com/devicelogin 输入一段由攻击者侧生成的 user code,浏览器里走完密码 + MFA,平台便把 access token 与 refresh token 推给了攻击者的轮询脚本。整个过程没有伪造域名、没有恶意附件、没有凭据被盗——MFA 实际上为别人的会话作了认证。

由此引出两个常被混淆的工程判断:

  1. MFA 验证的是“用户身份”,不是“授权对象”。 在 device code flow 里,授权对象(client_id 背后的应用)对用户几乎不可见。
  2. 拦不拦得住,主要不在邮件网关,而在 Conditional Access 的 “Authentication flows” 条件与 SignInLogs 的 authenticationProtocol 字段。 也就是要把治理点显式压到 Entra ID 控制面上,而不是继续在终端和邮件层堆策略。

本文沿这条主线,先复核协议本身,再用一次公开端点的实测记录证明“协议本身没漏洞、是信任假设被滥用”,然后给出可在 SOC 落地的检测信号、误报来源和条件访问治理路径。

2. 协议复核:RFC 8628 的三个被滥用的安全假设

RFC 8628 的最初动机是给智能电视、CLI、IoT 这类“弱输入设备”一个授权路径。规范流程如下:

协议本身合法、规范、字段清晰。它依赖三个隐含安全假设,攻击场景把它们逐一击破:

隐含假设攻击场景中如何破裂真实可观察的现象
是“用户主动”从受限设备发起授权攻击者远程发起 /devicecode,user_code 通过钓鱼邮件 / Teams 邀请 / Signal 私聊投给用户SignInLogs 出现一次合法的 deviceCode 登录,但用户当时并未操作任何受限设备
用户能“看到并确认”被授权的应用微软同意屏只显示应用名称;攻击者可注册显眼但无害的名字,或干脆走 first-party app_id用户认为自己只是“在微软官网点了一下确认”,对“授权对象”没有概念
“轮询的设备”可信轮询的是攻击者服务器,平台不知道用户和轮询端在物理上是两个实体token 颁发给攻击者后,受害者本地没有任何痕迹

被滥用的不是漏洞,而是这三条假设。这一点决定了所有“补丁修不了,配置和检测能管”。

3. 攻击链复盘:从 Teams 邀请到 PRT 持久化

把 Microsoft MSTIC、Volexity、Proofpoint 公开的多份报告交叉对齐,Storm-2372 / UNK_AcademicFlare 这一类活动的链路可以画成这样。它有意把社会工程、合法 OAuth 端点和 Entra ID 控制面分开:

几个值得单独说的工程点:

  • client_id 选择决定能拿到什么。 普通 first-party app id 只能拿 access/refresh token;用 Microsoft Authentication Broker 29d9ed98-a469-4536-ade2-f981bc1d605e 配合 device code,攻击者可以兑换 Primary Refresh Token (PRT),并“把自己注册成你租户里的一台设备”——这是 MSTIC 在 Storm-2372 报告里明确点出的升级路径,也是为什么仅仅“强制改密码”往往不够。
  • 拿到 token 之后的横向,全部走合法 Microsoft Graph API。 Microsoft 报告里给出过攻击者用关键字 password / admin / TeamViewer / AnyDesk / credentials / secret / ministry / government 在受害者收件箱里搜索的细节。这些行为本身字段合法、IP 来自微软 DC,靠传统“可疑外联”模型完全打不到,必须依赖语义关键字、突发 Graph 调用基线偏离。
  • 整条链没有恶意附件、没有伪造域名、没有终端落地物。 邮件安全网关、EDR 不是这条链的最佳拦截点;治理重心要前移到 Conditional Access,事中检测重心要落到 SignInLogs 的 authenticationProtocol = deviceCode 字段上。

4. 自验证:一次最小化的公开端点实测

为了让“协议本身没漏洞、被滥用的是假设”不停留在口头判断,我对微软自家的公开端点做了一次最小化复核:只用微软公开的 first-party client_id,不针对任何真实租户、不投递任何用户、不构造任何钓鱼载体,仅验证 RFC 8628 在 Entra ID 上的可观察行为。这一节给出实际命令与响应,作为后文检测信号的协议依据。

测试时间:2026-06-12 21:01–21:03 UTC。

4.1 用 Azure CLI 的公开 client_id 请求 device code

curl -sS -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46" \
  --data-urlencode "scope=https://graph.microsoft.com/.default"

实际响应(关键字段,已脱去 device_code 大部分内容):

{
  "user_code": "HD47NYBCH",
  "device_code": "HBgABIQEAAAAdDD7nC9b5Q7JPd_okEQRF...<截断>...",
  "verification_uri": "https://login.microsoft.com/device",
  "expires_in": 900,
  "interval": 5,
  "message": "To sign in, use a web browser to open the page https://login.microsoft.com/device and enter the code HD47NYBCH to authenticate."
}

可以直接对照 RFC 8628 §3.2 的字段定义:900 秒有效期、5 秒最小轮询间隔、面向用户的 verification_uri 是微软自家域名。没有任何字段表明“这台设备是不是你”,平台只关心 user 拿着合法 user_code 出现在了授权页面。

4.2 换成 Microsoft Authentication Broker 的 client_id

curl -sS -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=29d9ed98-a469-4536-ade2-f981bc1d605e" \
  --data-urlencode "scope=https://graph.microsoft.com/.default"

返回字段同上,能成功拿到 user_codedevice_code。结合 Elastic Security 已经在 prebuilt rule 里硬编码这个 app_id 的事实,可以看出:“能不能用 Broker app_id 走 device code”这件事本身就是检测线索,而不需要复杂行为模型。

4.3 拿一个未授权的 device_code 去 /token 轮询

curl -sS -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
  --data-urlencode "client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46" \
  --data-urlencode "device_code=$DC"

两次连续调用都返回:

{
  "error": "authorization_pending",
  "error_description": "AADSTS70016: The provided request has not yet been authorized by the user. ...",
  "error_codes": [70016]
}

没有触发 slow_down。意思是:只要轮询间隔 ≥ 5 秒,攻击者完全可以稳定地等到 user_code 被人工输入;这正是钓鱼脚本敢挂 900 秒不动的根据。

4.4 实测边界说明

  • 仅用微软公开 client_id;不向真实用户投递任何 user_code;不在任何受害者账号上完成授权;没有触碰目标租户的 SignInLogs。
  • 不能由此外推“某租户上一定能成功 PRT 升级”——后者依赖 Broker app_id + 用户在 Authentication Broker 同意屏点了授权 + 该租户未通过 Conditional Access 阻断 device code flow 三个条件叠加。
  • 测试目的只有一个:把后文“按 authenticationProtocol = deviceCode 检测”的字段路径与 RFC 8628 行为对上号。

5. 检测信号:SignInLogs 上能看到什么

设备码钓鱼会在 Entra ID SignInLogs 留下一组很有判别力的字段。综合 Splunk Security Content、Elastic 的 prebuilt rule、Cloudbrothers 的 hunting 模板和官方文档,可以把信号分成三层。

下面给出三条可以直接落到 Azure Log Analytics / Microsoft Sentinel 的 KQL 雏形。它们不是“开箱即用规则”,而是最小可读检测模板——真正生产化需要按租户白名单、IoT 设备、地理位置基线再收敛。

5.1 检测所有 device code flow 成功登录

SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| where AuthenticationProtocol == "deviceCode"
| project TimeGenerated, UserPrincipalName, AppDisplayName, AppId,
          ResourceDisplayName, IPAddress, Location, DeviceDetail, ConditionalAccessStatus

含义:先把所有成功的 device code 登录摆到桌面上,再人工/数据驱动地找异常。Cloudbrothers 给的初版 hunting 就是这种“先全量再收敛”的思路。

5.2 高保真:Authentication Broker + 交互式 + Office 资源

对应 Elastic 规则的核心条件,命中率高、误报很低:

SigninLogs
| where ResultType == 0
| where AuthenticationProtocol == "deviceCode"
| where AppId == "29d9ed98-a469-4536-ade2-f981bc1d605e"   // Microsoft Authentication Broker
| where ResourceId in (
    "00000002-0000-0ff1-ce00-000000000000",  // Exchange Online
    "00000003-0000-0ff1-ce00-000000000000",  // Microsoft Graph
    "00000005-0000-0ff1-ce00-000000000000"   // SharePoint Online
  )
| where IsInteractive == true

按 MITRE 映射,这条规则对应 T1566.002(Spearphishing Link)+ T1078.004(Valid Accounts: Cloud Accounts)+ T1550.001(Application Access Token),可直接放进 Sentinel 分析规则。

5.3 后续动作:设备注册突然出现 + 关键字邮件搜索

AuditLogs
| where TimeGenerated > ago(24h)
| where Category == "Device" and OperationName == "Add device"
| join kind=inner (
    SigninLogs
    | where AuthenticationProtocol == "deviceCode" and ResultType == 0
  ) on $left.InitiatedBy.user.userPrincipalName == $right.UserPrincipalName
| project TimeGenerated, UserPrincipalName, OperationName, IPAddress, AppDisplayName

如果同一用户先有一次成功 device code 登录,紧接着 AuditLogs 里出现 Add device,这通常就是 PRT 升级落地的痕迹——属于 Storm-2372 的“注册攻击者控制设备”那一步。

6. 误报来源与白名单收敛

device code flow 是合法功能,不是只有攻击者会用。把规则简单粗暴地变成告警,会被运维淹没。常见合法来源至少包括:

合法场景典型表现收敛思路
Azure CLI / az loginclient_id = 04b07795-8ddb-461a-bbee-02f9e1bf7b46;常见于工程师机器按用户组(IT/SRE)白名单;只在“非工程组”账号触发告警
kubectl 等 OIDC CLIclient_id = 04b07795-... 或 GitHub/CICD 的客户端同上
智能屏/会议设备登录 M365来自固定办公网段;ResourceDisplayName 多为 Teams Rooms按设备群 + 网段白名单
Microsoft 365 Apps 头次登录在某些受限输入场景极少;多在 VDI/IoT按 OS/设备类型字段过滤
红队 / 攻防演练与攻击者完全重合;只能靠业务侧报备区分按时间窗口排除已报备的演练

收敛原则可以总结成三句:

  1. 默认不告警,先“记录所有 device code 成功登录”,建立每个租户自己的基线。
  2. 告警从“离群人 + Authentication Broker app_id + Office 资源”开始;这一组合在大多数企业基本只有攻击者会触发。
  3. 凡是白名单需要长期保留的合法用例(IoT、Teams Room、特定运维),都要有人/工单挂钩,不能“随手开个 always allow”。

7. Conditional Access 治理:把 device code 默认关掉

事中检测再准,也不如事前把这个授权流默认关闭。Entra ID 在 Conditional Access 里新增的 “Authentication flows” 条件就是给这件事用的(Microsoft Learn 文档 concept-authentication-flows)。

关键工程实践(参考 Office365 IT Pros 的实操指南并对照 Microsoft Learn):

  1. 永远先排除“break-glass”紧急访问账号,避免策略本身把自己锁在门外。

  2. 第一次部署用 Report-only 模式,让 SignInLogs 出现 ConditionalAccessStatus == "reportOnlyFailure",再用 KQL 反推谁会被影响:

    SigninLogs
    | where

AuthenticationProtocol == "deviceCode" | where ConditionalAccessStatus == "reportOnlyFailure" | summarize count() by UserPrincipalName, AppDisplayName


把名单送去业务方一一确认,再决定是否纳入白名单。

3. **白名单越窄越好**:业务上真有 device code 需求的,通常是 IoT 大屏、Teams Rooms、少数 CLI 工具。可以另开一条“仅这些用户组允许 device code”的反向策略。
4. **配套打开 Token Protection / 缩短 token 寿命 / 启用 CAE**:让攻击者即使拿到 token,能用的窗口也尽量短。这条本文不展开,单独成文。

## 8. 应急响应:怀疑被命中后的最小动作集

如果检测规则真的命中或租户里出现可疑的 device code 登录,最有效的“先止血”动作不复杂,但顺序很重要:

```mermaid
flowchart TB
 A[确认告警:<br/>SigninLog deviceCode 成功登录] --> B[隔离: 禁用账号<br/>Disable user in Entra ID]
 B --> C[撤销所有 refresh token<br/>Graph: revokeSignInSessions]
 C --> D[审计该用户的<br/>已注册设备列表]
 D --> E{发现可疑设备?}
 E -- 是 --> F[Remove device + 重置 PRT]
 E -- 否 --> G[继续]
 G --> H[审计 OAuth 同意:<br/>Get-MgUserOauth2PermissionGrant]
 H --> I[撤销可疑应用同意]
 I --> J[审计 Graph 调用历史:<br/>关键字 password/admin/credentials]
 J --> K[决定:<br/>密码重置 / 邮件转发规则审计 / 通知合作伙伴]

几条容易被遗漏的细节:

  • revokeSignInSessions 只撤会话/refresh token,不撤已被注册的设备。如果攻击者已经走完 PRT 升级,那条设备记录必须显式 Remove-MgDevice 才能根除。
  • Outlook 收件箱规则要单独审计:很多攻击者拿到 token 之后第一件事,是设一条“包含 password 的邮件自动转发到外部地址”。这一类规则是设备码钓鱼最常见的“事后留痕”。
  • 不要只重置密码就报安全事件结案:MSTIC 在 Storm-2372 报告里强调 refresh token 与已注册设备在密码重置后仍然有效——只重置密码是被动止损,不是根因消除。

9. 给身份安全治理的几条具体判断

不要把这件事当成“又一种钓鱼”塞进员工培训里就完。它真正在重塑几条工程判断:

  1. OAuth 流程本身就是身份安全的攻击面。 把治理只压在“密码 + MFA”上,已经追不上现在的攻击者;要把 authenticationProtocol 字段直接纳入 SOC 数据模型。
  2. Conditional Access 的“authentication flows”条件是默认应该 Block 的。 它和 NetworkPolicy 在网络层一样,是少数能在控制面把一类高风险流量直接关掉的开关;这种开关默认开放就是失职。
  3. Token 生命周期不能交给应用自治。 CAE、Token Protection、shorter access token lifetime 这三项需要按租户硬性纳入身份基线。
  4. 检测规则要写到字段一级,不能停留在描述。 本文给的 appId = 29d9ed98-... + authenticationProtocol = deviceCode + resourceId ∈ Office 三元组,是当前公开情报里命中率最稳定的组合,应该当成最小检测基线。
  5. 应急响应必须包括“设备/PRT 复核”,不只是密码和会话。这是被很多“事件处置清单”遗忘的一步。

结论

Entra ID 设备码钓鱼的治理不是『关掉 device code flow』那么简单。业务确需的 IoT、会议室设备和特定 CLI 用户需要白名单,检测规则需要覆盖 Broker app_id 和 Office resource 的高保真模式,响应 runbook 需要包含会话吊销、设备清理和同意审计。检测、治理、响应三者缺一不可。

11. 边界与后续

  • 本文只覆盖 device code flow 这一种 OAuth 滥用,未展开 ROPC、OAuth Consent Phishing、Authentication Transfer 滥用等同类问题;这些将在身份安全系列里单独成文。
  • 自验证只覆盖了 Microsoft 公开端点的协议行为,不涉及任何真实租户的策略效果,不能由此外推具体租户上的命中率。
  • KQL 模板偏向最小可读,没有展开按时间窗口、地理位置、ASN 做聚合的工程化版本,生产化时必须按租户基线再调。
  • Token Protection / Continuous Access Evaluation / Token Binding 是与本文直接相关、但跨度更大的能力面,会作为后续文章单独覆盖。
分享

评论

登录 后参与讨论。

加载中…

相关文章