只读 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 ...
--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 |
接受 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` 文件。 |
---
## 所有标志