
읽기 전용 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. \
**What can this Entra ID client ID + secret actually do?**
You found an Entra ID (Azure AD) application credential — a client ID and secret —
on an authorized engagement, and the tenant it belongs to is in scope.
`secret_stalker` takes those two values and tells you, from a cold start:
1. **Is it valid, and when does the secret expire?** — and if not valid, *why*
(bad secret, expired secret, app not in the tenant…). For a valid secret it
reads the app registration's `passwordCredentials` and reports the expiration
date + days remaining (needs directory read; see note below).
2. **What Microsoft Graph rights does it carry?** — application permissions read
straight from the issued token, plus **Entra directory roles** it holds (even
detected passively from the token's `wids` claim) and **objects it owns**
(apps/SPs you can add credentials to).
3. **What control does it have over Azure?** — RBAC role assignments at
management-group and subscription scope.
4. **Can it reach real data?** — optional Key Vault (secrets / keys / certificates),
Storage (blob / file / queue / table), and Cosmos DB data-plane reachability checks.
5. **What's the impact?** — dangerous permissions, roles, ownership, and reachable
data mapped to known privesc / lateral-movement primitives, rated by severity,
with concrete **attack-path** narratives.
It authenticates with either a **client secret** or a **certificate** (`--cert`),
and works against **commercial and sovereign clouds** (`--cloud`).
It is **passive by default** and **never modifies anything** — read-only
enumeration only.
> ⚠️ **Authorized testing only.** Run it solely against tenants that are
> explicitly in scope for an engagement you are authorized to perform.
---
## 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
유일한 런타임 종속성은 requests입니다. 토큰은 로컬에서 (base64 +
JSON)으로 디코딩됩니다 — 서명 검증도, 암호화 라이브러리도, Microsoft SDK도 필요 없습니다. 유일한
예외는 인증서 인증(--cert)으로, JWT 클라이언트 어서션에 서명하려면 선택적 cryptography
패키지가 필요합니다. Python 3.7+가 필요합니다.
pip install . 후에는 python -m secret_stalker … 대신
secret_stalker …로 호출할 수 있습니다.
자격 증명으로 무엇을 할 수 있는지 알아보는 가장 빠른 방법:```bash
python -m secret_stalker
--tenant contoso.onmicrosoft.com
--client-id 11111111-2222-3333-4444-555555555555
--secret ''
`--tenant`는 테넌트 GUID 또는 도메인을 허용합니다 — 도메인은 공개 OpenID 구성 엔드포인트를 통해
해당 테넌트 ID로 자동 확인됩니다.
### 셸 기록에 비밀이 남지 않도록 하기
플래그 대신 환경 변수로 자격 증명을 전달하세요:```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에서 가져올 수 있습니다. 플래그가 환경 변수보다 우선합니다.
이는 셸 히스토리에만 해당되는 이야기가 아닙니다. 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는 **상용(commercial)** 클라우드를 대상으로 합니다. 소버린 테넌트의 경우,
`--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 데이터 플레인 대상(audience)인 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
- **토큰 획득**은 조사된 각 대상(Graph, ARM, 그리고 — `--active` 사용 시
일치하는 리소스가 발견되면 — Key Vault / Storage / Cosmos DB)을
나열합니다. Graph와 ARM은 *독립적*입니다: 자격 증명은 둘 중 하나만
보유할 수 있습니다.
- **시크릿**은 유효성과, 유효한 시크릿의 경우 만료일과 남은 일수를
표시합니다(만료 임박은 강조 표시). 만료된 시크릿에 대한 아래 참고를
보세요.
- **결과(Findings)**는 가장 먼저 읽어야 할 부분입니다 — 영향력이 큰 Graph 권한(`GRAPH`),
ARM 역할(`ARM`), Entra 디렉터리 역할(`ROLE`), 소유한 앱/SP(`OWN`),
도달 가능한 데이터 플레인 표면(`DATA`), 그리고 요청되었으나 동의되지 않은
동의 공격 대상(`WANT`) — 중복 제거되고 심각도로 평가됩니다. 모든 Key Vault
시크릿을 읽을 수 있거나 디렉터리 역할을 보유한 것은 위험한 Graph/ARM 권한이
없더라도 그 자체로 하나의 결과입니다.
- **공격 경로**는 최상위 결과를 구체적인 다음 단계로 전환합니다(예: *Privileged
Role Administrator → 자기 자신에게 Global Administrator 할당 → 테넌트 장악*).
- **활성 Graph 열거**(`--active`)는 각 읽기 전용 프로브가
반환한 내용을 보고합니다. 대부분의 프로브는 작은 상한 페이지를 요청하므로,
가득 찬 페이지는 `N+`로 표시됩니다(예: `users accessible (returned 5+)`) —
즉 *정확히* 5개가 아니라 *최소* 5개를 의미합니다. 상한이 없는 프로브
(`organization`, `directoryRoles`)는 `+` 없이 실제 총계를 보고합니다.
- **디렉터리 역할 / 소유 개체 / 위임된 권한**은 각각 별도 섹션으로 제공됩니다.
디렉터리 역할은 디렉터리 읽기 권한이 없어도 토큰의 `wids` 클레임에서
감지됩니다. 위임된 권한은 앱 전용 자격 증명으로는 사용할 수 없지만,
사용자 컨텍스트 전환 및 동의 공격 대상 선정을 위해 표시됩니다.
- **전체 위험도**는 가장 높은 단일 결과의 심각도입니다.
> **시크릿 만료 — 알 수 있는 것.** 만료 날짜는 토큰에 *없습니다*;
> Entra ID의 앱 등록 `passwordCredentials`에 있습니다. **유효한** 시크릿의 경우,
> secret_stalker가 Graph를 통해 이를 읽고 사용자의 시크릿을
> `hint`(처음 3자)로 올바른 자격 증명과 일치시킵니다 — 디렉터리 읽기 권한
> (`Application.Read.All` / `Directory.Read.All`)이 필요합니다. SP에 없으면 날짜는
> 추측하지 않고 사용 불가로 보고됩니다. **만료된** 시크릿은 인증 자체가
> 실패하므로, 만료된 자격 증명은 자신의 메타데이터를 읽을 수 없습니다 — 도구는
> 이를 `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 워커 수(기본값 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`는 함께 동작합니다 — 내보내기를 해도 리포트가 억제되지 않습니다("Exported …" 확인 메시지는 stderr로 출력되므로 `--json`을 파이프로 넘겨도 깨끗하게 유지됩니다).
내보내기 파일에는 자격 증명 컨텍스트(토큰 클레임, 시크릿 `hint`, 키 ID)가 포함되므로 공유 또는 동기화된 호스트에서 누출되지 않도록 **소유자 전용(`0600`)** 권한으로 작성됩니다. 이를 민감한 작업 산출물로 취급하세요. 심볼릭 링크를 통한 쓰기는 완전히 거부되므로 내보내기 경로를 다른 파일을 잘라내도록 리디렉션할 수 없습니다.
결과의 이름은 평가 대상 테넌트에서 비롯됩니다 — 앱 및 그룹 표시 이름, 컨테이너 및 blob 이름 — 따라서 신뢰할 수 없는 출력으로 취급됩니다:
- **CSV** 값 중 수식으로 읽힐 수 있는 값(앞에 `=`, `+`, `-`, `@`가 붙은)에는 작은따옴표가 접두사로 붙으므로 `=cmd|' /C calc'!A0` 같은 표시 이름은 스프레드시트에서 파일을 열 때 실행될 수 없습니다. 스프레드시트는 표시 시 따옴표를 제거합니다.
- **터미널, CSV 및 HTML** 출력에서는 제어 문자가 제거되므로 ANSI 이스케이프를 담은 이름이 터미널 제목을 바꾸거나 그 위의 결과를 덮어쓸 수 없습니다 — 리포트를 실시간으로 읽든, CSV를 `cat` 하든, HTML을 `cat` 하든 동일합니다.
- **JSON / NDJSON은 충실하게 유지됩니다**: `json.dumps`는 제어 문자를 `\uXXXX`로 인코딩하므로 텍스트로는 무해하지만, 파서는 테넌트가 반환한 정확한 값을 그대로 라운드트립할 수 있습니다. 원시 이름은 증거 자료이므로 여기에는 보존됩니다.
---
## 권한 GUID 확인 방식
`appRoleAssignments`는 GUID로 반환됩니다. secret_stalker는 단순 조회(appRole GUID는 전역적으로 고유함)를 통해 이를 이름으로 확인하며, 이는 **디렉터리 읽기가 거부된 경우에도** 계속 동작합니다:
- 잘 알려진 Graph 권한에 대한 최선 노력(best-effort) 맵이 `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.** 이것들은 서로 다른 토큰 대상(audience)입니다. 자격 증명은 한쪽에는 권한이 있고 다른 쪽에는 없을 수 있으므로 각각 독립적으로 검사합니다.
- **데이터 플레인 ≠ 컨트롤 플레인.** Key Vault에 대한 ARM 권한(관리)은 해당 시크릿을 읽을 수 있는 권한(데이터 플레인)과 동일하지 않습니다. `--active`에서는 데이터 플레인 연결 가능성이 리소스 자체의 토큰 대상으로 테스트되며 객체 **이름만** 나열되고 값이나 내용은 절대 나열되지 않습니다.
- **표면별 데이터 플레인 탐지.** 데이터 플레인 RBAC는 객체 유형/서비스별로 부여되므로 각각 독립적으로 검사합니다: Key Vault **비밀 / 키 / 인증서**, Storage **blob / 파일 / 큐 / 테이블**, Cosmos DB **데이터베이스**. `Storage File Data SMB Share Reader` 권한은 있지만 blob 읽기 권한이 없는 자격 증명도 누락되지 않고 표면화됩니다. (Cosmos는 비표준 AAD REST 헤더를 사용하며 **best-effort** 방식입니다 — `denied` 결과는 라이브 계정으로 검증하세요.)
- **테넌트 전체 검색.** 리소스는 보안 주체가 볼 수 있는 모든 구독에 걸쳐 Azure Resource Graph를 한 번 스윕하여 찾습니다(RBAC를 존중). ARG가 거부되면 구독별 공급자 목록으로 폴백합니다. 리포트는 사용된 경로를 태그로 표시합니다(`[discovery: resource-graph]` vs `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) — PEM 또는 PFX에서 JWT client-assertion(RS256)을 생성하고
인증서 만료를 보고합니다. auth.pywids 클레임 기반(디렉터리 읽기 불필요) — 역할별 점수화. graph.py / risk.pygraph.py--active) — 동의된 권한 부여 + 요청된
권한, 동의되지 않은 위험한 권한은 동의 공격 대상으로 표시. graph.py / risk.py--export report.html). risk.py / report.py--cloud) — public, US Gov(GCC High), US DoD,
그리고 China(21Vianet), 각각 올바른 Entra authority와 Graph / ARM / Key
Vault 대상(audience) 사용. --cloud | Entra authority | 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