Entra条件访问策略中的时间后门

admin 2026-04-22 05:28:08 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文揭示MicrosoftEntra条件访问策略中存在一个隐藏的基于时间条件,允许拥有条件访问管理员权限的攻击者创建时间后门,在特定日期或小时自动禁用安全策略。该后门在Azure门户、MicrosoftGraph和PowerShell模块中均不可见,且无法被What-If工具检测。文章通过探索IbizaAPI展示了具体操作方法,并指出这种隐蔽性对租户身份安全构成严重威胁。 综合评分: 85 文章分类: 漏洞分析,云安全,渗透测试,红队,安全运营


cover_image

Entra条件访问策略中的时间后门

Dubito Dubito

云原生安全指北

2026年4月21日 08:35 江苏

在小说阅读器读本章

去阅读

注:本文翻译自 Reversec 的文章《It’s Just a Matter of Time: Backdooring Conditional Access Policies》[1],可点击文末“阅读原文”按钮查看英文原文。

全文如下:

摘要

Microsoft Entra 中的条件访问(Conditional Access)策略包含一个隐藏的、基于时间的条件。该条件在 Azure 门户或 Microsoft Graph 中均不可见。最低拥有 Conditional Access Administrator(条件访问管理员)角色的攻击者即可添加此条件,从而创建一个基于时间的后门。该后门能在每周的特定日期或小时悄无声息地禁用安全策略。此更改对门户中的管理员不可见,What-If(假设分析)工具也不会报告,且在使用 Microsoft.Entra 等管理 PowerShell 模块的命令行输出中也会缺失该条件。

一、介绍

对于少数必须管理 Microsoft Entra 租户的人来说,条件访问(Conditional Access)策略被视为特定租户内身份安全的基石。你可以创建各种强大的策略,将访问组织的权限限制在少数受信任的设备上。这也是在整个资产范围内强制实施多因素身份验证(MFA,Multi-Factor Authentication)等控制措施的主要途径。此外,如果用户主体不满足特定标准,通过这种方式明确阻止其访问也非常有效。如果你曾在测试策略时不小心锁定了自己的测试账户,不得不使用应急备用账户来恢复,那么你就能切身体会到它的威力有多大!

顾名思义,条件访问策略使用一组已知的条件来定义策略何时应该生效、何时不应该生效。每个使用 Entra 的人都很熟悉这些条件,例如“请求源自哪个 IP 地址”、“是否是合规设备”或“是什么设备类型”。由于攻击者的滥用,安全社区在某种程度上已更擅长构建和理解这些条件的工作机制细节及其潜在漏洞。但是,如果我们习惯的这些条件并非全部可能的情况呢?那么,让我们直接深入探讨核心问题吧……

二、在 Ibiza 的适时停留

也许我们不应该直接深入,而是稍微绕个路,先铺垫一下背景。正如之前相关研究所揭示的,Azure 门户 (portal.azure.com) 会使用多种不同的 API 对 Azure 资源和 Entra 租户对象执行操作。其中有一个 API 被称为 Ibiza API,托管在 main.iam.ad.ext.azure.com。在众多功能中,该 API 允许用户在不直接使用 Microsoft Graph 的情况下对租户执行许多操作。关于这一鲜为人知且文档匮乏的 API,可以参考该领域其他专家的以下演讲和网站(由 Aled Mehta 创建)。他解释了 Ibiza API 的概念、如何进行身份验证,以及他如何尝试探索更多相关内容:

  • • https://www.youtube.com/watch?v=1Z8BA83X0YE
  • • https://nodoc.cloud/

三、闲着不妨探索一下这些 API

在项目交付间隙的休息时间里,我决定深入探索条件访问策略。这有助于我更好地理解这一安全控制机制,并根据我们对某些策略有效性的具体测试,提供更具针对性的建议,以应对攻击者尝试不同手段入侵环境的情况。起初,我使用 Microsoft Graph API 和相关的 PowerShell 命令集来查询策略并尝试对其进行操作。总体而言,这有助于我深入理解策略的定义方式。我也成功梳理出了一些有用的高层指导,即如何在组织中构建策略,使得多个策略部分重叠,从而即使某个策略被绕过,仍有其他策略能够捕获非预期的操作。

然而,由于我也一直在摆弄 Ibiza API,我决定看看能否使用该 API 来查询策略信息。于是,我利用 https://nodoc.cloud/ 中描述的认证方法,生成了一个可用于 Ibiza API 的有效访问令牌。使用 az CLI 时所需的具体命令如下:

az account get-access-token --resource 74658136-14ec-4630-ad9b-26e160ff0fc6

拿到有效令牌后,我接着研究条件访问策略能做些什么。通过查阅已记录的方法,我发现可以通过以下 HTTP GET 请求列出租户中的所有策略:

GET /api/Policies/Policies?top=445&nextLink= HTTP/1.1
Accept: application/json
Authorization: Bearer [REDACTED]
X-Ms-Client-Request-Id: 123
Host: main.iam.ad.ext.azure.com

这条请求返回了租户中每个策略 ID 的详细分解:

HTTP/1.1 200 OK
[为简洁起见已编辑]
Date: Tue, 31 Mar 2026 21:47:46 GMT
Content-Length: 1894

{
  "items": [
    ...[为简洁起见已编辑]...
    {
      "policyId": "8d7b8e12-f0e8-4190-a8e5-dd4de9bc0244",
      "policyName": "Block Bad Person",
      "applyRule": true,
      "policyState": 2,
      "usePolicyState": true,
      "baselineType": 0,
      "createdDateTime": "2026-03-31T21:42:34.9803608+00:00",
      "modifiedDateTime": null
    }
    ...[为简洁起见已编辑]...
}

接着,我想到可以通过 policyId 查询特定策略以获取更多详细信息。对应的 HTTP 请求如下:

GET /api/Policies/8d7b8e12-f0e8-4190-a8e5-dd4de9bc0244 HTTP/1.1
X-Ms-Client-Request-Id: 123
Authorization: Bearer [REDACTED]
Host: main.iam.ad.ext.azure.com

这返回了一些更有意思的内容——策略的完整描述!以下是这个示例策略的完整输出。该策略旨在将一名特定用户完全拒于租户门外:

HTTP/1.1 200 OK
[为简洁起见已编辑]
Date: Tue, 31 Mar 2026 21:50:20 GMT
Content-Length: 3815

{
  "usersV2": {
    "allUsers": 2,
    "included": {
      "allGuestUsers": false,
      "roles": false,
      "usersGroups": true,
      "roleIds": [],
      "externalUsers": null,
      "groupIds": [],
      "userIds": [
        "58845d0a-eccd-4c44-96b4-c8a61fe8e3e9" #被策略阻止的特定用户的 User ID
      ]
    },
    "excluded": {
      "allGuestUsers": false,
      "roles": false,
      "usersGroups": false,
      "roleIds": [],
      "externalUsers": null,
      "groupIds": [],
      "userIds": []
    }
  },
  "templateId": null,
  "servicePrincipals": {
    "allServicePrincipals": 1, #策略适用于所有应用程序
    "included": {
      "ids": []
    },
    "excluded": {
      "ids": []
    },
    "filter": null,
    "includeAllMicrosoftApps": false,
    "excludeAllMicrosoftApps": false,
    "userActions": null,
    "stepUpTags": null,
    "networkAccess": null
  },
  "controls": {
    "controlsOr": true,
    "blockAccess": true, #对任何匹配策略的请求强制执行显式阻止
    "challengeWithMfa": false,
    "requireAuthStrength": null,
    "compliantDevice": false,
    "domainJoinedDevice": false,
    "approvedClientApp": false,
    "claimProviderControlIds": [],
    "requireCompliantApp": false,
    "requirePasswordChange": false,
    "requiredFederatedAuthMethod": 0
  },
  "sessionControls": {
    "appEnforced": false,
    "cas": false,
    "cloudAppSecuritySessionControlType": 0,
    "signInFrequencyTimeSpan": {
      "type": 0,
      "value": 0,
      "authenticationType": 0,
      "frequencyInterval": 0
    },
    "signInFrequency": 0,
    "persistentBrowserSessionMode": 0,
    "continuousAccessEvaluation": 0,
    "resiliencyDefaults": 0,
    "secureSignIn": false,
    "secureApp": false,
    "networkAccessSecurity": null
  },
  "conditions": {
    "minUserRisk": {
      "lowRisk": false,
      "mediumRisk": false,
      "highRisk": false,
      "noRisk": false,
      "applyCondition": false
    },
    "minSigninRisk": {
      "noRisk": false,
      "lowRisk": false,
      "mediumRisk": false,
      "highRisk": false,
      "applyCondition": false
    },
    "signInRiskDetections": {
      "anonymousIPAddress": false,
      "unfamiliarFeatures": false,
      "realTimeThreatIntel": false,
      "applyCondition": false
    },
    "servicePrincipalRiskLevels": {
      "lowRisk": false,
      "mediumRisk": false,
      "highRisk": false,
      "applyCondition": false
    },
    "devicePlatforms": {
      "all": 1, #包含所有设备类型
      "included": {
        "android": false,
        "ios": false,
        "windowsPhone": false,
        "windows": false,
        "macOs": false,
        "linux": false
      },
      "excluded": {
        "android": false,
        "ios": false,
        "windowsPhone": false,
        "windows": false,
        "macOs": false,
        "linux": false
      },
      "applyCondition": true
    },
    "locations": {
      "includeLocationType": 0,
      "excludeAllTrusted": false,
      "applyCondition": false
    },
    "namedNetworks": {
      "includeLocationType": 1, #包含所有网络位置
      "excludeLocationType": 2,
      "includeTrustedIps": false,
      "excludeTrustedIps": false,
      "includedNetworkIds": [],
      "excludedNetworkIds": [],
      "includeCorpnet": false,
      "excludeCorpnet": false,
      "applyCondition": true
    },
    "clientApps": {
      "specificClientApps": false,
      "webBrowsers": false,
      "mobileDesktop": false,
      "exchangeActiveSync": false,
      "onlyAllowSupportedPlatforms": false,
      "applyCondition": false
    },
    "clientAppsV2": {
      "webBrowsers": false,
      "mobileDesktop": false,
      "modernAuth": false,
      "exchangeActiveSync": false,
      "onlyAllowSupportedPlatforms": false,
      "otherClients": false,
      "applyCondition": false
    },
    "time": {
      "all": 0,
      "included": {
        "type": 0,
        "timezoneId": null,
        "dateRange": {
          "startDateTime": "3/31/2026 12:00:00 AM",
          "endDateTime": "4/1/2026 12:00:00 AM"
        },
        "daysOfWeek": {
          "day": [0,1,2,3,4,5,6],
          "startTime": "3/31/2026 12:00:00 AM",
          "endTime": "4/1/2026 12:00:00 AM",
          "allDay": false
        },
        "isExcludeSet": false
      },
      "excluded": {
        "type": 0,
        "timezoneId": null,
        "dateRange": {
          "startDateTime": "3/31/2026 12:00:00 AM",
          "endDateTime": "4/1/2026 12:00:00 AM"
        },
        "daysOfWeek": {
          "day": [0,1,2,3,4,5,6],
          "startTime": "3/31/2026 12:00:00 AM",
          "endTime": "4/1/2026 12:00:00 AM",
          "allDay": false
        },
        "isExcludeSet": false
      },
      "applyCondition": false
    },
    "deviceState": {
      "includeDeviceStateType": 0,
      "excludeDomainJoionedDevice": false,
      "excludeCompliantDevice": false,
      "filter": null,
      "applyCondition": false
    }
  },
  "clientApplications": {
    "allServicePrincipals": 0,
    "filter": null,
    "includedServicePrincipals": null,
    "excludedServicePrincipals": null
  },
  "isAllProtocolsEnabled": false,
  "isUsersGroupsV2Enabled": false,
  "version": 1,
  "isFallbackUsed": false,
  "policyId": "8d7b8e12-f0e8-4190-a8e5-dd4de9bc0244",
  "policyName": "Block Bad Person",
  "applyRule": true,
  "policyState": 2, #强制执行状态
  "usePolicyState": true,
  "baselineType": 0,
  "createdDateTime": "2026-03-31T21:42:34.9803608+00:00",
  "modifiedDateTime": null
}

你可以滚动浏览并验证该策略:它匹配所有设备类型,旨在明确阻止 ID 为“58845d0a-eccd-4c44-96b4-c8a61fe8e3e9”的用户进行访问。我们可以尝试使用该账户登录来验证这一点。现在登录时,会收到条件访问错误提示:

乍一看,除了再次证明这些 API 对相同参数的命名约定毫无一致性之外,一切似乎都很正常。不过,观察力较强的人可能在浏览该策略时很快就注意到了某些怪异之处。没错,这也正是吸引我注意力的地方!

四、抱歉打扰,你知道“time”是什么吗?

在这个相当冗长的策略输出中,conditions 对象内部隐藏着一个非常陌生的 time 条件。

在我最初向 MSRC 报告此问题后不久,我发现了 Daniel Bradley 于 2023 年 11 月发布的博客。他在其中 发现了基于时间的条件的存在[2],但当时尚未找到实际应用该条件的方法。不过,在本博客发布时,Daniel 已于 2026 年 1 月发布了后续文章,成功 通过 Graph API 使其生效[3]。请务必查阅这两篇文章!

那么,让我们仔细审视一下这个条件,看看它是如何运作的:

"time": {
  "all": 0,
  "included": {
    "type": 0,
    "timezoneId": null,
    "dateRange": {
      "startDateTime": "3/31/2026 12:00:00 AM",
      "endDateTime": "4/1/2026 12:00:00 AM"
    },
    "daysOfWeek": {
      "day": [0, 1, 2, 3, 4, 5, 6],
      "startTime": "3/31/2026 12:00:00 AM",
      "endTime": "4/1/2026 12:00:00 AM",
      "allDay": false
    },
    "isExcludeSet": false
  },
  "excluded": {
    "type": 0,
    "timezoneId": null,
    "dateRange": {
      "startDateTime": "3/31/2026 12:00:00 AM",
      "endDateTime": "4/1/2026 12:00:00 AM"
    },
    "daysOfWeek": {
      "day": [0, 1, 2, 3, 4, 5, 6],
      "startTime": "3/31/2026 12:00:00 AM",
      "endTime": "4/1/2026 12:00:00 AM",
      "allDay": false
    },
    "isExcludeSet": false
  },
  "applyCondition": false
}

与其他所有已知条件的结构进行对比,该条件的定义似乎相当直观明了。通过测试得出的观察结果如下:

  • • 为激活该条件,应将 all 设置为 1。
  • • 基于时间的条件包含 included 和 excluded 集合定义,用于确定策略应在何时强制执行。若需启用其中一个或全部启用,还需将 type 设置为 1。
  • • 需要为集合定义一个 timezoneId,该字段应为字符串格式,例如 GMT Standard Time
  • • 对于所选时区,需要选择 startDateTime 和 endDateTime 来界定按小时或按天策略生效的边界范围。这将确定策略的整体 dateRange
  • • 需要定义策略在一周中的哪些 day(天)生效。取值范围从 0(周一)开始,到 6(周日)结束。在此阶段,可以选择任意需要评估的天数。
  • • 需要为每个已包含的 day 定义实际的 startTime 和 endTime,以确定策略在该天的生效时间段。典型例子是标准工作日时间,例如上午 09:00 到下午 05:00。或者,策略也允许将 allDay 键设置为 true,从而使条件在 24 小时内全天生效。

现在,我们对这个新条件有了更清晰的把握。然而,在这一阶段,我仍然感到困惑:这个看似核心的条件为何在网上几乎没有任何信息?不用说,我立刻采取了和其他研究人员一样的行动(由于过于急切地想测试策略效果,我差点封锁了自己的主管理员账户)。不过,在再次体会到应急备用账户的好处之后,我创建了一个名为 [email protected] 的测试用户,以便验证如何利用此条件在选定策略中有效植入“后门”。

五、等等,你是怎么绕过那条策略的?

攻击者通常希望在环境中实现不同级别的持久化,以确保即使初始访问权限被撤销,也能达成其目标。在传统环境中,攻击者会试图禁用某些限制性较强的条件访问策略,以便更轻松地访问环境。然而,禁用核心策略可能产生大量噪音,很可能会触发足够的警报,导致攻击者暴露行踪。

这正是该功能派上用场的地方。在我们开头提到的示例场景中,我们有一条显式阻止的条件访问策略。这条策略在很大程度上代表了组织中常见的安全边界,例如:

  • • 强制要求管理员使用安全密钥登录
  • • 仅允许从公司允许的 IP 地址范围访问租户
  • • 强制要求所有租户访问必须来自合规且受管理的设备

在我们的模拟场景中,攻击者试图利用 [email protected] 账户维持持久访问,但受到这条显式阻止控制的限制。假设攻击者在初始访问阶段已成功获取具有 Conditional Access Administrator(条件访问管理员)权限或更高级别角色的访问权限,他们便可以通过更新特定的条件访问策略,添加如下示例所示的 time 条件,从而绕过该策略:

"time": {
      "all": 1, #启用该条件
      "included": {
        "type": 1, #定义并启用包含集合
        "timezoneId": "GMT Standard Time", #设置时区格式
        "dateRange": {
          "startDateTime": "4/2/2026 12:00:00 AM", #定义条件评估为真的起始日期
          "endDateTime": "4/3/2027 12:00:00 AM" #定义条件不再评估为真的结束日期
        },
        "daysOfWeek": {
          "day": [0,1,2,3,4,5,6], #设置为每周的每一天
          "startTime": "4/2/2026 12:00:00 AM",
          "endTime": "4/2/2026 12:00:00 AM",
          "allDay": true #设置是否全天生效
        },
        "isExcludeSet": false
      },
      "excluded": {
        "type": 1, #定义并启用排除集合
        "timezoneId": "GMT Standard Time", #设置时区格式
        "dateRange": {
          "startDateTime": "4/2/2026 12:00:00 AM", #定义排除条件应被评估的起始日期
          "endDateTime": "4/22/2026 12:00:00 AM" #定义排除条件的结束日期
        },
        "daysOfWeek": {
          "day": [3,4], #设置排除条件在周四和周五生效
          "startTime": "4/2/2026 11:00:00 AM", #设置排除条件从 GMT 时间 11:00 开始
          "endTime": "4/2/2026 01:00:00 PM", #设置排除条件在 GMT 时间 13:00 结束
          "allDay": false
        },
        "isExcludeSet": true #设置使用排除集合
      },
      "applyCondition": true #设置条件为已应用

该策略包含了每周的所有天数,因此大多数情况下策略会正常评估……唯独在接下来的 20 天内,该策略 不会 在每周四和周五的 GMT 时间 11:00 至 13:00 之间被评估。

现在,当我们在 GMT 时间 11:00 至 13:00 期间尝试登录时,显式阻止策略被忽略,我们可以作为 [email protected] 成功进入租户。如下图所示:

查看登录事件(一旦日志生成后),我们可以看到,由于策略中被植入了时间排除后门,这条显式阻止策略 未被 评估:

如果试图查看该策略未被应用的原因,管理员很可能会感到困惑,因为该策略表面上看没有任何未满足的条件:

如果管理员在门户中查看策略定义,他们可能会注意到一些奇怪的蛛丝马迹。例如,他们可能会发现策略显示有三个活跃条件,但门户中仅有两个是可见的:

尝试使用 What if(假设分析)工具验证策略对用户的影响似乎毫无帮助。该工具暗示阻止策略应对该用户强制执行。这可能是因为策略是通过 Ibiza API 应用的,而该工具只能访问 Graph API 所报告的关于策略的信息:

最后,管理员很可能会尝试通过 Microsoft Graph 来验证策略。无论他们是直接使用 Graph Explorer 或自己的脚本,还是通过常用的管理 PowerShell 模块(例如 Microsoft.Entra)进行验证,结果同样无法为当前发生的情况提供合理解释。

尝试使用 Get-EntraConditionalAccessPolicy 命令集会提供另一个线索,表明出现了异常情况:

Get-EntraConditionalAccessPolicy -PolicyId 8d7b8e12-f0e8-4190-a8e5-dd4de9bc0244
Get-MgIdentityConditionalAccessPolicy_Get: 1037: The policy you requested contains preview features. Use the Beta endpoint to retrieve this policy.  Status: 400 (BadRequest) ErrorCode: BadRequest Date: 2026-04-02T09:13:58  [为简洁起见已省略剩余部分]

在花了几分钟安装 Microsoft.Entra.Beta 并运行 Get-EntraBetaConditionalAccessPolicy 后,我们确实得到了结果。但该结果实际上并未显示任何额外信息,更重要的是,其中完全没有策略中存在的 time 条件的任何痕迹。这表明 Graph 所能提供的关于该策略的信息仅限于此:

Get-EntraBetaConditionalAccessPolicy -PolicyId 8d7b8e12-f0e8-4190-a8e5-dd4de9bc0244 | ConvertTo-Json -Depth 100 -EnumsAsStrings

{
  "Conditions": {
    "AgentIdRiskLevels": null,
    "Applications": {
      "ApplicationFilter": {
        "Mode": null,
        "Rule": null
      },
      "ExcludeApplications": [],
      "GlobalSecureAccess": {},
      "IncludeApplications": [
        "All" #策略应用于所有应用程序
      ],
      "IncludeAuthenticationContextClassReferences": [],
      "IncludeUserActions": [],
      "NetworkAccess": {}
    },
    "AuthenticationFlows": {
      "TransferMethods": null
    },
    "ClientAppTypes": [
      "all"
    ],
    "ClientApplications": {
      "AgentIdServicePrincipalFilter": {
        "Mode": null,
        "Rule": null
      },
      "ExcludeAgentIdServicePrincipals": null,
      "ExcludeServicePrincipals": null,
      "IncludeAgentIdServicePrincipals": null,
      "IncludeServicePrincipals": null,
      "ServicePrincipalFilter": {
        "Mode": null,
        "Rule": null
      }
    },
    "DeviceStates": {
      "ExcludeStates": null,
      "IncludeStates": null
    },
    "Devices": {
      "DeviceFilter": {
        "Mode": null,
        "Rule": null
      },
      "ExcludeDeviceStates": null,
      "ExcludeDevices": null,
      "IncludeDeviceStates": null,
      "IncludeDevices": null
    },
    "InsiderRiskLevels": null,
    "Locations": {
      "ExcludeLocations": [],
      "IncludeLocations": [
        "All" #包含所有位置
      ]
    },
    "Platforms": {
      "ExcludePlatforms": [],
      "IncludePlatforms": [
        "all" #包含所有设备类型
      ]
    },
    "ServicePrincipalRiskLevels": null,
    "SignInRiskLevels": [],
    "UserRiskLevels": [],
    "Users": {
      "ExcludeGroups": [],
      "ExcludeGuestsOrExternalUsers": {
        "ExternalTenants": {
          "MembershipKind": null
        },
        "GuestOrExternalUserTypes": null
      },
      "ExcludeRoles": [],
      "ExcludeUsers": [],
      "IncludeGroups": [],
      "IncludeGuestsOrExternalUsers": {
        "ExternalTenants": {
          "MembershipKind": null
        },
        "GuestOrExternalUserTypes": null
      },
      "IncludeRoles": [],
      "IncludeUsers": [
        "58845d0a-eccd-4c44-96b4-c8a61fe8e3e9" # 策略应用于一个特定用户
      ]
    }
  },
  "CreatedDateTime": "2026-03-31T21:42:34.9803608Z",
  "DeletedDateTime": null,
  "Description": null,
  "DisplayName": "Block Bad Person",
  "GrantControls": {
    "AuthenticationStrength": {
      "AllowedCombinations": null,
      "CombinationConfigurations": null,
      "CreatedDateTime": null,
      "Description": null,
      "DisplayName": null,
      "Id": null,
      "ModifiedDateTime": null,
      "PolicyType": null,
      "RequirementsSatisfied": null
    },
    "BuiltInControls": [
      "block" #匹配策略时强制执行显式阻止
    ],
    "CustomAuthenticationFactors": [],
    "Operator": "OR",
    "TermsOfUse": []
  },
  "Id": "8d7b8e12-f0e8-4190-a8e5-dd4de9bc0244",
  "ModifiedDateTime": "2026-04-02T08:24:58.4061247Z",
  "SessionControls": {
    "ApplicationEnforcedRestrictions": {
      "IsEnabled": null
    },
    "CloudAppSecurity": {
      "CloudAppSecurityType": null,
      "IsEnabled": null
    },
    "ContinuousAccessEvaluation": {
      "Mode": null
    },
    "DisableResilienceDefaults": null,
    "GlobalSecureAccessFilteringProfile": {
      "IsEnabled": null,
      "ProfileId": null
    },
    "PersistentBrowser": {
      "IsEnabled": null,
      "Mode": null
    },
    "SecureSignInSession": {
      "IsEnabled": null
    },
    "SignInFrequency": {
      "AuthenticationType": null,
      "FrequencyInterval": null,
      "IsEnabled": null,
      "Type": null,
      "Value": null
    }
  },
  "State": "enabled",
  "ObjectId": "8d7b8e12-f0e8-4190-a8e5-dd4de9bc0244",
  "AdditionalProperties": {
    "@odata.context": "https://graph.microsoft.com/beta/$metadata#identity/conditionalAccess/policies/$entity"
  }
}

至此,我们可以有把握地说,攻击者已为自己创造了一个充足的机会窗口。即使其初始访问途径被阻断,他们也能重新进入租户。而不幸的是,如果管理员在某些情况下注意到特定时间段内的登录事件出现异常行为,他们很可能会对正在发生的事情感到困惑不解。

六、我能花点时间修复这个问题吗?

遗憾的是,答案是否定的。你无法在租户中关闭这一功能,因为它似乎是条件访问服务中相当核心的一部分。目前看来,查看确切策略的最可靠方法就是使用本文中展示的 Ibiza API。请注意,只要用户在租户中拥有 Conditional Access Administrator(条件访问管理员)或更高级别的权限,该条件就可能被添加到任何策略中。不过,即便我们无法阻止这种情况发生,至少我们或许可以对此进行监控。

七、如何监控此类问题?

好消息是,至少在策略中添加 time 条件的更改行为仍会记录在审计日志中!唯一的问题是,由于结构原因,查看变更的 Update policy(更新策略)事件解析起来有些困难。不过,我尝试(在一位友好机器人的协助下)编写了两个 KQL 查询,以帮助组织解析其日志中的相关事件,从而判断策略是否被添加了 time 条件。

第一个查询会显示所有的 Update policy(更新策略)事件,并尝试使其更易于人工阅读。这样管理员便能轻松查看策略的新旧状态:

AuditLogs
| where OperationName == "Update policy"
| extend InitiatedBy_UPN = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatedBy_App = tostring(InitiatedBy.app.displayName)
| extend Actor = iff(isnotempty(InitiatedBy_UPN), InitiatedBy_UPN, InitiatedBy_App)
| extend TargetPolicy = tostring(TargetResources[0].displayName)
| extend TargetPolicyId = tostring(TargetResources[0].id)
| mv-expand ModifiedProp = TargetResources[0].modifiedProperties
| extend
    PropertyName  = tostring(ModifiedProp.displayName),
    OldValue      = tostring(ModifiedProp.oldValue),
    NewValue      = tostring(ModifiedProp.newValue)
// 去除转义后的 JSON 包装引号,使值显示为干净的 JSON 格式
| extend
    OldValue = iff(OldValue startswith '"' and OldValue endswith '"',
                   substring(OldValue, 1, strlen(OldValue) - 2),
                   OldValue),
    NewValue = iff(NewValue startswith '"' and NewValue endswith '"',
                   substring(NewValue, 1, strlen(NewValue) - 2),
                   NewValue)
| project
    TimeGenerated,
    Actor,
    OperationName,
    Result,
    TargetPolicy,
    TargetPolicyId,
    PropertyName,
    OldValue,
    NewValue,
    CorrelationId
| sort by TimeGenerated desc, CorrelationId asc, PropertyName asc

以下 KQL 查询旨在仅标记出其中包含 time 条件的 Update policy(更新策略)事件。该查询通过在日志条目中查找字符串“TimeRanges”来实现,但在大规模运行时可能会稍慢一些。我猜想经验丰富的 KQL 用户可能能够优化它,但我确实希望确保安全团队至少能有一些工具用于快速威胁狩猎。因此,我还是将其包含在此处:

AuditLogs
| where OperationName == "Update policy"
| extend InitiatedBy_UPN = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatedBy_App = tostring(InitiatedBy.app.displayName)
| extend Actor = iff(isnotempty(InitiatedBy_UPN), InitiatedBy_UPN, InitiatedBy_App)
| extend TargetPolicy = tostring(TargetResources[0].displayName)
| extend TargetPolicyId = tostring(TargetResources[0].id)
| mv-expand ModifiedProp = TargetResources[0].modifiedProperties
| extend
    PropertyName  = tostring(ModifiedProp.displayName),
    OldValue      = tostring(ModifiedProp.oldValue),
    NewValue      = tostring(ModifiedProp.newValue)
| where NewValue has "TimeRanges"
// 去除双重序列化的包装引号以便更清晰地显示
| extend
    OldValueClean = iff(OldValue startswith '"' and OldValue endswith '"',
                        substring(OldValue, 1, strlen(OldValue) - 2),
                        OldValue),
    NewValueClean = iff(NewValue startswith '"' and NewValue endswith '"',
                        substring(NewValue, 1, strlen(NewValue) - 2),
                        NewValue)
| extend NewValueJson = parse_json(parse_json(NewValueClean))
| project
    TimeGenerated,
    Actor,
    OperationName,
    Result,
    TargetPolicy,
    TargetPolicyId,
    PropertyName,
    OldValue  = OldValueClean,
    NewValue  = NewValueClean,
    CorrelationId
| sort by TimeGenerated desc, CorrelationId asc

八、事件时间线

Reversec 与微软进行了接洽。以下是该案例处理过程的简要时间线:

  • • 2025 年 3 月 2 日 – 向 MSRC 提交报告
  • • 2025 年 3 月 3 日 – 分配案例编号
  • • 2025 年 3 月 3 日 – Reversec 补充了关于无法使用标准方法观察策略的额外背景信息
  • • 2025 年 3 月 11 日 – Reversec 请求更新进展
  • • 2025 年 3 月 11 日 – MSRC 回复称正在调查中
  • • 2025 年 3 月 28 日 – Reversec 请求更新进展
  • • 2025 年 4 月 21 日 – Reversec 请求更新进展
  • • 2025 年 4 月 28 日 – MSRC 确认该行为符合预期设计,不被视为安全问题。

完整回复如下:

再次感谢您向微软提交此问题。希望您一切顺利,并对此次的延迟表示歉意。我们确定此行为并非漏洞。因为基于时间的条件是 Conditional Access(CA,条件访问)中的一项安全功能,旨在根据一天中的时间或日期来限制对资源的访问。此功能目前处于私有预览阶段。我们不认为发现和使用基于时间的条件属于安全漏洞,原因如下:1) 它只能由 CA Administrators(条件访问管理员)启用;2) 向现有或新的 CA 策略添加基于时间的条件始终会记录在 CA 审计日志中。这些日志对租户内所有具有相应权限的管理员均可见。

总体而言,综合考虑各方面因素,与 MSRC 打交道的处理时间线还不算太糟。我也理解他们的观点,因为从技术上讲,这确实符合预期行为。然而,我仍然认为该条件缺乏可见性是一个非常现实的问题。即使审计日志事件包含了策略更新和 time 条件,但要解析这条日志条目并将其用于告警(以防有人真的在企业环境中利用它)也是极具挑战性的。

九、结论

条件访问策略是组织花费大量时间和精力才能正确配置的机制之一。因此,管理员和安全专业人员了解可应用于特定策略的所有可能条件至关重要。恶意滥用这种基于时间的条件确实需要相当高的权限。所以,只要组织将 Conditional Access Administrators(条件访问管理员)视为高权限角色,可被攻击者盯上的用户数量就可能有限,并且这些用户应受到其他附加控制的保护。

我能看到基于时间的条件对于某些企业的价值所在。例如,某些企业可能希望只允许在特定工作时间内访问其系统。我完全支持为组织提供能够使其日常工作更高效、更安全的工具。然而,我确实希望微软能优先考虑至少在 Graph Beta 端点中也公开详细的策略信息。这样一来,管理员就能更轻松地弄清楚特定策略中究竟定义了哪些内容,而无需针对这一特定场景去依赖 Ibiza API。即使某个功能处于私有预览阶段,当该功能自动出现在向全球所有租户提供的核心服务中时,也应保持一定程度的可见性一致性。

引用链接

[1] 《It’s Just a Matter of Time: Backdooring Conditional Access Policies》: https://labs.reversec.com/posts/2026/04/its-just-a-matter-of-time-backdooring-conditional-access-policies [2] 发现了基于时间的条件的存在: https://ourcloudnetwork.com/enabling-time-based-restrictions-in-conditional-access/ [3] 通过 Graph API 使其生效: https://ourcloudnetwork.com/configuring-time-based-conditional-access-policies/

交流群


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:云原生安全指北 Dubito Dubito《Entra条件访问策略中的时间后门》

评论:0   参与:  0