This commit is contained in:
Carlos Polop
2026-03-01 20:50:31 +01:00
parent 0e45e2e2c7
commit d847f32cc5
3 changed files with 160 additions and 5 deletions

View File

@@ -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 **<https://entrascopes.com/>** 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.

View File

@@ -100,7 +100,7 @@ az ad app update --id <app-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 <ResourceAppId> --query "appRoles[?id=='<id>'].value" -o tsv
az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?id=='d07a8cc0-3d51-4b77-b3b0-32704d1f69fa'].value" -o tsv
```
<details>
<summary>Find all applications with API permissions to non-Microsoft APIs (az cli)</summary>
```bash
#!/usr/bin/env bash
set -euo pipefail
# Known Microsoft first-party owner organization IDs.
MICROSOFT_OWNER_ORG_IDS=(
"f8cdef31-a31e-4b4a-93e4-5f571e91255a"
"72f988bf-86f1-41af-91ab-2d7cd011db47"
)
is_microsoft_owner() {
local owner="$1"
local id
for id in "${MICROSOFT_OWNER_ORG_IDS[@]}"; do
if [ "$owner" = "$id" ]; then
return 0
fi
done
return 1
}
command -v az >/dev/null 2>&1 || { echo "az CLI not found" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found" >&2; exit 1; }
az account show >/dev/null
apps_json="$(az ad app list --all --query '[?length(requiredResourceAccess) > `0`].[displayName,appId,requiredResourceAccess]' -o json)"
tmp_map="$(mktemp)"
tmp_ids="$(mktemp)"
trap 'rm -f "$tmp_map" "$tmp_ids"' EXIT
# Build unique resourceAppId values used by applications.
jq -r '.[][2][]?.resourceAppId' <<<"$apps_json" | sort -u > "$tmp_ids"
# Resolve resourceAppId -> owner organization + API display name.
while IFS= read -r rid; do
[ -n "$rid" ] || continue
sp_json="$(az ad sp show --id "$rid" --query '{owner:appOwnerOrganizationId,name:displayName}' -o json 2>/dev/null || true)"
owner="$(jq -r '.owner // "UNKNOWN"' <<<"$sp_json")"
name="$(jq -r '.name // "UNKNOWN"' <<<"$sp_json")"
printf '%s\t%s\t%s\n' "$rid" "$owner" "$name" >> "$tmp_map"
done < "$tmp_ids"
echo -e "appDisplayName\tappId\tresourceApiDisplayName\tresourceAppId\tresourceOwnerOrgId\tpermissionType\tpermissionId"
# Print only app permissions where the target API is NOT Microsoft-owned.
while IFS= read -r row; do
app_name="$(jq -r '.[0]' <<<"$row")"
app_id="$(jq -r '.[1]' <<<"$row")"
while IFS= read -r rra; do
resource_app_id="$(jq -r '.resourceAppId' <<<"$rra")"
map_line="$(awk -F '\t' -v id="$resource_app_id" '$1==id {print; exit}' "$tmp_map")"
owner_org="$(awk -F'\t' '{print $2}' <<<"$map_line")"
resource_name="$(awk -F'\t' '{print $3}' <<<"$map_line")"
[ -n "$owner_org" ] || owner_org="UNKNOWN"
[ -n "$resource_name" ] || resource_name="UNKNOWN"
if is_microsoft_owner "$owner_org"; then
continue
fi
while IFS= read -r access; do
perm_type="$(jq -r '.type' <<<"$access")"
perm_id="$(jq -r '.id' <<<"$access")"
echo -e "${app_name}\t${app_id}\t${resource_name}\t${resource_app_id}\t${owner_org}\t${perm_type}\t${perm_id}"
done < <(jq -c '.resourceAccess[]' <<<"$rra")
done < <(jq -c '.[2][]' <<<"$row")
done < <(jq -c '.[]' <<<"$apps_json")
```
</details>
## Service Principals
### `microsoft.directory/servicePrincipals/credentials/update`
@@ -397,4 +474,3 @@ az rest --method GET \
{{#include ../../../../banners/hacktricks-training.md}}

View File

@@ -819,6 +819,83 @@ az ad sp show --id <ResourceAppId> --query "appRoles[?id=='<id>'].value" -o tsv
az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[?id=='d07a8cc0-3d51-4b77-b3b0-32704d1f69fa'].value" -o tsv
```
<details>
<summary>Find all applications with API permissions to non-Microsoft APIs (az cli)</summary>
```bash
#!/usr/bin/env bash
set -euo pipefail
# Known Microsoft first-party owner organization IDs.
MICROSOFT_OWNER_ORG_IDS=(
"f8cdef31-a31e-4b4a-93e4-5f571e91255a"
"72f988bf-86f1-41af-91ab-2d7cd011db47"
)
is_microsoft_owner() {
local owner="$1"
local id
for id in "${MICROSOFT_OWNER_ORG_IDS[@]}"; do
if [ "$owner" = "$id" ]; then
return 0
fi
done
return 1
}
command -v az >/dev/null 2>&1 || { echo "az CLI not found" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found" >&2; exit 1; }
az account show >/dev/null
apps_json="$(az ad app list --all --query '[?length(requiredResourceAccess) > `0`].[displayName,appId,requiredResourceAccess]' -o json)"
tmp_map="$(mktemp)"
tmp_ids="$(mktemp)"
trap 'rm -f "$tmp_map" "$tmp_ids"' EXIT
# Build unique resourceAppId values used by applications.
jq -r '.[][2][]?.resourceAppId' <<<"$apps_json" | sort -u > "$tmp_ids"
# Resolve resourceAppId -> owner organization + API display name.
while IFS= read -r rid; do
[ -n "$rid" ] || continue
sp_json="$(az ad sp show --id "$rid" --query '{owner:appOwnerOrganizationId,name:displayName}' -o json 2>/dev/null || true)"
owner="$(jq -r '.owner // "UNKNOWN"' <<<"$sp_json")"
name="$(jq -r '.name // "UNKNOWN"' <<<"$sp_json")"
printf '%s\t%s\t%s\n' "$rid" "$owner" "$name" >> "$tmp_map"
done < "$tmp_ids"
echo -e "appDisplayName\tappId\tresourceApiDisplayName\tresourceAppId\tresourceOwnerOrgId\tpermissionType\tpermissionId"
# Print only app permissions where the target API is NOT Microsoft-owned.
while IFS= read -r row; do
app_name="$(jq -r '.[0]' <<<"$row")"
app_id="$(jq -r '.[1]' <<<"$row")"
while IFS= read -r rra; do
resource_app_id="$(jq -r '.resourceAppId' <<<"$rra")"
map_line="$(awk -F '\t' -v id="$resource_app_id" '$1==id {print; exit}' "$tmp_map")"
owner_org="$(awk -F'\t' '{print $2}' <<<"$map_line")"
resource_name="$(awk -F'\t' '{print $3}' <<<"$map_line")"
[ -n "$owner_org" ] || owner_org="UNKNOWN"
[ -n "$resource_name" ] || resource_name="UNKNOWN"
if is_microsoft_owner "$owner_org"; then
continue
fi
while IFS= read -r access; do
perm_type="$(jq -r '.type' <<<"$access")"
perm_id="$(jq -r '.id' <<<"$access")"
echo -e "${app_name}\t${app_id}\t${resource_name}\t${resource_app_id}\t${owner_org}\t${perm_type}\t${perm_id}"
done < <(jq -c '.resourceAccess[]' <<<"$rra")
done < <(jq -c '.[2][]' <<<"$row")
done < <(jq -c '.[]' <<<"$apps_json")
```
</details>
{{#endtab }}
{{#tab name="Az" }}
@@ -1308,4 +1385,3 @@ The default mode is **Audit**:
{{#include ../../../banners/hacktricks-training.md}}