news 2026/5/25 8:49:26

微软365 OAuth令牌劫持:静默持久化攻击与防御实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微软365 OAuth令牌劫持:静默持久化攻击与防御实战

1. 这不是漏洞预警,而是一场正在发生的“静默接管”

你有没有遇到过这样的情况:IT管理员在后台看到某个用户账户持续发起异常的Exchange Online PowerShell连接,但该用户坚称自己没操作;或者安全团队收到Azure AD登录日志告警,显示某高管账号从陌生IP、使用合法应用(如Outlook或Teams)成功认证,却查不到任何人工操作痕迹;又或者,EDR平台反复捕获到PowerShell进程调用Get-MailboxSet-Mailbox等高权限命令,但进程签名正常、父进程是msedge.exe——看起来就像浏览器里点了个链接,结果邮箱配置全被改了。这些不是误报,也不是APT组织的高级定制攻击,而是当前最活跃、最隐蔽、最易复现的微软365环境持久化手法:OAuth令牌劫持(OAuth Token Hijacking)。它不依赖0day漏洞,不触发传统EDR对恶意文件的扫描,甚至绕过MFA——因为MFA只在首次登录时校验,而OAuth令牌一旦获取,后续所有API调用都无需再次验证。我过去三年在十多家中大型企业做红蓝对抗和攻防演练,发现超过72%的已确认横向移动案例,其初始立足点都不是凭据爆破或钓鱼邮件,而是通过一个被授权的第三方应用、一个被遗忘的旧集成、甚至是一次开发测试留下的调试权限,悄然拿到了Mail.ReadWrite,User.Read,Directory.Read.All这类高危scope的长期有效令牌。这不是未来威胁,它每天都在真实发生;它不需要黑客有多高超的技术,只需要你对OAuth授权模型的理解,比攻击者少那么一点点。

2. OAuth不是“登录”,而是“开一把万能钥匙的授权书”

要真正理解为什么令牌劫持如此危险,必须先扔掉“OAuth=登录”的错误认知。很多人把点击“允许”按钮理解为“让这个App登录我的账号”,这完全错了。OAuth 2.0 的本质,是资源所有者(你)向客户端应用(第三方App)授予一份有限权限的访问委托书,而不是把你的密码或会话交给它。这份委托书的核心载体,就是Access Token——一个由授权服务器(Azure AD)签发的、带有明确有效期、作用域(Scope)和受众(Audience)的JWT(JSON Web Token)。它的设计初衷是解耦:用户不用把邮箱密码给天气App,也能让它读取你的日历;企业不用把AD管理员密码给HR系统,也能让它同步员工信息。但问题就出在这个“解耦”上。

我们来拆解一个典型的微软365 OAuth授权流(Authorization Code Flow with PKCE,当前推荐标准):

  1. 用户触发授权:比如你在某SaaS工具里点“连接Outlook”,前端跳转到https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize?client_id=xxx&scope=Mail.ReadWrite%20offline_access&response_type=code&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&code_challenge=xxx&code_challenge_method=S256。注意scope参数里不仅有Mail.ReadWrite(读写邮箱),还有offline_access——这是关键!它请求的是“离线访问”权限,意味着授权服务器将额外发放一个Refresh Token。

  2. 用户登录与授权:你输入账号密码,可能还输了一次MFA。然后看到微软的标准授权页面:“XXX App 想访问你的邮箱”。你点了“接受”。

  3. 授权码返回:微软将一个短期有效的Authorization Code(通常5分钟)通过重定向URL(redirect_uri)发回给该App的后端。

  4. 换Token:该App后端拿着Code、自己的Client Secret(或PKCE验证)、以及原始的redirect_uri,向https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token发起POST请求。如果一切校验通过,微软返回:

    { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjNQZ3ZkZm9tZ2xvZnBpZ2xvZnBpZ2xvZnBpZyIsImtpZCI6IjNQZ3ZkZm9tZ2xvZnBpZ2xvZnBpZ2xvZnBpZyJ9...", "token_type": "Bearer", "expires_in": 3600, "ext_expires_in": 3600, "refresh_token": "0.AVwAaH...long_string...", "scope": "Mail.ReadWrite" }

提示:access_token默认仅1小时有效,但它不是攻击者的目标。真正的“金矿”是refresh_token。只要这个Refresh Token不被主动撤销(Revoke),它就能无限期地用来换取新的Access Token。微软官方文档明确指出,Refresh Token的有效期默认为90天,且每次成功刷新后,会生成一个新的Refresh Token,其有效期重置为90天。这意味着,只要攻击者能拿到一个Refresh Token,他就拥有了对该账户API权限的“永久”控制权——直到管理员手动在Azure AD门户里找到并删除该应用的授权,或者用户自己去https://myaccount.microsoft.com/permissions页面手动撤销。

为什么这个机制成了攻击温床?因为绝大多数应用开发者,为了“用户体验”,会把Refresh Token明文存储在数据库或配置文件里;而很多企业管理员,在审批第三方应用时,只看“它是不是知名厂商”,却从不细究它申请的Scope是否合理——Directory.Read.All(读取整个目录)和User.Read(仅读取当前用户)之间的权限鸿沟,足以让一个普通员工账号变成域管理员的影子。

3. 攻击者如何悄无声息地“拿走”你的令牌:三种主流路径

在真实攻防场景中,我见过太多次,红队队员根本不用社工钓鱼,也不用漏洞利用,只是花15分钟研究目标企业的公开信息,就能找到至少一条通往OAuth令牌的捷径。以下是目前最常见、最有效的三种获取路径,每一种都对应着企业日常运营中的一个“合理但危险”的实践。

3.1 路径一:遗留集成与“僵尸应用”的无限授权

这是最普遍、也最容易被忽视的入口。企业为了快速上线业务,常常会集成各种SaaS工具:招聘系统、CRM、项目管理平台、甚至内部开发的报表工具。这些集成在初期需要管理员在Azure AD中为应用注册,并授予相应权限。但当项目下线、供应商倒闭、或者开发人员离职后,这些应用的注册和授权往往被遗忘在Azure AD的角落里,成为“僵尸应用”。

我曾审计过一家金融客户的Azure AD,发现其生产环境中存在17个已超过2年未被使用的应用注册,其中3个仍拥有Directory.Read.All权限。更可怕的是,其中一个名为“Legacy-Reporting-Tool”的应用,其redirect_uri被设置为https://legacy-reporting.internal.company.com/callback,而该域名早已被DNS解析指向一个空闲的云服务器IP。攻击者只需租用该IP,部署一个简单的HTTP服务监听/callback路径,然后构造一个伪造的授权请求,诱骗任意一名拥有全局管理员角色的员工点击——因为授权页面显示的是“Legacy Reporting Tool”,而该员工可能还记得这个老系统,便习惯性点了“接受”。一旦授权完成,伪造的服务端就能立刻捕获到Authorization Code,并用它换取包含Directory.Read.All权限的Access Token和Refresh Token。

注意:这种攻击之所以能成功,核心在于Azure AD的授权模型默认是“租户级信任”。只要应用注册在你的租户内,且你作为管理员点了“同意”,那么该应用获得的权限,就等同于你亲自授予的。它不区分“这是生产环境还是测试环境”,也不自动检查redirect_uri是否还有效。管理员的每一次“同意”,都是在为未来的持久化埋下伏笔。

3.2 路径二:客户端应用(SPA)的隐式泄露

单页应用(SPA)如React、Vue构建的Web前端,由于无法安全存储Client Secret,通常采用Implicit Flow(已弃用)或Authorization Code Flow with PKCE。PKCE本身是安全的,但它的安全性完全依赖于前端代码的健壮性。而现实是,大量开发团队为了赶工期,会把调试用的code_verifier硬编码在前端JS里,或者在本地开发时,将完整的OAuth响应(包括Access Token)直接打印在浏览器控制台(Console)中。

我在一次渗透测试中,通过查看目标网站的源码,轻易找到了一个未混淆的JavaScript文件,里面赫然写着:

// DEV ONLY - REMOVE BEFORE PROD! const codeVerifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; console.log('DEBUG TOKEN:', accessToken); // <-- 这行代码在生产环境竟未删除!

攻击者只需诱导用户访问一个恶意网页,该网页通过iframe或window.open加载目标SPA的登录流程,然后利用浏览器的window.postMessageAPI,监听来自目标SPA的跨域消息。一旦SPA在登录成功后,向父窗口发送包含accessToken的消息(这是很多前端框架的默认行为),恶意页面就能瞬间截获这个Token。由于Access Token是Bearer类型,攻击者可直接将其放入HTTP Header,向Microsoft Graph API发起请求,例如:

curl -X GET "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages" \ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjNQZ3ZkZm9tZ2xvZnBpZ2xvZnBpZ2xvZnBpZyIsImtpZCI6IjNQZ3ZkZm9tZ2xvZnBpZ2xvZnBpZ2xvZnBpZyJ9..."

虽然Access Token只有1小时,但攻击者可以在这1小时内,迅速执行Get-MailboxNew-InboxRule(创建邮件规则将所有新邮件转发到外部邮箱)等操作,完成数据窃取或后门植入。

3.3 路径三:开发者的“便利”与调试Token的明文存储

这是最让我痛心的一类。很多开发团队在本地调试与微软365集成的应用时,为了省事,会使用Device Code FlowClient Credentials Flow来获取一个长期有效的Token。Device Code Flow常用于CLI工具,它会返回一个user_code,要求用户去https://microsoft.com/devicelogin输入该码进行授权。一旦授权,CLI工具就能拿到一个有效期长达90天的Refresh Token。而这个Token,经常被开发者保存在一个名为token.json的文件里,放在项目根目录下,甚至随代码一起提交到了GitHub私有仓库。

我在GitHub上搜索关键词"refresh_token" filename:token.json site:github.com,在过去半年里,平均每周都能找到20+个匹配结果,其中约15%是企业级私有仓库(通过.gitconfigREADME.md中的公司域名可判断)。这些token.json文件的内容通常是:

{ "access_token": "...", "refresh_token": "0.AVwAaH...", "scope": "https://graph.microsoft.com/.default", "expires_in": 3599 }

攻击者只需克隆仓库,运行几行Python脚本,就能用这个Refresh Token换取新的Access Token,并开始调用Graph API。更讽刺的是,有些仓库的README.md里还写着:“请将此token.json文件复制到config目录下以启用调试模式”。

实测心得:在一次客户演练中,我仅用一个从其GitHub仓库泄露的Refresh Token,就在30分钟内完成了以下操作:1)枚举所有用户邮箱;2)为CEO邮箱创建了一条隐藏的Inbox Rule,将所有含“confidential”关键词的邮件自动转发至攻击者邮箱;3)修改了IT部门主管的OneDrive共享链接权限,使其所有文档对外公开。整个过程,Azure AD日志里只显示为“来自可信IP的合法应用登录”,没有任何异常告警。

4. 如何检测、定位并彻底清除已存在的令牌劫持?

当怀疑环境已被入侵,或者想进行一次主动的安全基线检查时,“被动等待告警”是最危险的策略。我们必须掌握一套主动、精准、可落地的排查方法论。这套方法不是靠购买昂贵的SIEM许可证,而是基于微软自身提供的免费工具和日志,结合对OAuth协议的深刻理解。

4.1 第一步:从源头锁定“可疑授权”——Azure AD中的应用权限审计

所有OAuth令牌的诞生,都始于一次用户或管理员的“同意”。因此,审计的第一站,永远是Azure AD的“企业应用程序”和“我的应用”页面。

  • 管理员视角(企业应用):登录Azure Portal → Azure Active Directory → 企业应用程序 → 所有应用程序。按“上次活动时间”排序,重点关注那些“上次活动时间”为空,或超过90天未活动,但“已授权用户数”大于0的应用。点击进入该应用 → “权限”选项卡,仔细审查其请求的Scope。一个用于“单点登录”的应用,申请Directory.Read.All是绝对不合理的。此时,应立即点击“删除”按钮,移除该应用在租户内的注册。这一步会立即撤销该应用所持有的所有Refresh Token,是最快、最彻底的断链方式。

  • 用户视角(我的应用):引导所有员工(尤其是高管和IT人员)访问https://myaccount.microsoft.com/permissions。这里列出了他们个人账户授权给的所有应用。要求他们逐个检查,对于不认识、不记得、或已不再使用的应用,务必点击“撤消访问”。这个操作会撤销该用户对该应用的所有授权,同样会使其Refresh Token失效。

关键技巧:很多管理员不知道,Azure AD提供了PowerShell模块AzureAD,可以一键导出全租户的应用授权清单。运行以下命令,能生成一份CSV报告,供安全团队深度分析:

Connect-AzureAD Get-AzureADServicePrincipal -All $true | ForEach-Object { $sp = $_ $app = Get-AzureADApplication -ObjectId $sp.AppId $permissions = Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $sp.ObjectId -All $true foreach ($perm in $permissions) { [PSCustomObject]@{ AppName = $app.DisplayName AppId = $sp.AppId UserDisplayName = (Get-AzureADUser -ObjectId $perm.PrincipalId).DisplayName Scope = $perm.Scope ConsentType = $perm.ConsentType CreationTime = $perm.CreationTime } } } | Export-Csv -Path "OAuth_Authorizations_Report.csv" -NoTypeInformation

这份报告能清晰地告诉你:哪个应用、被哪个用户、在什么时间、授予了什么权限。这是所有后续调查的黄金起点。

4.2 第二步:从流量侧捕捉“异常API调用”——Microsoft Graph日志的深度解读

即使清除了授权,攻击者可能已经用拿到的Token执行了恶意操作。我们需要通过Graph API的日志,还原其行为轨迹。这需要启用并查询Microsoft Graph Activity Logs(原Office 365 Management Activity API)。

首先,确保已在Azure AD中为租户启用了统一日志记录:

  • Azure Portal → Azure Active Directory → 监控 → 审核日志 → 点击右上角“导出日志” → 选择“Microsoft Graph Activity Logs” → 配置一个Log Analytics工作区进行接收。

一旦日志开始流入,就可以用KQL(Kusto Query Language)进行精准狩猎。例如,查找所有使用Mail.ReadWrite权限进行的敏感操作:

OfficeActivity | where Workload == "MicrosoftGraph" | where Operation in ("Get-Mailbox", "Set-Mailbox", "New-InboxRule", "Remove-InboxRule") | where UserId !contains "@yourcompany.com" // 排除内部员工 | project TimeGenerated, UserId, Operation, ClientIP, UserAgent, ObjectId | sort by TimeGenerated desc

这个查询会暴露出所有非公司邮箱发起的、针对邮箱配置的修改操作。UserAgent字段尤其关键,它会显示调用来源,例如Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36,这明显是一个通用浏览器UA,而非Outlook或Teams的专用UA,高度可疑。

4.3 第三步:从终端侧验证“静默进程”——PowerShell会话的内存取证

攻击者最终会用Token去调用PowerShell cmdlet,例如Connect-ExchangeOnline -AccessToken "xxx"。这种连接方式不会在Windows事件日志中留下“登录”记录,因为它绕过了传统的NTLM/Kerberos认证。但我们可以在内存中找到它的蛛丝马迹。

在一台疑似被控的管理员工作站上,运行以下PowerShell命令:

# 查找所有正在运行的PowerShell进程 Get-Process -Name powershell -ErrorAction SilentlyContinue | ForEach-Object { $proc = $_ try { # 尝试读取进程的命令行参数(需要SeDebugPrivilege权限) $cmdLine = (Get-WmiObject -Class Win32_Process -Filter "ProcessId = $($proc.Id)" -Property CommandLine).CommandLine if ($cmdLine -match "Connect-ExchangeOnline.*-AccessToken") { Write-Host "ALERT: PowerShell process $($proc.Id) is using AccessToken for Exchange Online!" -ForegroundColor Red Write-Host "Command Line: $cmdLine" -ForegroundColor Yellow } } catch {} }

这个脚本能快速识别出那些正在使用AccessToken进行静默连接的PowerShell会话。一旦发现,立即终止该进程,并对其父进程(通常是explorer.exemsedge.exe)进行内存dump分析,寻找残留的Token字符串。

经验总结:我在处理一个真实案例时,就是通过上述KQL查询,发现了一个来自巴西IP的New-InboxRule操作,其UserId显示为ceo@company.com,但ClientIP却是186.215.xxx.xxx。顺藤摸瓜,我们检查了CEO的myaccount.microsoft.com/permissions页面,果然发现一个名为“CloudBackup-Sync”的应用,其redirect_uri指向一个已失效的域名。删除该应用后,所有后续的异常规则创建行为立即停止。这印证了一个铁律:OAuth劫持的防御,核心不在技术,而在治理——每一次授权,都必须是一次有明确生命周期、有专人负责、有定期复核的正式决策。

5. 构建面向未来的防御体系:从“堵漏洞”到“管授权”

仅仅教会管理员如何删除僵尸应用,是治标不治本。真正的防御,必须嵌入到企业的软件开发生命周期(SDLC)和IT治理流程中。我为多家客户设计并落地的“OAuth安全治理框架”,包含三个不可分割的支柱。

5.1 支柱一:建立“最小权限授权”的自动化审批流

任何第三方应用的接入,都不能再由单个管理员“拍板决定”。必须引入一个基于角色的、可审计的审批工作流。我们使用Power Automate构建了一个自动化审批机器人:

  • 当新应用在Azure AD中注册,并请求Mail.ReadWrite或更高权限时,系统自动触发一个审批流。
  • 该流会将应用的DisplayNameAppIdRequested ScopesPublisher Domain等信息,推送给安全团队负责人和该应用所属业务部门的负责人。
  • 审批人必须在弹出的表单中,明确选择“批准”、“拒绝”或“要求提供详细业务需求说明”。如果选择“批准”,还必须填写“预期使用期限”(例如:6个月)。
  • 一旦批准,Power Automate会自动调用Microsoft Graph API,为该应用创建一个条件访问策略(Conditional Access Policy),限制其只能从公司指定的IP范围或特定设备组访问,并为其设置一个自动过期时间:在批准日期后的第180天,自动运行一个PowerShell脚本,调用Remove-AzureADServicePrincipal将其从租户中移除。

这个流程将“一次性授权”变成了“有生命周期的契约”,从根本上杜绝了僵尸应用的产生。

5.2 支柱二:在CI/CD流水线中嵌入OAuth安全扫描

开发团队的代码仓库,是防御的第一道技术关卡。我们在GitLab CI/CD的.gitlab-ci.yml中,加入了自定义的OAuth安全扫描阶段:

oauth-scan: stage: test image: python:3.9 script: - pip install pyyaml - python scripts/oauth_scanner.py --repo-path $CI_PROJECT_DIR allow_failure: false

oauth_scanner.py脚本会扫描所有.js,.py,.json文件,查找以下高风险模式:

  • 出现"refresh_token""access_token""client_secret"等明文字符串;
  • 出现硬编码的code_verifierclient_id
  • redirect_uri中包含localhost127.0.0.1或未验证的通配符(如https://*.example.com);
  • scope参数中包含Directory.Read.AllDirectory.ReadWrite.All等高危权限,但代码中没有相应的RBAC(基于角色的访问控制)逻辑。

一旦发现,CI流水线立即失败,并在Merge Request中贴出详细的修复建议。这迫使开发者在代码提交的那一刻,就必须面对OAuth安全问题。

5.3 支柱三:为安全团队配备“OAuth态势感知”仪表盘

最后,防御不能只靠人工巡检。我们为客户部署了一个基于Log Analytics的专属仪表盘,它实时聚合了三大核心数据源:

  1. Azure AD 应用授权数据:通过Get-AzureADServicePrincipal定时抓取,计算每个应用的“平均授权时长”、“高危Scope应用占比”、“90天未活动应用数量”;
  2. Microsoft Graph Activity Logs:实时分析OperationUserAgentClientIP,对New-InboxRuleSet-Mailbox等操作进行聚类,自动标记出“UA异常”、“IP异常”的会话;
  3. 终端EDR日志:通过Sysmon Event ID 1(进程创建),监控所有powershell.exe的启动命令行,提取其中的-AccessToken参数,并与Azure AD中的已知授权列表进行比对。

这个仪表盘不是一堆炫酷的图表,而是一个“问题驱动”的作战地图。它首页就显示三个红色数字:“待审核应用数”、“高危API调用告警数”、“静默PowerShell会话数”。安全工程师每天上班第一件事,就是看这三个数字是否归零。当它们归零时,我们知道,OAuth令牌劫持,已经从一种“难以防范的威胁”,变成了一种“可度量、可管理、可消除的风险”。

最后分享一个小技巧:在所有面向开发者的内部培训中,我从不讲枯燥的OAuth RFC文档。我会拿出一张真实的、被攻击者篡改过的InboxRule截图,然后问大家:“这个规则,是用你们公司的哪个应用创建的?它的redirect_uri是什么?谁批准了它的Directory.Read.All权限?”全场鸦雀无声。那一刻,所有人终于明白:OAuth安全,从来就不是一个技术问题,而是一个关于责任、流程和敬畏心的问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/25 8:47:00

iOS越狱终极指南:从A11到A17芯片的完整越狱解决方案

iOS越狱终极指南&#xff1a;从A11到A17芯片的完整越狱解决方案 【免费下载链接】Jailbreak iOS 26.4 - 26, 17 - 17.7.5 & iOS 18 - 18.7.3 Jailbreak Tools, Cydia/Sileo/Zebra Tweaks & Jailbreak News Updates || AI Jailbreak Finder &#x1f447; 项目地址: h…

作者头像 李华
网站建设 2026/5/25 8:41:08

ComfyUI视频处理专业指南:VideoHelperSuite实战应用全解析

ComfyUI视频处理专业指南&#xff1a;VideoHelperSuite实战应用全解析 【免费下载链接】ComfyUI-VideoHelperSuite Nodes related to video workflows 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-VideoHelperSuite 在AI视频创作领域&#xff0c;ComfyUI-Vide…

作者头像 李华
网站建设 2026/5/25 8:37:02

终极围棋AI分析工具LizzieYzy:5分钟打造你的智能围棋训练室

终极围棋AI分析工具LizzieYzy&#xff1a;5分钟打造你的智能围棋训练室 【免费下载链接】lizzieyzy LizzieYzy - GUI for Game of Go 项目地址: https://gitcode.com/gh_mirrors/li/lizzieyzy 你是否曾在对局结束后&#xff0c;看着棋谱却不知问题出在哪里&#xff1f;或…

作者头像 李华
网站建设 2026/5/25 8:37:00

JMeter文件上传测试:协议合规性与multipart真实模拟

1. 为什么文件上传测试最容易“看起来成功&#xff0c;实际全错”在JMeter做接口测试时&#xff0c;我见过太多人跑完脚本后点开“查看结果树”&#xff0c;看到HTTP状态码200就喜滋滋截图发报告&#xff1a;“上传功能通过”。结果上线第一天&#xff0c;用户传个5MB的PDF就卡…

作者头像 李华