From d847f32cc54ee9d9e3e60684136a81c18d309e5e Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Sun, 1 Mar 2026 20:50:31 +0100 Subject: [PATCH] f --- .../az-tokens-and-public-applications.md | 7 +- .../az-entraid-privesc/README.md | 80 ++++++++++++++++++- .../azure-security/az-services/az-azuread.md | 78 +++++++++++++++++- 3 files changed, 160 insertions(+), 5 deletions(-) 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 f5cdddbb3..24432d929 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 @@ -216,7 +216,7 @@ These refresh tokens must be minted in that broker context (a regular refresh to ### Goal and purpose -The goal of BroCI is to reuse a valid user session from a broker-capable app chain and request tokens for another trusted app/resource pair without running a new full interactive flow each time. +The goal of BroCI is to reuse a valid user session from a broker-capable app chain and request tokens for another trusted app/resource pair. Therefore, allowing to "escalate privileges" from the original token. From an offensive perspective, this matters because: @@ -235,6 +235,9 @@ NAA/BroCI token exchanges are **not** the same as a regular OAuth refresh exchan - You generally cannot "convert" a normal refresh token into a BroCI-valid one in code. - You need a refresh token already issued by a compatible brokered flow. +Check the web **** to find BroCI configured apps an the trust relationships they have. + + ### Mental model Think of BroCI as: @@ -245,7 +248,7 @@ If any part of that broker chain does not match, the exchange fails. ### Where to find a BroCI-valid refresh token -In authorized testing/lab scenarios, one practical way is browser portal traffic collection: +One practical way is browser portal traffic collection: 1. Sign in to `https://entra.microsoft.com` (or Azure portal). 2. Open DevTools -> Network. 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 506814b74..cb400ebd1 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 @@ -100,7 +100,7 @@ az ad app update --id --web-redirect-uris "https://original.com/callbac ### Applications Privilege Escalation -**As explained in [this post](https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/)** it was very common to find default applications that have **API permissions** of type **`Application`** assigned to them. An API Permission (as called in the Entra ID console) of type **`Application`** means that the application can access the API without a user context (without a user login into the app), and without needing Entra ID roles to allow it. Therefore, it's very common to find **high privileged applications in every Entra ID tenant**. +**As explained in [this post](https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/)** it was very common to find default applications that have **API permissions** of type **`Application`** assigned to them. An API Permission (as called in the Entra ID console) of type **`Application`** means that the application can access the API and perform actions without a user context (without a user login into the app), and without needing Entra ID roles to allow it. Therefore, it's very common to find **high privileged applications in every Entra ID tenant**. Then, if an attacker has any permission/role that allows to **update the credentials (secret o certificate) of the application**, the attacker can generate a new credential and then use it to **authenticate as the application**, gaining all the permissions that the application has. @@ -138,6 +138,83 @@ 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 ``` +
+Find all applications with API permissions to non-Microsoft APIs (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") +``` + +
+ ## Service Principals ### `microsoft.directory/servicePrincipals/credentials/update` @@ -397,4 +474,3 @@ az rest --method GET \ {{#include ../../../../banners/hacktricks-training.md}} - 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 85b9a8059..f492ff004 100644 --- a/src/pentesting-cloud/azure-security/az-services/az-azuread.md +++ b/src/pentesting-cloud/azure-security/az-services/az-azuread.md @@ -819,6 +819,83 @@ 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 ``` +
+Find all applications with API permissions to non-Microsoft APIs (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" }} @@ -1308,4 +1385,3 @@ The default mode is **Audit**: {{#include ../../../banners/hacktricks-training.md}} -