Replace brittle managed identity enumeration examples

This commit is contained in:
Carlos Polop
2026-05-05 16:27:01 +02:00
parent 393c6997b1
commit 406b2549aa
@@ -889,44 +889,17 @@ However, not every process will necessarily get the same practical result:
Therefore, if a request works from Run Command but fails from SSH, the usual explanation is a difference in OS user, environment, routing, proxy, firewall, or namespace, not a general Azure rule that only agent execution contexts can reach `168.63.129.16`.
### Run Command vs SSH Context
### Managed Identity Access From Inside the VM
Azure provides multiple ways to execute commands inside a VM, but **they do not run in the same context**.
The reliable way to use a VM's managed identities is the **IMDS managed identity endpoint** at `169.254.169.254`, not the WireServer `ExtensionsConfig` XML. Scripts that only search `ExtensionsConfig` for `UserAssignedIdentity` nodes are not reliable because:
#### Run Command
- The VM's managed identity assignment is not guaranteed to be represented as `UserAssignedIdentity` nodes in extension XML.
- They miss **system-assigned managed identities**.
- They only find user-assigned identities if the current GoalState/extension data happens to expose the expected XML shape.
Run Command is an Azure feature that executes scripts via the **VM Agent**.
Microsoft's documented security model is that **all code running on the VM can request tokens for the managed identities available on that VM**. If several user-assigned identities exist, request a specific one with `client_id`, `object_id`, or `msi_res_id`.
- Uses: `Microsoft.Compute/virtualMachines/runCommand/action`
- Runs through the **Azure VM Agent**
- Usually runs with elevated local privileges (`root` on Linux or `SYSTEM` on Windows)
- Can often reach WireServer/GoalState/ExtensionsConfig even when a low-privileged user is blocked by local controls
Example:
```bash
az vm run-command invoke \
--resource-group <rsc-group> \
--name <vm-name> \
--command-id RunShellScript \
--scripts @script.sh
```
#### SSH Session
When connecting via SSH:
- Runs as a **regular OS user**
- Uses standard network stack
- Does **not** have VM Agent privileges by default
As a result:
- Requests to `168.63.129.16` can work from SSH if the guest configuration allows it
- Requests may fail if blocked by local firewall, proxy, routing, network namespace, or user-level controls
- GoalState requests require the correct endpoint path and headers
**Script Examples to get attached managed identities:**
The following examples first request a token. Then they try to read the VM resource from Azure Resource Manager and print its `identity` property. The second step only works if the managed identity has permissions such as `Microsoft.Compute/virtualMachines/read` on the VM.
{{#tabs }}
{{#tab name="Linux" }}
@@ -935,193 +908,133 @@ As a result:
#!/usr/bin/env bash
set -euo pipefail
ws="http://168.63.129.16"
imds="http://169.254.169.254/metadata"
api_version="2021-02-01"
resource="${1:-https://management.azure.com/}"
echo "[*] Getting Goal State..."
# Optional. Examples:
# export MSI_SELECTOR='client_id=<client-id>'
# export MSI_SELECTOR='object_id=<principal-id>'
# export MSI_SELECTOR='msi_res_id=/subscriptions/.../userAssignedIdentities/name'
selector="${MSI_SELECTOR:-}"
goal_urls=(
"$ws/machine?comp=goalstate"
"$ws/machine/?comp=goalstate"
)
urlencode() {
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
}
goal_xml=""
for url in "${goal_urls[@]}"; do
if goal_xml="$(curl -fsS -H "x-ms-version: 2012-11-30" "$url" 2>/dev/null)"; then
echo "[+] GoalState OK via $url"
break
fi
done
if [[ -z "$goal_xml" ]]; then
echo "[-] No GoalState endpoint responded"
exit 1
token_url="$imds/identity/oauth2/token?api-version=$api_version&resource=$(urlencode "$resource")"
if [[ -n "$selector" ]]; then
token_url="$token_url&$selector"
fi
ext_url="$(
GOAL_XML="$goal_xml" python3 - <<'PY'
import os
import xml.etree.ElementTree as ET
echo "[*] Requesting managed identity token for: $resource"
token_json="$(curl -fsS --noproxy "*" -H "Metadata:true" "$token_url")"
xml = os.environ["GOAL_XML"].strip()
root = ET.fromstring(xml)
def lname(tag):
return tag.rsplit("}", 1)[-1]
for el in root.iter():
if lname(el.tag) == "ExtensionsConfig" and (el.text or "").strip():
print(el.text.strip())
break
access_token="$(
TOKEN_JSON="$token_json" python3 - <<'PY'
import json, os
print(json.loads(os.environ["TOKEN_JSON"])["access_token"])
PY
)"
if [[ -z "$ext_url" ]]; then
echo "[-] No ExtensionsConfig URL found in GoalState"
echo "[*] Identity-like nodes seen in GoalState:"
GOAL_XML="$goal_xml" python3 - <<'PY'
import os
import xml.etree.ElementTree as ET
TOKEN="$access_token" python3 - <<'PY'
import base64, json, os
xml = os.environ["GOAL_XML"].strip()
root = ET.fromstring(xml)
token = os.environ["TOKEN"]
payload = token.split(".")[1]
payload += "=" * (-len(payload) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload))
def lname(tag):
return tag.rsplit("}", 1)[-1]
found = False
for el in root.iter():
name = lname(el.tag)
if "Identity" in name:
found = True
text = (el.text or "").strip()
print(f"<{name}>{text}</{name}>")
if not found:
print(" (none)")
print("[+] Token acquired")
for key in ("tid", "appid", "oid", "xms_mirid"):
if key in claims:
print(f" {key}: {claims[key]}")
PY
exit 0
echo "[*] Trying to read the VM identity property through ARM..."
compute_json="$(curl -fsS --noproxy "*" -H "Metadata:true" "$imds/instance/compute?api-version=$api_version")"
vm_id="$(
COMPUTE_JSON="$compute_json" python3 - <<'PY'
import json, os
print(json.loads(os.environ["COMPUTE_JSON"])["resourceId"])
PY
)"
arm_url="https://management.azure.com${vm_id}?api-version=2024-07-01"
if vm_json="$(curl -fsS -H "Authorization: Bearer $access_token" "$arm_url" 2>/dev/null)"; then
VM_JSON="$vm_json" python3 - <<'PY'
import json, os
vm = json.loads(os.environ["VM_JSON"])
print(json.dumps(vm.get("identity", {}), indent=2))
PY
else
echo "[-] Could not read the VM resource with this identity. The token may still be valid, but it lacks ARM read permissions on the VM."
fi
echo "[*] Getting ExtensionsConfig..."
ext_xml="$(curl -fsS -H "x-ms-version: 2012-11-30" "$ext_url")"
EXT_XML="$ext_xml" python3 - <<'PY'
import os
import xml.etree.ElementTree as ET
xml = os.environ["EXT_XML"].strip()
root = ET.fromstring(xml)
def lname(tag):
return tag.rsplit("}", 1)[-1]
ids = [el for el in root.iter() if lname(el.tag) == "UserAssignedIdentity"]
if not ids:
print("[-] No UserAssignedIdentity nodes found")
print("[*] Identity-like nodes present in ExtensionsConfig:")
shown = False
for el in root.iter():
name = lname(el.tag)
if "Identity" in name:
shown = True
text = (el.text or "").strip()
attrs = " ".join(f'{k}="{v}"' for k, v in el.attrib.items())
if attrs:
print(f" <{name} {attrs}>{text}</{name}>")
else:
print(f" <{name}>{text}</{name}>")
if not shown:
print(" (none)")
raise SystemExit(0)
for idnode in ids:
client_id = ""
object_id = ""
resource_id = ""
for child in idnode.iter():
name = lname(child.tag)
text = (child.text or "").strip()
if name == "IdentityClientId":
client_id = text
elif name == "IdentityObjectId":
object_id = text
elif name == "IdentityResourceId":
resource_id = text
print()
print("[+] Managed Identity:")
print(f" ClientId : {client_id}")
print(f" ObjectId : {object_id}")
print(f" ResourceId : {resource_id}")
PY
```
{{#endtab }}
{{#tab name="Windows" }}
```bash
$ws = "http://168.63.129.16"
$h = @{
"x-ms-version" = "2012-11-30"
```powershell
$imds = "http://169.254.169.254/metadata"
$apiVersion = "2021-02-01"
$resource = "https://management.azure.com/"
# Optional. Examples:
# $env:MSI_SELECTOR = "client_id=<client-id>"
# $env:MSI_SELECTOR = "object_id=<principal-id>"
# $env:MSI_SELECTOR = "msi_res_id=/subscriptions/.../userAssignedIdentities/name"
$selector = $env:MSI_SELECTOR
function Invoke-Imds {
param([string]$Uri)
$params = @{
Method = "GET"
Uri = $Uri
Headers = @{ Metadata = "true" }
UseBasicParsing = $true
}
if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey("NoProxy")) {
$params.NoProxy = $true
}
Invoke-RestMethod @params
}
Write-Host "[*] Getting Goal State..." -ForegroundColor Cyan
function Decode-JwtPayload {
param([string]$Token)
$goalUrls = @(
"$ws/machine?comp=goalstate",
"$ws/machine/?comp=goalstate"
)
$gs = $null
foreach ($url in $goalUrls) {
try {
$gs = Invoke-WebRequest -Uri $url -Headers $h -UseBasicParsing -ErrorAction Stop
Write-Host "[+] GoalState OK via $url" -ForegroundColor Green
break
} catch {}
$payload = $Token.Split(".")[1].Replace("-", "+").Replace("_", "/")
switch ($payload.Length % 4) {
2 { $payload += "==" }
3 { $payload += "=" }
}
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json
}
if (-not $gs) {
Write-Host "[-] No GoalState endpoint responded" -ForegroundColor Red
return
$encodedResource = [uri]::EscapeDataString($resource)
$tokenUri = "$imds/identity/oauth2/token?api-version=$apiVersion&resource=$encodedResource"
if ($selector) {
$tokenUri = "$tokenUri&$selector"
}
[xml]$xml = $gs.Content
$cfg = $xml.GoalState.Container.RoleInstanceList.RoleInstance.Configuration
Write-Host "[*] Requesting managed identity token for: $resource"
$tokenResponse = Invoke-Imds -Uri $tokenUri
$claims = Decode-JwtPayload -Token $tokenResponse.access_token
$extUrl = $cfg.ExtensionsConfig
Write-Host "[+] Token acquired"
$claims | Select-Object tid, appid, oid, xms_mirid | Format-List
Write-Host "[*] Getting ExtensionsConfig..." -ForegroundColor Cyan
Write-Host "[*] Trying to read the VM identity property through ARM..."
$compute = Invoke-Imds -Uri "$imds/instance/compute?api-version=$apiVersion"
$armUri = "https://management.azure.com$($compute.resourceId)?api-version=2024-07-01"
try {
$ext = Invoke-WebRequest -Uri $extUrl -Headers $h -UseBasicParsing -ErrorAction Stop
[xml]$extXml = $ext.Content
$vm = Invoke-RestMethod -Method GET -Uri $armUri -Headers @{ Authorization = "Bearer $($tokenResponse.access_token)" } -UseBasicParsing
$vm.identity | ConvertTo-Json -Depth 20
} catch {
Write-Host "[-] Error getting ExtensionsConfig" -ForegroundColor Red
return
}
# Extract Managed Identity info
$ids = $extXml.SelectNodes("//UserAssignedIdentity")
if (!$ids) {
Write-Host "[-] No User Assigned Identities found" -ForegroundColor Red
return
}
foreach ($id in $ids) {
$clientId = $id.IdentityClientId
$objectId = $id.IdentityObjectId
$resourceId = $id.IdentityResourceId
Write-Host "`n[+] Managed Identity:" -ForegroundColor Green
Write-Host " ClientId : $clientId"
Write-Host " ObjectId : $objectId"
Write-Host " ResourceId : $resourceId"
Write-Host "[-] Could not read the VM resource with this identity. The token may still be valid, but it lacks ARM read permissions on the VM."
}
```
@@ -1158,6 +1071,7 @@ foreach ($id in $ids) {
- [https://learn.microsoft.com/en-us/azure/virtual-machines/overview](https://learn.microsoft.com/en-us/azure/virtual-machines/overview)
- [https://hausec.com/2022/05/04/azure-virtual-machine-execution-techniques/](https://hausec.com/2022/05/04/azure-virtual-machine-execution-techniques/)
- [https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service](https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service)
- [https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token)
- [https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16](https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16)
- [https://learn.microsoft.com/en-us/azure/virtual-machines/linux/no-agent](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/no-agent)
- [https://learn.microsoft.com/en-us/azure/virtual-machines/run-command](https://learn.microsoft.com/en-us/azure/virtual-machines/run-command)
@@ -1165,5 +1079,3 @@ foreach ($id in $ids) {
- [https://www.cybercx.com.au/blog/azure-ssrf-metadata/](https://www.cybercx.com.au/blog/azure-ssrf-metadata/)
{{#include ../../../../banners/hacktricks-training.md}}