
Avaliação somente leitura de credenciais de aplicativo do Entra ID: enumera permissões do Graph, RBAC do Azure e dados de nuvem acessíveis e, em seguida, mapeia as descobertas para caminhos de elevação de privilégios e movimento lateral.
/ / ______ ___ ___ / /_ / / / /__ _ / / /_____ ____ \ / -) / -) -)/ / \ \ / __/ _ `// / '/ -) / //_/_/_/_/ _/ // _/_,////_\__/_/
╔╦╦╬╬╬╬╬╬╦╦╗
╔╬╬╬╝╝┘ ╚╝╝╬╬╬┐
╬╬╝╚╩╬╗╔ ╚╬╬╬
╬╝ ╚╬╬╗╗ ╔ ╚╬╗ ╬╬ ╔╗ ╚╬╬╬╬╬╬╦ ╬╬ 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. \
**O que este ID do cliente + segredo do Entra ID realmente pode fazer?**
Você encontrou uma credencial de aplicativo do Entra ID (Azure AD) — um ID do cliente e um segredo —
em um engajamento autorizado, e o locatário ao qual ela pertence está no escopo.
O `secret_stalker` pega esses dois valores e diz a você, partindo do zero:
1. **A credencial é válida e quando o segredo expira?** — e, se não for válida, *por quê*
(segredo incorreto, segredo expirado, aplicativo fora do locatário…). Para um segredo válido, ele
lê os `passwordCredentials` do registro do aplicativo e informa a data de expiração
+ dias restantes (exige leitura do diretório; veja a nota abaixo).
2. **Quais direitos do Microsoft Graph ela possui?** — permissões de aplicativo lidas
diretamente do token emitido, além das **funções de diretório do Entra** que ela detém (mesmo
detectadas passivamente a partir da declaração `wids` do token) e **objetos que ela possui**
(aplicativos/SPs aos quais você pode adicionar credenciais).
3. **Que controle ela tem sobre o Azure?** — atribuições de função RBAC no escopo do
grupo de gerenciamento e da assinatura.
4. **Ela consegue alcançar dados reais?** — verificações opcionais de Key Vault (segredos / chaves / certificados),
Storage (blob / arquivo / fila / tabela) e Cosmos DB para verificação de alcance no plano de dados.
5. **Qual é o impacto?** — permissões perigosas, funções, propriedade e dados
alcançáveis mapeados a primitivas conhecidas de privesc / movimento lateral, classificados por
gravidade, com narrativas concretas de **caminho de ataque**.
Ele se autentica com um **segredo do cliente** ou um **certificado** (`--cert`),
e funciona em **nuvens comerciais e soberanas** (`--cloud`).
Ele é **passivo por padrão** e **nunca modifica nada** — apenas
enumeração somente leitura.
> ⚠️ **Somente testes autorizados.** Execute-o apenas contra locatários que estejam
> explicitamente no escopo de um engajamento que você está autorizado a realizar.
---
## Install```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
A única dependência de runtime é requests. Os tokens são decodificados localmente (base64 + JSON) — sem verificação de assinatura, sem biblioteca de criptografia, sem SDK da Microsoft. A exceção é a autenticação por certificado (--cert), que precisa do pacote opcional cryptography para assinar a asserção do cliente JWT. Requer Python 3.7+.
Após pip install ., você pode invocá-lo como secret_stalker … em vez de python -m secret_stalker ….
A maneira mais rápida de descobrir o que uma credencial pode fazer:```bash
python -m secret_stalker
--tenant contoso.onmicrosoft.com
--client-id 11111111-2222-3333-4444-555555555555
--secret ''
`--tenant` aceita um GUID de tenant ou um domínio — um domínio é resolvido para o seu
ID de tenant automaticamente por meio do endpoint público de configuração OpenID.
### Mantenha o segredo fora do seu histórico de shell
Passe as credenciais por meio de variáveis de ambiente em vez de flags:```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
Qualquer um de --tenant / --client-id / --secret pode vir de SS_TENANT /
SS_CLIENT_ID / SS_SECRET. As flags têm precedência sobre o ambiente.
Isto não se limita ao histórico da shell: um valor de argv é legível por qualquer
utilizador local durante a vida do processo (ps, /proc/<pid>/cmdline). Se --secret
ou --cert-password for passado como uma flag, a ferramenta imprime um lembrete de uma linha para
stderr — nunca aparece na saída de --json ou --export.
Os registos de aplicações costumam usar um certificado em vez de um segredo. Passe --cert
(um PEM contendo a chave privada e o certificado, ou um .pfx/.p12) e a
ferramenta autentica-se com uma asserção de cliente JWT assinada:```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 ''
Autenticação por certificado requer o pacote opcional `cryptography` (`pip install '.[cert]'`).
A ferramenta relata a expiração do próprio certificado (correspondente à sua impressão digital no `keyCredentials` do aplicativo), assim como faz para um segredo. `--cert`/`--cert-password` também são lidos de `SS_CERT` / `SS_CERT_PASSWORD`.
### Nuvens soberanas e governamentais
Por padrão, o secret_stalker tem como alvo a nuvem **comercial**. Para locatários soberanos, passe `--cloud` (ou `SS_CLOUD`) para que a autoridade do Entra e os endpoints do Graph / ARM / Key Vault correspondam — caso contrário, credenciais válidas parecem não ter acesso:```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 ...
Aliases como gov, dod, commercial, gcc-high e 21vianet são aceitos.
(O público-alvo do plano de dados do Storage, storage.azure.com, é o mesmo em todas as nuvens.)
Isso autentica, armazena em cache o mapa de appRole do Graph do locatário em
~/.secret_stalker/app_roles_cache.json, e sai. Ignore-o se a
credencial não puder ler os service principals — o mapa incluído ainda cobre as
permissões bem conhecidas.
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
- **Aquisição de token** lista cada público-alvo sondado (Graph, ARM e — sob
`--active`, quando recursos correspondentes são descobertos — Key Vault / Storage /
Cosmos DB). Graph e ARM são *independentes*: uma credencial pode ter um e não
o outro.
- **Segredo** mostra a validade e, para um segredo válido, a data de expiração e os dias
restantes (proximidade da expiração é destacada). Veja a nota abaixo sobre segredos expirados.
- **Descobertas** é a parte a ler primeiro — permissões Graph de alto impacto (`GRAPH`),
funções ARM (`ARM`), funções de diretório do Entra (`ROLE`), apps/SPs próprios (`OWN`),
superfícies de plano de dados acessíveis (`DATA`) e alvos de ataque de consentimento
solicitados mas não consentidos (`WANT`) — deduplicadas e classificadas por severidade. Ser capaz de
ler todos os segredos do Key Vault, ou ter uma função de diretório, é uma descoberta por si só,
mesmo sem concessão perigosa de Graph/ARM.
- **Caminhos de ataque** transformam as principais descobertas em passos concretos (ex.: *Privileged
Role Administrator → atribuir Global Administrator a si mesmo → assumir o tenant*).
- **Enumeração ativa do Graph** (`--active`) relata o que cada sonda somente-leitura
retornou. A maioria das sondas solicita uma pequena página limitada, portanto uma página cheia é mostrada como
`N+` (ex.: `users accessible (returned 5+)`) — significando *pelo menos* cinco, não
exatamente cinco. Sondas sem limite (`organization`, `directoryRoles`) relatam um
total real sem `+`.
- **Funções de diretório / Objetos próprios / Permissões delegadas** têm suas próprias seções.
As funções de diretório são detectadas a partir da declaração `wids` do token, mesmo sem leitura de diretório;
permissões delegadas não são utilizáveis por uma credencial somente de aplicativo, mas são mostradas
para pivôs de contexto de usuário e direcionamento de ataques de consentimento.
- **Risco geral** é a severidade da descoberta individual mais alta.
> **Expiração de segredo — o que é possível saber.** A data de expiração *não* está no token;
> ela vive em `passwordCredentials` do registro do aplicativo no Entra ID. Para um
> segredo **válido**, o secret_stalker lê a data via Graph e associa seu segredo à
> credencial certa pelo `hint` (primeiros 3 caracteres) — isso requer leitura de diretório
> (`Application.Read.All` / `Directory.Read.All`); se o SP não tiver, a data é
> informada como indisponível em vez de adivinhada. Para um segredo **expirado**, a autenticação
> em si falha, então a credencial morta não consegue ler seus próprios metadados — a ferramenta
> o marca como `EXPIRED (AADSTS7000222)`, mas a data exata de término não é recuperável
> somente por essa credencial.
### Códigos de saída
Útil para scripts:
| Código | Significado |
|------|---------|
| `0` | A credencial é válida (obteve pelo menos um token). |
| `2` | A credencial é inválida / sem acesso. |
| `1` | Erro — o tenant não pôde ser resolvido, o certificado não pôde ser carregado ou o arquivo `--export` não pôde ser gravado. |
---
## Todas as flags
| Flag | Efeito |
|------|--------|
| `--tenant` | GUID ou domínio do tenant. (ou `SS_TENANT`) |
| `--cloud` | Nuvem Azure: `public` (padrão), `usgov` (GCC High), `usdod` (DoD), `china` (21Vianet). Seleciona a autoridade do Entra e os endpoints Graph/ARM/Key Vault. Aliases como `gov`/`dod`/`commercial` aceitos. (ou `SS_CLOUD`) |
| `--client-id` | ID do aplicativo (cliente). (ou `SS_CLIENT_ID`) |
| `--secret` | Segredo do cliente. Prefira `SS_SECRET` para mantê-lo fora do histórico. |
| `--cert` | Certificado para autenticação por asserção JWT em vez de segredo: um PEM (chave+cert) ou `.pfx`/`.p12`. Requer `cryptography`. (ou `SS_CERT`) |
| `--cert-password` | Senha para chave/PFX criptografada do `--cert`. (ou `SS_CERT_PASSWORD`) |
| `--active` | Enumeração somente-leitura opt-in: amostras de objetos do Graph **mais** acessibilidade ao plano de dados do Key Vault / Storage. Desativada por padrão para ficar discreta. |
| `--deep` | Com `--active`: desça um nível no Storage acessível — liste blobs em contêineres acessíveis e arquivos em compartilhamentos acessíveis (apenas nomes, com limite). Mais ruidoso. |
| `--no-arm` | Pule a enumeração de grupo de gerenciamento / assinatura / RBAC (somente Graph). |
| `--workers N` | Workers HTTP paralelos para buscas de escopo ARM e sondas de plano de dados (padrão 8; `1` = sequencial). |
| `--update-manifest` | Busca o mapa autoritativo appRole GUID→nome do tenant ativo (Graph **mais** quaisquer outras APIs de recurso em que esta credencial esteja atribuída), armazena em cache e sai. |
| `--json` | Imprime o resultado aninhado completo como JSON em vez do relatório. |
| `--export PATH` | Grava os resultados em um arquivo. Formato inferido pela extensão (`.csv` / `.ndjson` / `.jsonl` / `.json` / `.html`). Os arquivos são gravados somente para o proprietário (`0600`). |
| `--export-format` | Força o formato de exportação (`ndjson` / `csv` / `json` / `html`). |
| `--timeout N` | Tempo limite por solicitação em segundos (padrão 20). Solicitações do plano de controle ARM (enumeração RBAC + descoberta do Resource Graph) usam um tempo limite maior — `1.5×`, mínimo 30s — porque são mais lentas. |
| `--verbose`, `-v` | Rastreia cada solicitação HTTP Graph/ARM/plano de dados (método, URL, status) para stderr. |
| `--no-banner` | Suprime o banner ASCII. |
| `--version` | Imprime a versão e sai. |
---
## Exportando resultados
`--export` achata o resultado em **um registro por item descoberto** —
credencial, token, permissão Graph, atribuição de app-role, função ARM,
ocorrência de plano de dados e descoberta pontuada — cada um carregando o contexto da credencial para que uma linha se sustente
sozinha.```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
Cada registro carrega um record_type (credential, secret, token,
graph_permission, app_role_assignment, directory_role, owned_object,
arm_role, dataplane, delegated_permission, requested_permission,
finding), para que um consumidor possa filtrar apenas o que precisa — por exemplo, somente os
resultados pontuados:```bash
jq 'select(.record_type=="finding")' results.ndjson
O relatório do terminal e o `--export` funcionam juntos — a exportação não suprime o relatório (a confirmação "Exported …" vai para o stderr, então o pipe com `--json` permanece limpo).
Os arquivos exportados carregam contexto da credencial (declarações do token, o `hint` secreto, IDs de chave), por isso são gravados **somente para o dono (`0600`)** para evitar vazamentos em um host compartilhado ou sincronizado. Trate-os como artefatos sensíveis de engajamento. A gravação por meio de um symlink é recusada de imediato, portanto um caminho de exportação não pode ser redirecionado para truncar outra coisa.
Os nomes em um resultado vêm do tenant sob avaliação — nomes de exibição de aplicativos e grupos, nomes de contêineres e blobs — portanto são tratados como saída não confiável:
- **CSV** values that would be read as a formula (leading `=`, `+`, `-`, `@`) are
prefixed with a single quote, so a display name like `=cmd|' /C calc'!A0` can't
execute when the file is opened in a spreadsheet. Spreadsheets strip the quote
on display.
- **Terminal, CSV and HTML** output has control characters stripped, so a name
carrying ANSI escapes can't retitle your terminal or overwrite the findings
above it — whether you read the report live, `cat` the CSV, or `cat` the HTML.
- **JSON / NDJSON são mantidos fiéis**: `json.dumps` codifica caracteres de
controle como `\uXXXX`, o que é inerte como texto, enquanto um parser ainda
devolve exatamente o valor que o tenant retornou. O nome bruto é uma evidência,
então é preservado ali.
---
## Como os GUIDs de permissão são resolvidos
`appRoleAssignments` retornam como GUIDs. O secret_stalker os resolve para nomes por meio de uma busca simples (os GUIDs de appRole são globalmente exclusivos), que continua funcionando **mesmo quando leituras de diretório são negadas**:
- Um mapa de melhor esforço das permissões conhecidas do Graph está incluído em
`secret_stalker/data/graph_app_roles.json`.
- O `--update-manifest` o substitui por dados autoritativos obtidos ao vivo do
tenant em escopo — Microsoft Graph **além de todas as outras APIs de recurso
nas quais esta credencial está atribuída** (por exemplo, Exchange Online,
SharePoint) —, para que GUIDs não pertencentes ao Graph também sejam resolvidos.
- Um GUID desconhecido é exibido **bruto e sinalizado** — a ferramenta nunca
adivinha um nome.
---
## Como funciona (a versão curta)
- **Validade + permissões em uma única requisição.** A declaração `roles` de um
token Graph bem-sucedido *é* a lista de permissões de aplicativo concedidas. O
secret_stalker lê isso do token decodificado — rápido e silencioso, sem chamadas
Graph necessárias.
- **Graph ≠ ARM.** São públicos de token diferentes. Uma credencial pode ter
direitos em um e não no outro, então cada um é sondado independentemente.
- **Plano de dados ≠ plano de controle.** Ter direitos ARM sobre um Key Vault
(gerenciamento) não é o mesmo que conseguir ler seus segredos (plano de dados).
Com `--active`, a acessibilidade do plano de dados é testada com o público de
token do próprio recurso — e ela lista **apenas nomes** de objetos, nunca valores
ou conteúdos.
- **Sondagem do plano de dados por superfície.** O RBAC do plano de dados é
concedido por tipo de objeto / serviço, então cada um é sondado
independentemente: **segredos / chaves / certificados** do Key Vault,
**blob / arquivo / fila / tabela** do Storage e **bancos de dados** do Cosmos DB.
Uma credencial que é `Storage File Data SMB Share Reader` mas não é leitora de
blob é trazida à tona, não omitida. (O Cosmos usa um cabeçalho REST AAD não
padrão e é **melhor esforço** — valide um resultado `denied` contra uma conta
ativa.)
- **Descoberta em todo o tenant.** Os recursos são encontrados com uma única
varredura do Azure Resource Graph em todas as assinaturas que o principal pode
ver (respeitando RBAC), com fallback para a listagem de provedores por assinatura
se o ARG for negado. O relatório sinaliza qual caminho foi usado
(`[discovery: resource-graph]` vs `per-subscription`). A varredura pagina pelos
resultados até um limite (40 páginas × 1000 linhas por tipo de recurso), de modo
que uma execução sempre termina; o `--verbose` informa se o limite for atingido.
- **Mapeamento de severidade** está em `secret_stalker/risk.py` — edite-o para
ajustar o que sua equipe considera de alto impacto.
---
## Estrutura do projeto```
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) — asserção de cliente JWT (RS256) a partir de um PEM ou PFX, com relato de expiração do certificado. auth.pywids do token (sem necessidade de leitura do diretório) — pontuadas por função. graph.py / risk.pygraph.py--active) — concessões consentidas + permissões solicitadas, com permissões perigosas não consentidas sinalizadas como alvos de ataque de consentimento. graph.py / risk.py--export report.html). risk.py / report.py--cloud | Autoridade Entra | Microsoft Graph | ARM | Key Vault |
|---|
public (padrão) | 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 |
--cloud) — pública, US Gov (GCC High), US DoD e China (21Vianet), cada uma com a autoridade Entra correta e públicos do Graph / ARM / Key Vault. clouds.pyarm.pydataplane.py / risk.py+ (ex.: 25+) marca onde a listagem foi limitada em vez de subnotificar silenciosamente. dataplane.py429/503 respeitando Retry-After, para que a limitação transitória não seja interpretada como "negado / sem acesso". util.py--deep) — lista blobs em contêineres acessíveis e arquivos em compartilhamentos acessíveis, somente nomes e limitado. dataplane.py--update-manifest armazena em cache appRoles para cada API de recurso na qual a credencial é atribuída, não apenas o Graph. graph.py / manifest.py--workers N) entre consultas de escopo ARM e sondagens do plano de dados, com isolamento de erros por item. util.py