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}}
-