只读 Entra ID 应用凭据评估:枚举 Graph 权限、Azure RBAC 和可访问的云数据,然后将发现映射到权限提升和横向移动路径。
/ / ______ ___ ___ / /_ / / / /__ _ / / /_____ ____ \ / -) / -) -)/ / \ \ / __/ _ `// / '/ -) / //_/_/_/_/ _/ // _/_,////_\__/_/
╔╦╦╬╬╬╬╬╬╦╦╗
╔╬╬╬╝╝┘ ╚╝╝╬╬╬┐
╬╬╝╚╩╬╗╔ ╚╬╬╬
╬╝ ╚╬╬╗╗ ╔ ╚╬╗ ╬╬ ╔╗ ╚╬╬╬╬╬╬╦ ╬╬ we found your secret... ╔╬┤ ╬╬╬ ╬╬╬╬╬╬╬╬╝╝╝╬╬╗ ...now let's see what it ╬╬┤ ╚╩┘ ╚╬╬╬╬╬╩ ╠╬╬ can REALLY do. ( o_o)>=|= ╬╬┤ ╠╬╬ ╬╬ ╦╗ ╗╗ ╬╬ [ client_id + secret -> total recall ] └╬┐ ╚╬╗╗ ╔╬╬╝ ╔╬┘ └╬╗ ╚╩╩╬╬╬╩╩╝╝ ╔╬╬ ╚╬╬╬╗ ┌╗╬╬╝┘ ╚╩╬╬╬╦╦╦╦╦╦╬╬╬╝╝ ╚╚╝╝╝╝ // pst... that app registration talks too much. \
**这个 Entra ID 客户端 ID + 客户端机密实际上能做什么?**
你发现了一个 Entra ID(Azure AD)应用程序凭据 — 一个客户端 ID 和客户端机密 —
它来自一次授权评估,并且其所属租户在范围内。
`secret_stalker` 获取这两个值,并从零开始告诉你:
1. **它是否有效,机密何时过期?** — 并且如果无效,*为什么*
(错误的机密、已过期的机密、应用不在租户中…)。对于有效的机密,它
会读取应用注册的 `passwordCredentials` 并报告过期
日期 + 剩余天数(需要目录读取;见下方说明)。
2. **它拥有哪些 Microsoft Graph 权限?** — 应用程序权限直接读取自
颁发的令牌,加上**它所持有的 Entra 目录角色**(甚至
被动地从令牌的 `wids` 声明中检测到)以及**它拥有的对象**
(应用/SP,你可以为其添加凭据)。
3. **它对 Azure 有什么控制权?** — RBAC 角色分配在
管理组和订阅范围。
4. **它能接触到真实数据吗?** — 可选的 Key Vault(机密 / 密钥 / 证书),
Storage(blob / file / queue / table)以及 Cosmos DB 数据平面可达性检查。
5. **影响是什么?** — 危险的权限、角色、所有权,以及可触达的
数据映射到已知的权限提升 / 横向移动原语,按严重性评级,
并附有具体的**攻击路径**说明。
它可以使用**客户端机密**或**证书**(`--cert`)进行身份验证,
并且可用于**商业云和主权云**(`--cloud`)。
它**默认为被动模式**,并且**从不修改任何内容** — 仅进行只读
枚举。
> ⚠️ **仅限授权测试。** 请仅针对那些
> 明确在你获授权执行的项目范围内的租户运行。
---
## 安装```bash
pip install -r requirements.txt # just runs it from source
# — or —
pip install . # installs the `secret_stalker` command
pip install '.[cert]' # + certificate (--cert) auth support
pip install '.[dev]' # + pytest for the test suite
唯一的运行时依赖是 requests。令牌在本地解码(base64 + JSON)——不涉及签名验证、加密库或 Microsoft SDK。唯一的例外是证书认证(--cert),它需要可选的 cryptography 包来签署 JWT 客户端断言。需要 Python 3.7+。
执行 pip install . 之后,你可以用 secret_stalker … 来调用它,而不用再写 python -m secret_stalker …。
找出一个凭据能做什么的最快方式:```bash
python -m secret_stalker
--tenant contoso.onmicrosoft.com
--client-id 11111111-2222-3333-4444-555555555555
--secret ''
`--tenant` 接受租户 GUID 或域名 — 域名会通过公共 OpenID 配置端点自动解析为其租户 ID。
### 让机密远离 shell 历史记录
通过环境变量而不是标志传递凭据:```bash
export SS_TENANT=contoso.onmicrosoft.com
export SS_CLIENT_ID=11111111-2222-3333-4444-555555555555
export SS_SECRET='<client-secret>'
python -m secret_stalker
--tenant / --client-id / --secret 中的任意一个都可以来自 SS_TENANT /
SS_CLIENT_ID / SS_SECRET。命令行标志优先于环境变量。
这不仅与 shell 历史记录有关:argv 值在进程的整个生命周期内可被任何本地
用户读取(ps、/proc/<pid>/cmdline)。如果 --secret
或 --cert-password 作为标志传入,工具会打印一行提醒到
stderr — 该值绝不会出现在 --json 或 --export 输出中。
应用注册通常使用证书而非机密。传递 --cert
(一个包含私钥 和 证书的 PEM,或一个 .pfx/.p12),并且
工具将使用带签名的 JWT 客户端断言进行身份验证:```bash
python -m secret_stalker --tenant contoso.onmicrosoft.com
--client-id --cert ./app.pem # or app.pfx
python -m secret_stalker ... --cert app.pfx --cert-password ''
证书认证需要可选的 `cryptography` 包(`pip install '.[cert]'`)。
该工具报告证书自身的有效期(根据应用 `keyCredentials` 中的指纹进行匹配),与处理机密的方式相同。`--cert`/`--cert-password` 也会从 `SS_CERT` / `SS_CERT_PASSWORD` 中读取。
### 主权云和政府云
默认情况下,secret_stalker 以**商业**云为目标。对于主权租户,请传递 `--cloud`(或 `SS_CLOUD`),以便 Entra 颁发机构以及 Graph / ARM / Key Vault 端点匹配 — 否则有效的凭据看起来就像没有访问权限:```bash
# US Government (GCC High)
python -m secret_stalker --cloud usgov --tenant contoso.onmicrosoft.us ...
# US DoD (L5)
python -m secret_stalker --cloud usdod ...
# Azure operated by 21Vianet (China)
python -m secret_stalker --cloud china --tenant contoso.partner.onmschina.cn ...
接受 gov、dod、commercial、gcc-high 和 21vianet 等别名。
(Storage 数据平面的受众 storage.azure.com 在所有云中均相同。)
这会进行身份验证,将租户的 Graph appRole 映射缓存到
~/.secret_stalker/app_roles_cache.json,然后退出。如果该凭据
无法读取服务主体 — 内置映射仍然覆盖了
众所周知的权限。
Credential status : VALID Tenant : aaaaaaaa-... Client (app) id : 1111... App display name : Recon App SP object id : cccc... Secret : valid — expires 2027-03-01 (in 207 days)
OK graph OK arm NO storage — no storage token
...
[CRITICAL] (GRAPH) Application.ReadWrite.All Can add credentials to any app/SP and impersonate it — tenant-wide pivot. [CRITICAL] (ARM) Owner Full control including granting access to others. [CRITICAL] (DATA) keyvault:secrets Can read Key Vault secret values — connection strings, passwords, tokens. [MEDIUM] (GRAPH) Mail.Read Read all mailboxes — data exposure.
Overall risk: CRITICAL
- **令牌获取(Token acquisition)** 列出每个探测到的受众(Graph、ARM,以及在
`--active` 下发现匹配资源时的 Key Vault / Storage / Cosmos DB)。Graph 与
ARM 是*相互独立*的:一个凭据可以持有其中之一而不持有另一个。
- **机密(Secret)** 显示有效性;对于有效机密,还显示过期日期和剩余天数
(临近过期会高亮显示)。关于过期机密,请参阅下面的说明。
- **发现项(Findings)** 是最先要阅读的部分——高影响的 Graph 权限(`GRAPH`)、
ARM 角色(`ARM`)、Entra 目录角色(`ROLE`)、拥有的应用/服务主体(`OWN`)、
可达的数据平面(`DATA`),以及已请求但未获同意的同意攻击目标(`WANT`)——
经去重并按严重性评级。即使没有危险的 Graph/ARM 授权,能够读取每个
Key Vault 机密或持有目录角色本身也是一项发现项。
- **攻击路径(Attack paths)** 将最重要的发现转成具体的后续步骤(例如
*Privileged Role Administrator → 为自己分配 Global Administrator → 租户接管*)。
- **Active Graph 枚举**(`--active`)报告每个只读探测返回的结果。大多数探测
请求的是小而有上限的分页,因此已满的页面会显示为 `N+`(例如
`users accessible (returned 5+)`)——表示*至少*五个,而非正好五个。
没有上限的探测(`organization`、`directoryRoles`)会报告不带 `+` 的
真实总数。
- **目录角色 / 拥有的对象 / 委派权限** 各自拥有独立的小节。即使没有目录读取
权限,也会根据令牌的 `wids` 声明检测目录角色;委派权限无法被仅应用(app-only)
凭据使用,但会显示出来,用于用户上下文 pivot 和同意攻击目标定位。
- **整体风险(Overall risk)** 是单项发现中最高的严重性等级。
> **机密过期——哪些信息可知。** 过期日期*不在*令牌中;它存在于 Entra ID 中
> 应用注册的 `passwordCredentials` 里。对于**有效**的机密,secret_stalker 会
> 通过 Graph 读取它,并根据 `hint`(前 3 个字符)将你的机密匹配到正确的
> 凭据——这需要目录读取权限(`Application.Read.All` / `Directory.Read.All`);
> 如果服务主体缺少该权限,日期会报告为不可用,而不会进行猜测。对于
> **已过期**的机密,身份验证本身会失败,因此失效的凭据无法读取其自身的
> 元数据——该工具会将其标记为 `EXPIRED (AADSTS7000222)`,但仅凭该凭据无法
> 检索确切的结束日期。
### 退出码
便于脚本化使用:
| 代码 | 含义 |
|------|---------|
| `0` | 凭据有效(至少获得一个令牌)。 |
| `2` | 凭据无效 / 没有访问权限。 |
| `1` | 错误——无法解析租户、无法加载证书,或无法写入 `--export` 文件。 |
---
## 所有标志
| 标志 | 作用 |
|------|--------|
| `--tenant` | 租户 GUID 或域名。(或 `SS_TENANT`) |
| `--cloud` | Azure 云:`public`(默认)、`usgov`(GCC High)、`usdod`(DoD)、`china`(21Vianet)。选择 Entra 颁发机构以及 Graph/ARM/Key Vault 端点。接受 `gov`/`dod`/`commercial` 等别名。(或 `SS_CLOUD`) |
| `--client-id` | 应用程序(客户端)ID。(或 `SS_CLIENT_ID`) |
| `--secret` | 客户端机密。建议优先使用 `SS_SECRET`,避免其出现在历史记录中。 |
| `--cert` | 用于 JWT 断言身份验证(而非客户端机密)的证书:PEM(密钥+证书)或 `.pfx`/`.p12`。需要 `cryptography`。(或 `SS_CERT`) |
| `--cert-password` | 加密的 `--cert` 密钥/PFX 的密码。(或 `SS_CERT_PASSWORD`) |
| `--active` | 选择加入的只读枚举:Graph 对象样本**以及** Key Vault / Storage 数据平面可达性。默认关闭以保持安静。 |
| `--deep` | 与 `--active` 一起使用:深入一层进入可达的 Storage——列出可访问容器中的 blob 和可访问共享中的文件(仅名称,有上限)。噪音更大。 |
| `--no-arm` | 跳过管理组 / 订阅 / RBAC 枚举(仅 Graph)。 |
| `--workers N` | ARM 范围查找和数据平面探测的并行 HTTP worker 数(默认 8;`1` = 顺序执行)。 |
| `--update-manifest` | 从实时租户获取权威的 appRole GUID→名称映射(Graph **以及**此凭据被分配到的任何其他资源 API),缓存后退出。 |
| `--json` | 将完整的嵌套结果以 JSON 形式打印,而不是报告。 |
| `--export PATH` | 将结果写入文件。格式根据扩展名推断(`.csv` / `.ndjson` / `.jsonl` / `.json` / `.html`)。文件以仅所有者可读写(`0600`)权限写入。 |
| `--export-format` | 强制指定导出格式(`ndjson` / `csv` / `json` / `html`)。 |
| `--timeout N` | 每个请求的超时时间(秒,默认 20)。ARM 控制平面请求(RBAC 枚举 + Resource Graph 发现)使用更长的超时时间——`1.5×`,最少 30 秒——因为它们运行较慢。 |
| `--verbose`, `-v` | 将每个 Graph/ARM/数据平面 HTTP 请求(方法、URL、状态)跟踪输出到 stderr。 |
| `--no-banner` | 抑制 ASCII 横幅。 |
| `--version` | 打印版本并退出。 |
---
## 导出结果
`--export` 将结果扁平化为**每个发现项一条记录**——
凭据、令牌、Graph 权限、应用角色分配、ARM 角色、数据平面
命中以及评分发现项——每条记录都带有凭据上下文,因此每一行都能独立
成立。```bash
# NDJSON — stream into a SIEM / log pipeline
python -m secret_stalker --export results.ndjson
# CSV — open in a spreadsheet for triage
python -m secret_stalker --active --export results.csv
# Full nested JSON to a file
python -m secret_stalker --export results.json
每条记录都带有一个 record_type(credential、secret、token、
graph_permission、app_role_assignment、directory_role、owned_object、
arm_role、dataplane、delegated_permission、requested_permission、
finding),因此使用者可以只筛选出自己需要的内容——例如,仅筛选
已计分的命中项:```bash
jq 'select(.record_type=="finding")' results.ndjson
终端报告与 `--export` 协同工作 —— 导出不会抑制
报告(“已导出 …”确认信息进入 stderr,因此通过管道传递 `--json` 时仍保持
干净)。
导出文件携带凭据上下文(令牌声明、secret `hint`、密钥 ID),
因此以 **仅所有者(`0600`)** 权限写入,以避免在共享或同步的
主机上泄露。请将它们视为敏感的参与产物。通过符号链接写入会被
直接拒绝,这样导出路径无法被重定向以截断其他
文件。
结果中的名称来自被评估的租户 —— 应用和组显示
名称、容器和 Blob 名称 —— 因此它们被视为不受信任的输出:
- **CSV** 值若会被读取为公式(以 `=`、`+`、`-`、`@` 开头),则
会添加单引号前缀,因此像 `=cmd|' /C calc'!A0` 这样的显示名称无法
在电子表格中打开文件时执行。电子表格会在显示时
移除引号。
- **终端、CSV 和 HTML** 输出中的控制字符会被剥离,因此一个名称
如果携带 ANSI 转义序列,就无法重设你的终端标题或覆盖上方的发现结果
—— 无论你是实时阅读报告、`cat` CSV 还是 `cat` HTML。
- **JSON / NDJSON 保持原样**:`json.dumps` 将控制字符编码为
`\uXXXX`,作为文本它是惰性的,但解析器仍能往返还原租户返回的精确
值。原始名称是证据,因此会在其中保留。
---
## 如何解析权限 GUID
`appRoleAssignments` 返回的是 GUID。secret_stalker 通过
扁平查找(appRole GUID 全局唯一)将其解析为名称,即使在
目录读取被拒绝时也能正常工作:
- 一份尽力而为的已知 Graph 权限映射随附于
`secret_stalker/data/graph_app_roles.json`。
- `--update-manifest` 会用从范围内租户实时拉取的权威数据覆盖它 —— Microsoft Graph **以及此凭据
被分配到的每个其他资源 API**(例如 Exchange Online、SharePoint),因此非 Graph GUID
也能解析。
- 未知 GUID 会 **原样显示并标记** —— 该工具从不猜测名称。
---
## 工作原理(简介)
- **有效性和权限在一次请求中完成。** 成功的 Graph 令牌的 `roles`
声明*就是*已授予的应用程序权限列表。secret_stalker 从解码后的令牌中读取它
—— 快速且安静,无需 Graph 调用。
- **Graph ≠ ARM。** 它们是不同的令牌受众。凭据可能在一个上拥有权限
而在另一个上没有,因此每个都会被独立探测。
- **数据平面 ≠ 控制平面。** 对 Key Vault(管理)拥有 ARM 权限
并不等同于能够读取其机密(数据平面)。在 `--active` 下,
会使用资源自身的令牌受众测试数据平面可达性 —— 并且它
仅列出对象的**名称**,从不列出值或内容。
- **按表层进行数据平面探测。** 数据平面 RBAC 按对象类型 /
服务授予,因此每个都被独立探测:Key Vault **机密 / 密钥 /
证书**、Storage **blob / 文件 / 队列 / 表**,以及 Cosmos DB
**数据库**。如果凭据是 `Storage File Data SMB Share Reader` 但不是
blob 读取器,它就会被发现而不会漏掉。(Cosmos 使用非标准 AAD REST 头
并且是 **尽力而为** —— 请针对实际账户验证 `denied` 结果。)
- **租户级发现。** 使用单个 Azure Resource Graph
扫描主体可以看到的每个订阅(尊重 RBAC),如果 ARG 被拒绝,则
回退到按订阅列出提供程序。报告会标记使用了哪条
路径(`[discovery: resource-graph]` 与 `per-subscription`)。扫描
会分页获取结果,直到达到上限(每种资源类型 40 页 × 1000 行),因此
运行总能终止;`--verbose` 会在达到上限时说明这一点。
- **严重性映射** 位于 `secret_stalker/risk.py` 中 —— 编辑它以调整
你的团队认为高风险的内容。
---
## 项目结构```
secret_stalker/
clouds.py Azure cloud endpoint table (public / usgov / usdod / china)
auth.py client_credentials flow + tenant discovery + AADSTS decoding
jwt_utils.py local JWT claim extraction
manifest.py Graph appRole GUID -> name resolution (bundled + live cache)
graph.py service principal lookup + appRole resolution + active probes
arm.py management-group / subscription RBAC + resource discovery
dataplane.py Key Vault / Storage data-plane reachability probes
risk.py permission/role/data-plane -> impact mapping (tune this)
report.py terminal + JSON output
export.py flatten to NDJSON / CSV / JSON records for ingestion
util.py shared HTTP (retry/backoff + safe JSON), pmap parallel map,
untrusted-output sanitizing
banner.py ASCII banner (stderr only)
cli.py orchestration
data/graph_app_roles.json bundled permission manifest
tests/ pytest suite (run: pytest)
pyproject.toml packaging + `secret_stalker` console entry point
--cert) — JWT 客户端断言(RS256),从 PEM 或 PFX
生成,并报告证书过期时间。 auth.pywids 声明进行被动检测
(无需读取目录)— 按角色评分。 graph.py / risk.pygraph.py--active) — 已同意的授权 + 已请求的
权限,将未经同意的危险权限标记为同意攻击目标。 graph.py / risk.py--export report.html)。 risk.py / report.py--cloud) — 公有云、美国政府云(GCC High)、美国国防部云,
以及中国(21Vianet),各自使用正确的 Entra 颁发机构和 Graph / ARM / Key Vault
受众。 --cloud | Entra 颁发机构 | Microsoft Graph | ARM | Key Vault |
|---|
public(默认) | login.microsoftonline.com | graph.microsoft.com | management.azure.com | vault.azure.net |
usgov(GCC High) | login.microsoftonline.us | graph.microsoft.us | management.usgovcloudapi.net | vault.usgovcloudapi.net |
usdod(DoD) | login.microsoftonline.us | dod-graph.microsoft.us | management.usgovcloudapi.net | vault.usgovcloudapi.net |
china(21Vianet) | login.chinacloudapi.cn | microsoftgraph.chinacloudapi.cn | management.chinacloudapi.cn | vault.azure.cn |
clouds.pyarm.pydataplane.py / risk.py+(例如 25+)表示列表被截断,而不是
静默少报。 dataplane.py429/503,
并遵循 Retry-After,因此瞬时限流不会被误读为“拒绝 /
无访问权限”。 util.py--deep) — 列出可访问容器中的 blob 和
可访问共享中的文件,仅名称且受上限限制。 dataplane.py--update-manifest 会为凭据被分配到的每个资源 API
缓存 appRoles,而不仅仅是 Graph。 graph.py / manifest.py--workers N) 用于 ARM 范围查找和数据平面探测,
具备逐项错误隔离。 util.py