quarkslab/QAZPT
GitHub: quarkslab/QAZPT
Quarkslab 出品的 EntraID 权限继承追踪工具,通过图分析揭示用户、应用和服务主体之间的传递性权限与提权路径。
Stars: 2 | Forks: 0
# QAZPT
## 目录
- [什么是 QAZPT?](#what-is-qazpt)
- [功能](#features)
- [应用场景](#use-cases)
- [安装](#installation)
- [用法](#usage)
- [继承](#inheritance)
- [继承树](#inheritance-tree)
- [继承的权限](#inherited-permission)
- [Neo4j 图导出](#neo4j-graph-export)
- [前置条件](#prerequisites)
- [导出](#export)
- [图模式](#graph-schema)
- [实用的 Cypher 查询](#useful-cypher-queries)
- [文档](#documentation)
- [所需权限](#required-permissions)
- [如何计算继承](#how-inheritance-is-computed)
- [直接所有权](#direct-ownership)
- [AppRegistration 的 ServicePrincipal](#serviceprincipal-of-an-appregistration)
- [联合身份凭据](#federated-identity-credentials)
- [EntraID 角色](#entraid-roles)
- [Microsoft Graph 应用角色](#microsoft-graph-application-roles)
- [如何计算传递性权限](#how-transitive-permissions-are-computed)
- [局限性与未来工作](#limitations-and-future-work)
- [与其他工具的比较](#comparison-with-other-tools)
## 什么是 QAZPT?
QAZPT(Quarkslab Azure Permission Tracker)是一款旨在分析和跟踪权限——EntraID 角色、应用角色和委派权限——在 Microsoft EntraID(原 Azure Active Directory)环境中通过直接或间接方式进行继承的工具。目前,它未将 Azure Resource Manager (ARM) 纳入考虑范围。它有助于识别权限如何在用户、应用程序和服务主体等各种实体之间传播,从而使安全团队能够更好地理解和管理访问控制。
**注意:** QAZPT 仍在开发中(WIP),因此应谨慎解读其结果。此外,继承的计算非常复杂,无法涵盖所有边缘情况。有关如何计算继承及其局限性的更多详细信息,请参阅文档。
### 功能
- 分析并可视化 EntraID 实体(用户、AppRegistration、ServicePrincipal)之间的继承关系,揭示复杂的关系和潜在的权限提升路径。
- 识别并可视化通过继承获得的权限,帮助检测权限过高的实体。
- 以 CSV 和 JSON 格式导出详细报告以供进一步分析,其中包含实体、其自身权限以及继承的权限。
- 将完整的图谱导出到 **Neo4j** 数据库,以便进行交互式探索和高级 Cypher 查询。
### 应用场景
QAZPT 可以协助 EntraID 环境中的攻防安全工作:
- 通过揭示每个实体的有效权限来评估 EntraID 环境的安全态势
- 识别跨 EntraID 实体的潜在权限提升路径
- 确保没有实体获得意外权限,并且访问控制已正确配置
- 通过梳理继承的权限,协助检测后渗透的持久化技术
- 随时间推移监控和审计权限变更,通过对比报告来检测异常
## 安装
```
uv sync
uv run qazpt --help
```
或使用 pip:
```
pip install .
qazpt --help
```
## 用法
```
qazpt --help
usage: QAZPT [-h] [-v] [-d] {inheritance} ...
positional arguments:
{inheritance}
inheritance Analyze inheritance
options:
-h, --help show this help message and exit
-v, --verbose
-d, --debug Enable debug mode (more verbose)
-C CACHE_DIR, --local-cache-dir CACHE_DIR
Use the specified local directory for caching data or load cached data. Default is ~/.qazpt_cache
```
### 继承
```
usage: QAZPT inheritance [-h] [-t | -p | -g DB_NAME | -c CSV_NAME | --as-json [JSON_PATH] | --as-neo4j] [--neo4j-uri BOLT_URI] [-A] [-P] [-f TYPE,TYPE,...] [-x TYPE,TYPE,...]
[--hide-delegated-permissions] [--neo4j-user USERNAME] [--neo4j-password PASSWORD] [-r] [--legacy-dfs] [--direct-origin] [--true-origin]
options:
-h, --help show this help message and exit
-t, --inheritance-tree
[Default] Output inheritance as trees
-p, --show-gained-permissions
Output entities with their gained permissions
-c, --as-csv CSV_NAME
Output entities, permissions and indirects permissions gained through inheritance as CSV
--as-json [JSON_PATH]
Output entities, permissions and indirects permissions gained through inheritance as JSON file if specified, else to stdout
--as-neo4j Export the graph to a Neo4j database.
--neo4j-uri BOLT_URI Bolt URI of the Neo4j instance (used with --as-neo4j, default: bolt://127.0.0.1:7687)
-A, --show-all When analyzing inheritance, show also entities without any permissions
-P, --privileged-only
When analyzing inheritance, only show privileged entities
-f, --filter-by-type TYPE,TYPE,...
When analyzing inheritance, only show entities of type included in the comma separated list. Available types are: User, ServicePrincipal,
ManagedIdentityServicePrincipal, AppRegistration
-x, --exclude-by-type TYPE,TYPE,...
Same as --filter-by-type but exclude the types instead of including them
--hide-delegated-permissions
When analyzing inheritance, do not show delegated permissions
--neo4j-user USERNAME
Neo4j username (used with --as-neo4j, default: neo4j)
--neo4j-password PASSWORD
Neo4j password (used with --as-neo4j, default: qazpt)
-r, --refresh When analyzing inheritance, refresh the data
--legacy-dfs When computing inheritance, use legacy cycle-safe DFS algorithm instead of DAG condensation algorithm (default is condensed). Computation is faster butwill
produce non-exhaustive results in cyclic graphs. Use this flag if your environment is acyclic or if you encounter issues with the condensation algorithm.
--direct-origin When distributing condensed inheritance, inherited permissions are shown with their direct neighbors as the origin. If A(X)->B(Y)->C(Z), A will get (YZ)
with B as origin.Use this flag if you prefer to see the path of inheritance more explicitly.Using this flag may cause more verbose outputs in graphs with a
lot of strongly connected components (several nodes with cyclic inheritance).This flag is mutually exclusive with --true-origin.
--true-origin [Default] When distributing condensed inheritance, inherited permissions are shown with their original owners as the origin. If A(X)->B(Y)->C(Z), A will get (Y) with
B as origin and (Z) with C as origin.Use this flag if you prefer to see the original source of inherited permissions.Showing true origin permissions may
result in more verbose outputs in graphs with populated strongly connected components.This flag is mutually exclusive with --direct-origin. This is the
default behavior.
```
#### 继承树
要打印 EntraID 实体的继承树,请使用 `--inheritance-tree` 选项,这是默认行为:
```
qazpt inheritance
### 用户 ###
alice/alice@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000001
--[ownership]--> AppRegistration/general_admin_app/00000000-0000-0000-0000-000000000002
--[sp_of_app]--> ServicePrincipal/general_admin_app/00000000-0000-0000-0000-000000000003 [HAS PRIVILEGED ROLE]
--[Entraid_Role (Global Administrator)]--> All entities
### 应用程序 ###
AppRegistration/general_admin_app/00000000-0000-0000-0000-000000000002
--[sp_of_app]--> ServicePrincipal/general_admin_app/00000000-0000-0000-0000-000000000003 [HAS PRIVILEGED ROLE]
--[Entraid_Role (Global Administrator)]--> All entities
```
添加 `-P, --privileged_only` 以仅显示在 EntraID 中拥有特权内置角色的实体。
#### 继承的权限
使用 `--direct-origin` 可将直接相邻实体显示为继承权限的来源。默认情况下,会显示继承权限的原始来源(例如所有者)。
要获取有效的**已获得**的继承权限:
```
qazpt inheritance --show-gained-permissions [--hide-delegated-permissions]
### 用户 ###
alice/alice@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000001
Entra ID Role :
- Global Administrator inherited from AppRegistration/general_admin_app/00000000-0000-0000-0000-000000000002
### ServicePrincipal ###
ServicePrincipal/AppTest_userpasswordprofile_readwrite/00000000-0000-0000-0000-000000000004
Entra ID Role :
- Global Administrator inherited from bob/bob@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000005 [HAS PRIVILEGED ROLE]
- Global Administrator inherited from alice/alice@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000001
AppRoleAssignments :
- Graph/Toto.Titi inherited from bob/bob@contoso.onmicrosoft.com/00000000-0000-0000-0000-000000000005 [HAS PRIVILEGED ROLE]
```
已经具有高特权的实体——即那些持有 EntraID 角色或 Microsoft Graph 权限,使其能够控制所有其他实体的实体——将不会出现在此处,因为它们实际上不会获得任何新权限,并且不会计算它们的继承权限。
如果实体从高特权实体继承,则继承的权限将仅限于该高特权实体自身的权限。
要获取实体、其自身权限及其继承权限的完整列表,可以使用 `--as-csv` 将数据导出为 CSV 文件,或使用 `--as-json` 选项导出为 JSON 格式。请注意,如果没有为 JSON 输出提供文件路径,它将被打印到标准输出。
```
qazpt inheritance --as-csv inheritance_output.csv
```
```
qazpt inheritance --as-json [file_name.json]
```
#### Neo4j 图导出
QAZPT 可以将完整的继承图导出到 Neo4j 数据库以进行交互式探索。
##### 前置条件
启动一个 Neo4j 实例。项目根目录下提供了一个 `docker-compose.yml`:
```
docker compose up -d
```
这将启动 Neo4j 5,其 Browser UI 位于 ,Bolt 端点位于 `bolt://127.0.0.1:7687`(凭据:`neo4j` / `qazpt`)。
##### 导出
```
qazpt inheritance --as-neo4j
```
默认情况下,没有权限且没有具体所有权/控制链接的 ServicePrincipal 将被省略,以保持图表的可读性。传递 `--show-all` 可包含它们:
```
qazpt inheritance --as-neo4j --show-all
```
连接选项(所有选项都有与提供的 `docker-compose.yml` 匹配的默认值):
| 选项 | 默认值 | 描述 |
| --- | --- | --- |
| `--neo4j-uri` | `bolt://127.0.0.1:7687` | Neo4j 实例的 Bolt URI |
| `--neo4j-user` | `neo4j` | Neo4j 用户名 |
| `--neo4j-password` | `qazpt` | Neo4j 密码 |
**注意:** 每次导出都会通过仅删除带有 QAZPT 标签(`User`、`ServicePrincipal`、`ManagedIdentityServicePrincipal`、`AppRegistration`、`EntraIDRole`、`AppRole`、`DelegatedPermission`)的节点来清除先前的 QAZPT 数据。同一数据库中的其他节点和数据不受影响。
与其他筛选器结合使用:
```
# 仅导出 Users 和 ServicePrincipals
qazpt inheritance --as-neo4j --filter-by-type User,ServicePrincipal
# 仅显示特权实体
qazpt inheritance --as-neo4j --privileged-only
```
##### 图模式
**实体节点** —— 每种类型一个标签,使用 `--filter-by-type` / `--exclude-by-type` 过滤:
| 标签 | 关键属性 |
| --- | --- |
| `:User` | `id`、`display_name`、`user_principal_name`、`scope_level` |
| `:ServicePrincipal` | `id`、`display_name`、`app_id`、`sp_type`、`scope_level` |
| `:ManagedIdentityServicePrincipal` | `id`、`display_name`、`app_id`、`scope_level` |
| `:AppRegistration` | `id`、`display_name`、`app_id`、`scope_level` |
`scope_level` 属性反映了实体的直接特权级别(例如 `SERVICE_PRINCIPAL`、`USER`、`ALL`、`NONE_OR_DEFAULT`),由 QAZPT 的继承解析器计算得出。
**权限节点:**
| 标签 | 关键属性 |
| --- | --- |
| `:EntraIDRole` | `id`、`display_name`、`is_privileged`、`is_built_in` |
| `:AppRole` | `id`、`display_name`、`value`、`resource_display_name`、`resource_id` |
| `:DelegatedPermission` | `id`、`scope`、`resource_id`、`resource_display_name`、`principal_id`、`principal_display_name` |
**关系:**
| 关系 | 含义 | 属性 |
| --- | --- | --- |
| `CONTROLS` | 具体的继承链接(所有权、应用的 SP、FIC 等) | `type`、`detail` |
| `HAS_ENTRAID_ROLE` | 实体直接持有 EntraID 角色 | `resource_scope`、`directory_scope` |
| `HAS_APP_ROLE` | 实体持有 Microsoft Graph 应用角色 | — |
| `HAS_DELEGATED_PERMISSION` | 实体持有委派的 OAuth2 权限 | `consent_type` |
##### 实用的 Cypher 查询
按名称搜索(不区分大小写):
```
MATCH (e) WHERE toLower(e.display_name) CONTAINS toLower('alice') RETURN e
```
具有特定 EntraID 角色的所有实体(直接持有者):
```
MATCH (e)-[:HAS_ENTRAID_ROLE]->(r:EntraIDRole {display_name: 'Global Administrator'})
RETURN e.display_name, e.entity_type
```
所有通向全局管理员 (Global Administrator) 的实体(直接 + 通过具体链接继承 + 通过虚拟范围):
```
// Direct holders
MATCH p=(e)-[:HAS_ENTRAID_ROLE]->(r:EntraIDRole {display_name: 'Global Administrator'})
RETURN p
UNION
// Via concrete inheritance chain
MATCH p=(e)-[:CONTROLS*1..]->(target)-[:HAS_ENTRAID_ROLE]->(:EntraIDRole {display_name: 'Global Administrator'})
RETURN p
```
通向全局管理员 (Global Administrator) 的最短路径:
```
MATCH (target)-[:HAS_ENTRAID_ROLE]->(r:EntraIDRole {display_name: "Global Administrator"})
MATCH (e) WHERE NOT (e)-[:HAS_ENTRAID_ROLE]->(r) AND e <> target
WITH e, r, target
MATCH p = allShortestPaths((e)-[:CONTROLS*1..6]->(target))
WHERE NONE(n IN nodes(p)[1..-1] WHERE (n)-[:HAS_ENTRAID_ROLE]->(r))
MATCH q = (target)-[:HAS_ENTRAID_ROLE]->(r)
RETURN p, q
```
具有提升的直接范围级别的实体:
```
MATCH (e) WHERE e.scope_level IN ['USER', 'APPLICATION', 'ALL']
RETURN e.display_name, e.entity_type, e.scope_level ORDER BY e.scope_level
```
通向具有提升的直接范围级别实体的最短路径:
```
MATCH q = (target {scope_level: "ALL"})-[:HAS_APP_ROLE|:HAS_ENTRAID_ROLE]->(r)
MATCH (e) WHERE NOT e.scope_level = "ALL" AND e <> target
WITH e, target, q
MATCH p = allShortestPaths((e)-[:CONTROLS*1..6]->(target))
WHERE NONE(n IN nodes(p)[1..-1] WHERE n.scope_level = "ALL")
RETURN p, q
```
## 文档
### 所需权限
为了正确分析继承,QAZPT 需要足够的权限才能读取目标 EntraID 环境中的实体及其权限。
最低限度的 Microsoft Graph 所需权限包括:
- `User.Read.All`
- `Application.Read.All`
- `RoleManagement.Read.Directory`
也可以使用 `Directory.Read.All` 来涵盖所有必需的读取权限。
请注意,目录中的每个成员用户都可以读取大部分目录内容并成功委派此权限。使用访客帐户或服务主体将需要显式授予上述权限。
### 如何计算继承
继承是借助于位于 `qazpt/core/graph/resolvers/` 中的解析器 (`InheritanceResolverRule`) 来计算的。
这些解析器负责确定实体之间的关系以及权限是如何继承的。
目前,解析器在计算每个实体的有效权限之前,会执行一次传递以建立继承关系。虽然这种方法确保了树状视图的准确性,但它没有考虑到在初始计算之后可能从 EntraID 或 Microsoft Graph 权限中出现的潜在新继承路径。
Tarjan 算法的使用通过识别图中的循环并将其压缩为单个节点来解决此问题。这确保了即使在具有循环继承的图中,也能详尽地计算传递性权限。通过将循环视为统一实体,该算法能够更精确、更完整地计算有效权限。
目前,使用 5 个解析器来查找实体之间的继承关系:
#### 直接所有权
一个实体被设置为另一个实体的所有者(例如,用户是 AppRegistration 的所有者)
#### AppRegistration 的 ServicePrincipal
ServicePrincipal 是 AppRegistration 的活动实例。拥有 AppRegistration 意味着您可以控制其相应的 ServicePrincipal。
#### 联合身份凭据
配置了联合身份凭据 (FIC) 的应用程序允许已配置的、潜在的外部身份提供者请求该应用程序的 token。因此,任何能够针对已配置的身份提供者进行身份验证的身份,都有可能获得对该应用程序及其权限的访问权。
#### EntraID 角色
多个 EntraID 角色可以控制不同类型的实体:
- 全局管理员 (Global Administrator):所有实体
- 应用程序管理员 (Application Administrator):所有 AppRegistration 和 ServicePrincipal(并同意非 Microsoft Graph 的应用角色)
- 云应用程序管理员 (Cloud Application Administrator):与上述相同,但不适用于应用代理
#### Microsoft Graph 应用角色
多个 Microsoft Graph 应用角色能够控制不同类型的实体。
- RoleManagement.ReadWrite.Directory:所有实体
- 此权限可以分配 EntraID 角色(例如全局管理员),并修改 PIM 配置。
- AppRoleAssignment.ReadWrite.All:所有实体
- 此权限可以为任何其他实体分配任何其他应用角色,而无需管理员同意。它可用于分配 `RoleManagement.ReadWrite.Directory`,后者又可用于分配全局管理员角色。
- User-PasswordProfile.ReadWrite.All:通过更改密码控制用户。仅在不强制执行 MFA 时适用。
- User.ReadWrite.All:部分用户。可用于编辑权限较低的用户配置文件。(不明确)
- UserAuthMethod-Passkey.ReadWrite.All:所有用户。
- 此权限可用于为任何用户添加通行密钥作为身份验证方式。通行密钥允许在没有密码和 2FA 的情况下进行身份验证。
- UserAuthenticationMethod.ReadWrite.All:与上述相同,但包括所有其他身份验证方式。
- Application.ReadWrite.All:所有应用程序和 ServicePrincipal。
- Application.ReadUpdate.All:与上述相同。区别在于此权限不允许创建新应用程序。
- ServicePrincipalEndpoint.ReadWrite.All:所有 ServicePrincipal。
### 如何计算传递性权限
在通过不同解析器链接节点后,将使用深度优先搜索 (DFS) 和循环安全方法 (`--legacy-dfs`) 分析图以计算每个实体的有效权限,或者利用 Tarjan 算法将图压缩为有向无环图 (DAG),然后执行自下而上的遍历以计算继承的权限。
每个节点都会继承其子节点的权限,除非该节点已经具有高特权(例如,拥有 EntraID 角色或 Microsoft Graph 权限,使其能够控制所有其他实体)。
## 局限性与未来工作
- 尚不支持组。但是,会考虑通过组获得的传递性 EntraID 角色。
- 尚未实现对 Azure Resource Manager (ARM) 权限和继承的支持,因此不会为托管标识和用户分配的托管标识计算通过 ARM RBAC 进行的继承。
- 尚不支持 EntraID 角色范围(EntraID v1/v2)。
- 尚不支持管理单元(EntraID v1/v2)。
- 尚不支持 PIM(Privileged Identity Management)(traID v1/v2)。
- **联合身份凭据 (FIC):** 目前仅支持 INTERNAL(内部)身份。尚不支持外部身份提供者(例如 GitHub、Google)。
- **EntraID 角色覆盖范围:** 该工具仅涵盖评估中常用的一部分高特权角色。存在其他具有安全影响的角色,但尚未包含在内。
## 与其他工具的比较
您可能会想,既然已经有诸如 [Bloodhound (With AzureHound)](https://github.com/BloodHoundAD/BloodHound)、[ROADtools](https://github.com/dirkjanm/ROADtools)、[ScoutSuite](https://github.com/nccgroup/ScoutSuite) 等能够分析 EntraID 环境中权限的工具,为什么还要使用 QAZPT?
首先,QAZPT 并不旨在取代这些工具,而是一个专注于 EntraID 安全特定方面的补充工具:权限继承。虽然其他工具可能提供更广泛的功能和分析,但 QAZPT 旨在对权限继承提供更深入、更全面的分析,这是 EntraID 安全中经常容易被忽视的一个关键方面。
目前似乎没有其他工具对 EntraID 环境中的权限继承提供深入分析,特别是在不同的继承路径(所有权、FIC、EntraID 角色、Microsoft Graph 权限滥用)以及通过继承获得的有效权限方面。QAZPT 通过提供一个专门用于分析和理解 EntraID 环境中权限继承的专用工具填补了这一空白。
标签:Azure, Azure Active Directory, Cypher查询, IAM, Microsoft Entra ID, Neo4j, Quarkslab, Streamlit, 企业安全, 安全合规, 攻击面分析, 权限分析, 权限管理, 权限继承, 模型越狱, 网络代理, 网络安全, 网络资产管理, 访问控制, 请求拦截, 身份与访问管理, 逆向工具, 隐私保护