1. 核心观点
设备码钓鱼(Device Code Phishing)不是“伪造微软登录页”,而是把 RFC 8628 描述的 OAuth 2.0 设备授权流(Device Authorization Grant)反过来用:攻击者扮演“受限设备/客户端”,受害者扮演“显示设备”。受害者在真正的 microsoft.com/devicelogin 输入一段由攻击者侧生成的 user code,浏览器里走完密码 + MFA,平台便把 access token 与 refresh token 推给了攻击者的轮询脚本。整个过程没有伪造域名、没有恶意附件、没有凭据被盗——MFA 实际上为别人的会话作了认证。
由此引出两个常被混淆的工程判断:
- MFA 验证的是“用户身份”,不是“授权对象”。 在 device code flow 里,授权对象(client_id 背后的应用)对用户几乎不可见。
- 拦不拦得住,主要不在邮件网关,而在 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_code 和 device_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 login | client_id = 04b07795-8ddb-461a-bbee-02f9e1bf7b46;常见于工程师机器 | 按用户组(IT/SRE)白名单;只在“非工程组”账号触发告警 |
kubectl 等 OIDC CLI | client_id = 04b07795-... 或 GitHub/CICD 的客户端 | 同上 |
| 智能屏/会议设备登录 M365 | 来自固定办公网段;ResourceDisplayName 多为 Teams Rooms | 按设备群 + 网段白名单 |
| Microsoft 365 Apps 头次登录在某些受限输入场景 | 极少;多在 VDI/IoT | 按 OS/设备类型字段过滤 |
| 红队 / 攻防演练 | 与攻击者完全重合;只能靠业务侧报备区分 | 按时间窗口排除已报备的演练 |
收敛原则可以总结成三句:
- 默认不告警,先“记录所有 device code 成功登录”,建立每个租户自己的基线。
- 告警从“离群人 + Authentication Broker app_id + Office 资源”开始;这一组合在大多数企业基本只有攻击者会触发。
- 凡是白名单需要长期保留的合法用例(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):
-
永远先排除“break-glass”紧急访问账号,避免策略本身把自己锁在门外。
-
第一次部署用 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. 给身份安全治理的几条具体判断
不要把这件事当成“又一种钓鱼”塞进员工培训里就完。它真正在重塑几条工程判断:
- OAuth 流程本身就是身份安全的攻击面。 把治理只压在“密码 + MFA”上,已经追不上现在的攻击者;要把
authenticationProtocol字段直接纳入 SOC 数据模型。 - Conditional Access 的“authentication flows”条件是默认应该 Block 的。 它和 NetworkPolicy 在网络层一样,是少数能在控制面把一类高风险流量直接关掉的开关;这种开关默认开放就是失职。
- Token 生命周期不能交给应用自治。 CAE、Token Protection、shorter access token lifetime 这三项需要按租户硬性纳入身份基线。
- 检测规则要写到字段一级,不能停留在描述。 本文给的
appId = 29d9ed98-... + authenticationProtocol = deviceCode + resourceId ∈ Office三元组,是当前公开情报里命中率最稳定的组合,应该当成最小检测基线。 - 应急响应必须包括“设备/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 是与本文直接相关、但跨度更大的能力面,会作为后续文章单独覆盖。

