ConsentFix(也称 AuthCodeFix)的交接点不是密码,而是登录后的回调 URL。用户可以在真实身份提供方完成登录与 MFA,随后把含授权码的地址交给外部页面;另一方若满足兑换条件,就可能拿到资源访问令牌。
2026 年 1 月的样本涉及 Azure CLI 与 Microsoft Entra 登录日志。把授权请求、浏览器回调、非交互式登录和资源调用分开,既能定位异常,也能避免把“异地登录”直接判成授权码钓鱼。
授权码不等于访问令牌
正常流程中,浏览器带回短期授权码,客户端向令牌端点兑换,再携带访问令牌调用资源 API。授权码只代表流程的中间结果;访问能力仍受令牌受众、范围、用户权限及资源端检查约束。
sequenceDiagram participant B as 浏览器 participant I as 身份平台 participant C as 客户端 participant R as 资源 API B->>I: 登录与授权请求 I-->>B: 回调 URI 中的授权码 B->>C: 交付授权响应 C->>I: 授权码与绑定参数 I-->>C: 访问令牌 C->>R: 携带令牌的请求 R-->>C: 按权限返回结果
授权码兑换需满足客户端、回调 URI 等约束。采用 PKCE 时,还要证明持有相应的 code_verifier;保密客户端另有客户端认证要求。样本中的请求未展示 code_challenge,不代表所有应用流程都缺少绑定。
PKCE 防范的是截获合法客户端授权码后、缺少 verifier 的兑换方。若外部方自己发起授权请求并掌握 verifier,PKCE 本身并不证明用户把结果交给了正确应用。判断重点始终是请求由谁发起,以及绑定材料由谁持有。
浏览器报错发生在哪一层

界面示意图。账户、应用和授权码均为示例;三个界面不代表每次登录都会依次出现。
额外的权限同意页可能因既有授权和应用配置而省略。账户选择也不等于重新输入密码或重新完成 MFA,login_hint 更不是身份验证结果。真实登录页面与有效会话,并不足以证明后续复制地址的要求可信。
| 观测 | 所在层 | 对授权响应的意义 |
|---|---|---|
AADSTS50011 |
身份平台的回调 URI 校验 | 该请求的回调与注册配置不匹配 |
DNS_PROBE_FINISHED_NXDOMAIN |
回调主机的 DNS 解析 | 浏览器尚未从目标服务取得 HTTP 响应 |
ERR_CONNECTION_REFUSED |
本地回调端口连接 | 没有成功连接到回调接收器 |
| HTTP 404 | 已到达的 HTTP 服务 | 服务返回路径不存在,与连接拒绝不同 |
另一应用样本回到 https://aadrm.com/adminpowershell 时显示 DNS 错误;Azure CLI 样本则停在 localhost 连接拒绝页。两者都说明“回调 URI 获准”与“浏览器成功加载回调页面”不是一回事。错误页面出现时,带有 code 的地址仍可能留在地址栏。
检查两段请求的绑定材料
以下是 Azure CLI 样本中的关键字段,账户已替换为示例。它们用于阅读记录,不是完整授权请求。
回调的结构为 http://localhost:1605/?code=<redacted>&session_state=<redacted>。检查兑换记录时,应对应相同的 client_id、redirect_uri、授权码及适用的 PKCE 或客户端认证材料。state、OIDC 的 session_state 与 Entra 日志的 SessionId 各有语义,名称相似不构成通用等价关系。
样本还混用了不带 /v2.0 的授权端点和 v2 令牌端点。调查时应保留完整端点版本,不把一次成功推广成任意组合都能互换。资源范围中的 /.default 是 Microsoft identity platform 的约定,不是授予所有 API 权限的通配符。
令牌回复的可确认字段如下;范围和凭据均已省略,4535 是该次回复的 expires_in,不是产品固定有效期。
时间差必须先问时间字段
Azure CLI 的两条记录具有相同的账户、应用和非空会话标识,但来源地址不同。展示中的网络位置分别是 GR / Athens 与 DE / Frankfurt Am Main;IP 地理位置只是辅助线索。
| 来源表 | 展示的 TimeGenerated,UTC |
位置 |
|---|---|---|
SigninLogs |
2026-01-24T21:39:55.8257112Z | GR |
AADNonInteractiveUserSignInLogs |
2026-01-24T21:44:32.3584638Z | DE |
两项时间戳相差 276.5327526 秒,按整秒边界计算是 277。展示字段是 TimeGenerated,所以它首先说明日志时间的间隔,而非已确认的授权码持有时长。正式关联应使用 CreatedDateTime,并保留 TimeGenerated 用于判断入库延迟。
同一会话的非交互式登录也可能是正常令牌续期。仅凭表名和相同 SessionId,还不足以证明该事件使用了被复制的授权码;需要结合请求、浏览器行为和应用上下文。
用事件键关联,而非按用户去重
下面是检测候选查询,限定样本中的 Azure CLI 应用。它以租户、用户对象 ID、应用和非空会话共同关联,使用实际事件时间筛选前后十分钟,并保留两侧事件 ID。
let app_ids = dynamic(["04b07795-8ddb-461a-bbee-02f9e1bf7b46"]);
let lookback = 1d;
let window_seconds = 600.0;
let interactive = SigninLogs
| where TimeGenerated > ago(lookback)
| where IsInteractive == true and AppId in~ (app_ids)
| where tostring(ResultType) == "0"
| project TenantKey=tolower(AADTenantId), UserKey=tolower(UserId),
AppId=tolower(AppId), SessionId, InteractiveId=Id,
InteractiveTime=CreatedDateTime, InteractiveIP=IPAddress,
InteractiveCountry=Location, InteractiveAgent=UserAgent;
let noninteractive = AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(lookback)
| where AppId in~ (app_ids)
| where toint(parse_json(tostring(Status)).errorCode) == 0
| project TenantKey=tolower(AADTenantId), UserKey=tolower(UserId),
AppId=tolower(AppId), SessionId, NonInteractiveId=Id,
NonInteractiveTime=CreatedDateTime, NonInteractiveIP=IPAddress,
NonInteractiveCountry=Location, NonInteractiveAgent=UserAgent,
ResourceDisplayName, UniqueTokenIdentifier;
interactive
| where isnotempty(TenantKey) and isnotempty(UserKey) and isnotempty(SessionId)
| join kind=inner noninteractive on TenantKey, UserKey, AppId, SessionId
| extend Seconds=(NonInteractiveTime - InteractiveTime) / 1s
| where Seconds between (0.0 .. window_seconds)
| where isnotempty(InteractiveIP) and isnotempty(NonInteractiveIP)
| where InteractiveIP != NonInteractiveIP
| extend CrossCountry=isnotempty(InteractiveCountry)
and isnotempty(NonInteractiveCountry)
and InteractiveCountry != NonInteractiveCountry
| project InteractiveId, NonInteractiveId, InteractiveTime, NonInteractiveTime,
Seconds, TenantKey, UserKey, AppId, SessionId, InteractiveIP,
NonInteractiveIP, CrossCountry, InteractiveCountry,
NonInteractiveCountry, InteractiveAgent, NonInteractiveAgent,
ResourceDisplayName, UniqueTokenIdentifier这段查询尚未在真实工作区执行。离线模型已覆盖空会话、不同租户、不同用户、失败事件、负时间差、十分钟边界和相同 IP 等 12 个用例;一秒内的异常不被人为排除,两个候选前序事件也都保留。
部署时有四个容易影响结果的细节:
SigninLogs.ResultType的成功值是字符串0;另一张表的文档将其描述为 Success / Failure。查询在非交互式侧读取Status.errorCode,避免直接假设同一编码。Status在不同表中可能是动态对象或字符串,parse_json(tostring(Status))统一读取形态;缺少状态码的记录应单独检查覆盖率,而非自动当作成功。AADTenantId对应身份租户;TenantId在日志表中表示 Log Analytics 工作区。跨工作区或多租户调查不要混用。join kind=inner保留候选配对;不要使用按左侧键自动去重的默认联接来悄悄丢弃活动。输出行数也不是攻击次数。
源地址字符串比较仍只是初筛。IPv4 映射地址、IPv6 格式差异、VPN 与代理出口都值得归一化或人工核验。CrossCountry 只作为附加信息,不应当作必须满足的条件。
从会话走到实际资源请求
后续两张资源追踪记录属于 Aadrm Admin Powershell,其应用 ID 为 90f610bf-206d-4950-b61d-37fa6fd1b224,不是前面的 Azure CLI。应把它视为另一案例,避免拼成同一次 277 秒关联的后续活动。
其中非交互式登录的 UniqueTokenIdentifier 与 Graph 日志的 SignInActivityId 对应,观测到的请求路径是 /v1.0/me。这说明存在身份资料查询记录;是否成功及是否访问其他数据,还要检查响应状态与更多资源日志。
以下以占位令牌标识演示关联,实际调查使用前序事件中提取的标识。查询同时限定租户和用户,保留请求 ID、方法、URI 与结果。
let tokens = AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(1d)
| where UniqueTokenIdentifier == "TOKEN_ID_EXAMPLE"
| project TenantKey=tolower(AADTenantId), UserKey=tolower(UserId),
TokenKey=UniqueTokenIdentifier, SignInId=Id;
MicrosoftGraphActivityLogs
| where TimeGenerated > ago(1d)
| extend TenantKey=tolower(AadTenantId), UserKey=tolower(UserId),
TokenKey=SignInActivityId
| join kind=inner tokens on TenantKey, UserKey, TokenKey
| project TimeGenerated, SignInId, RequestId, RequestMethod,
RequestUri, ResponseStatusCode, TokenKeyGraph 活动日志需要事先接入相应日志目的地。没有匹配行可能来自采集、留存、字段覆盖或资源不在 Graph 等原因,不宜直接解释为没有资源访问。原始授权码与访问令牌不应进入告警标题、工单正文或共享截图。
处置关注交接,而非否定 MFA
MFA 可以正确完成,但授权结果仍被用户转交。这个事实不等于条件访问对所有客户端失效,也不等于任何第一方应用都具有相同权限。需要核对具体应用、资源、客户端类型和当时的策略评估结果。
调查应保全登录与资源日志,确认异常会话、相关令牌和已发生操作,再按组织流程撤销受影响会话或授权并验证后续活动。配置治理则关注必要的应用使用范围、回调与权限、PKCE 的实际发起方,以及令牌保护对目标客户端和资源的支持条件。
判断这类事件,最有用的组合是:真实登录后的回调地址、用户交接行为、同会话的跨来源活动,以及与令牌关联的资源请求。单独一个 CLI 登录或异地 IP,只够启动调查。