Skip to content

Commit 406b254

Browse files
committed
Replace brittle managed identity enumeration examples
1 parent 393c699 commit 406b254

1 file changed

Lines changed: 104 additions & 192 deletions

File tree

  • src/pentesting-cloud/azure-security/az-services/vms

src/pentesting-cloud/azure-security/az-services/vms/README.md

Lines changed: 104 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -889,44 +889,17 @@ However, not every process will necessarily get the same practical result:
889889
890890
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`.
891891
892-
### Run Command vs SSH Context
892+
### Managed Identity Access From Inside the VM
893893
894-
Azure provides multiple ways to execute commands inside a VM, but **they do not run in the same context**.
894+
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:
895895
896-
#### Run Command
896+
- The VM's managed identity assignment is not guaranteed to be represented as `UserAssignedIdentity` nodes in extension XML.
897+
- They miss **system-assigned managed identities**.
898+
- They only find user-assigned identities if the current GoalState/extension data happens to expose the expected XML shape.
897899
898-
Run Command is an Azure feature that executes scripts via the **VM Agent**.
900+
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`.
899901
900-
- Uses: `Microsoft.Compute/virtualMachines/runCommand/action`
901-
- Runs through the **Azure VM Agent**
902-
- Usually runs with elevated local privileges (`root` on Linux or `SYSTEM` on Windows)
903-
- Can often reach WireServer/GoalState/ExtensionsConfig even when a low-privileged user is blocked by local controls
904-
905-
Example:
906-
907-
```bash
908-
az vm run-command invoke \
909-
--resource-group <rsc-group> \
910-
--name <vm-name> \
911-
--command-id RunShellScript \
912-
--scripts @script.sh
913-
```
914-
915-
#### SSH Session
916-
917-
When connecting via SSH:
918-
919-
- Runs as a **regular OS user**
920-
- Uses standard network stack
921-
- Does **not** have VM Agent privileges by default
922-
923-
As a result:
924-
925-
- Requests to `168.63.129.16` can work from SSH if the guest configuration allows it
926-
- Requests may fail if blocked by local firewall, proxy, routing, network namespace, or user-level controls
927-
- GoalState requests require the correct endpoint path and headers
928-
929-
**Script Examples to get attached managed identities:**
902+
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.
930903
931904
{{#tabs }}
932905
{{#tab name="Linux" }}
@@ -935,193 +908,133 @@ As a result:
935908
#!/usr/bin/env bash
936909
set -euo pipefail
937910
938-
ws="http://168.63.129.16"
939-
940-
echo "[*] Getting Goal State..."
911+
imds="http://169.254.169.254/metadata"
912+
api_version="2021-02-01"
913+
resource="${1:-https://management.azure.com/}"
941914
942-
goal_urls=(
943-
"$ws/machine?comp=goalstate"
944-
"$ws/machine/?comp=goalstate"
945-
)
915+
# Optional. Examples:
916+
# export MSI_SELECTOR='client_id=<client-id>'
917+
# export MSI_SELECTOR='object_id=<principal-id>'
918+
# export MSI_SELECTOR='msi_res_id=/subscriptions/.../userAssignedIdentities/name'
919+
selector="${MSI_SELECTOR:-}"
946920
947-
goal_xml=""
948-
for url in "${goal_urls[@]}"; do
949-
if goal_xml="$(curl -fsS -H "x-ms-version: 2012-11-30" "$url" 2>/dev/null)"; then
950-
echo "[+] GoalState OK via $url"
951-
break
952-
fi
953-
done
921+
urlencode() {
922+
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
923+
}
954924
955-
if [[ -z "$goal_xml" ]]; then
956-
echo "[-] No GoalState endpoint responded"
957-
exit 1
925+
token_url="$imds/identity/oauth2/token?api-version=$api_version&resource=$(urlencode "$resource")"
926+
if [[ -n "$selector" ]]; then
927+
token_url="$token_url&$selector"
958928
fi
959929
960-
ext_url="$(
961-
GOAL_XML="$goal_xml" python3 - <<'PY'
962-
import os
963-
import xml.etree.ElementTree as ET
930+
echo "[*] Requesting managed identity token for: $resource"
931+
token_json="$(curl -fsS --noproxy "*" -H "Metadata:true" "$token_url")"
932+
933+
access_token="$(
934+
TOKEN_JSON="$token_json" python3 - <<'PY'
935+
import json, os
936+
print(json.loads(os.environ["TOKEN_JSON"])["access_token"])
937+
PY
938+
)"
964939
965-
xml = os.environ["GOAL_XML"].strip()
966-
root = ET.fromstring(xml)
940+
TOKEN="$access_token" python3 - <<'PY'
941+
import base64, json, os
967942
968-
def lname(tag):
969-
return tag.rsplit("}", 1)[-1]
943+
token = os.environ["TOKEN"]
944+
payload = token.split(".")[1]
945+
payload += "=" * (-len(payload) % 4)
946+
claims = json.loads(base64.urlsafe_b64decode(payload))
970947
971-
for el in root.iter():
972-
if lname(el.tag) == "ExtensionsConfig" and (el.text or "").strip():
973-
print(el.text.strip())
974-
break
948+
print("[+] Token acquired")
949+
for key in ("tid", "appid", "oid", "xms_mirid"):
950+
if key in claims:
951+
print(f" {key}: {claims[key]}")
975952
PY
976-
)"
977953
978-
if [[ -z "$ext_url" ]]; then
979-
echo "[-] No ExtensionsConfig URL found in GoalState"
980-
echo "[*] Identity-like nodes seen in GoalState:"
981-
GOAL_XML="$goal_xml" python3 - <<'PY'
982-
import os
983-
import xml.etree.ElementTree as ET
984-
985-
xml = os.environ["GOAL_XML"].strip()
986-
root = ET.fromstring(xml)
987-
988-
def lname(tag):
989-
return tag.rsplit("}", 1)[-1]
990-
991-
found = False
992-
for el in root.iter():
993-
name = lname(el.tag)
994-
if "Identity" in name:
995-
found = True
996-
text = (el.text or "").strip()
997-
print(f"<{name}>{text}</{name}>")
998-
999-
if not found:
1000-
print(" (none)")
954+
echo "[*] Trying to read the VM identity property through ARM..."
955+
compute_json="$(curl -fsS --noproxy "*" -H "Metadata:true" "$imds/instance/compute?api-version=$api_version")"
956+
vm_id="$(
957+
COMPUTE_JSON="$compute_json" python3 - <<'PY'
958+
import json, os
959+
print(json.loads(os.environ["COMPUTE_JSON"])["resourceId"])
1001960
PY
1002-
exit 0
1003-
fi
961+
)"
1004962
1005-
echo "[*] Getting ExtensionsConfig..."
1006-
ext_xml="$(curl -fsS -H "x-ms-version: 2012-11-30" "$ext_url")"
1007-
1008-
EXT_XML="$ext_xml" python3 - <<'PY'
1009-
import os
1010-
import xml.etree.ElementTree as ET
1011-
1012-
xml = os.environ["EXT_XML"].strip()
1013-
root = ET.fromstring(xml)
1014-
1015-
def lname(tag):
1016-
return tag.rsplit("}", 1)[-1]
1017-
1018-
ids = [el for el in root.iter() if lname(el.tag) == "UserAssignedIdentity"]
1019-
1020-
if not ids:
1021-
print("[-] No UserAssignedIdentity nodes found")
1022-
print("[*] Identity-like nodes present in ExtensionsConfig:")
1023-
shown = False
1024-
for el in root.iter():
1025-
name = lname(el.tag)
1026-
if "Identity" in name:
1027-
shown = True
1028-
text = (el.text or "").strip()
1029-
attrs = " ".join(f'{k}="{v}"' for k, v in el.attrib.items())
1030-
if attrs:
1031-
print(f" <{name} {attrs}>{text}</{name}>")
1032-
else:
1033-
print(f" <{name}>{text}</{name}>")
1034-
if not shown:
1035-
print(" (none)")
1036-
raise SystemExit(0)
1037-
1038-
for idnode in ids:
1039-
client_id = ""
1040-
object_id = ""
1041-
resource_id = ""
1042-
1043-
for child in idnode.iter():
1044-
name = lname(child.tag)
1045-
text = (child.text or "").strip()
1046-
if name == "IdentityClientId":
1047-
client_id = text
1048-
elif name == "IdentityObjectId":
1049-
object_id = text
1050-
elif name == "IdentityResourceId":
1051-
resource_id = text
1052-
1053-
print()
1054-
print("[+] Managed Identity:")
1055-
print(f" ClientId : {client_id}")
1056-
print(f" ObjectId : {object_id}")
1057-
print(f" ResourceId : {resource_id}")
963+
arm_url="https://management.azure.com${vm_id}?api-version=2024-07-01"
964+
if vm_json="$(curl -fsS -H "Authorization: Bearer $access_token" "$arm_url" 2>/dev/null)"; then
965+
VM_JSON="$vm_json" python3 - <<'PY'
966+
import json, os
967+
vm = json.loads(os.environ["VM_JSON"])
968+
print(json.dumps(vm.get("identity", {}), indent=2))
1058969
PY
970+
else
971+
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."
972+
fi
1059973
```
1060974
1061975
{{#endtab }}
1062976
1063977
{{#tab name="Windows" }}
1064978
1065-
```bash
1066-
$ws = "http://168.63.129.16"
1067-
$h = @{
1068-
"x-ms-version" = "2012-11-30"
979+
```powershell
980+
$imds = "http://169.254.169.254/metadata"
981+
$apiVersion = "2021-02-01"
982+
$resource = "https://management.azure.com/"
983+
984+
# Optional. Examples:
985+
# $env:MSI_SELECTOR = "client_id=<client-id>"
986+
# $env:MSI_SELECTOR = "object_id=<principal-id>"
987+
# $env:MSI_SELECTOR = "msi_res_id=/subscriptions/.../userAssignedIdentities/name"
988+
$selector = $env:MSI_SELECTOR
989+
990+
function Invoke-Imds {
991+
param([string]$Uri)
992+
993+
$params = @{
994+
Method = "GET"
995+
Uri = $Uri
996+
Headers = @{ Metadata = "true" }
997+
UseBasicParsing = $true
998+
}
999+
if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey("NoProxy")) {
1000+
$params.NoProxy = $true
1001+
}
1002+
Invoke-RestMethod @params
10691003
}
10701004
1071-
Write-Host "[*] Getting Goal State..." -ForegroundColor Cyan
1072-
1073-
$goalUrls = @(
1074-
"$ws/machine?comp=goalstate",
1075-
"$ws/machine/?comp=goalstate"
1076-
)
1077-
1078-
$gs = $null
1005+
function Decode-JwtPayload {
1006+
param([string]$Token)
10791007
1080-
foreach ($url in $goalUrls) {
1081-
try {
1082-
$gs = Invoke-WebRequest -Uri $url -Headers $h -UseBasicParsing -ErrorAction Stop
1083-
Write-Host "[+] GoalState OK via $url" -ForegroundColor Green
1084-
break
1085-
} catch {}
1008+
$payload = $Token.Split(".")[1].Replace("-", "+").Replace("_", "/")
1009+
switch ($payload.Length % 4) {
1010+
2 { $payload += "==" }
1011+
3 { $payload += "=" }
1012+
}
1013+
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json
10861014
}
10871015
1088-
if (-not $gs) {
1089-
Write-Host "[-] No GoalState endpoint responded" -ForegroundColor Red
1090-
return
1016+
$encodedResource = [uri]::EscapeDataString($resource)
1017+
$tokenUri = "$imds/identity/oauth2/token?api-version=$apiVersion&resource=$encodedResource"
1018+
if ($selector) {
1019+
$tokenUri = "$tokenUri&$selector"
10911020
}
10921021
1093-
[xml]$xml = $gs.Content
1094-
$cfg = $xml.GoalState.Container.RoleInstanceList.RoleInstance.Configuration
1022+
Write-Host "[*] Requesting managed identity token for: $resource"
1023+
$tokenResponse = Invoke-Imds -Uri $tokenUri
1024+
$claims = Decode-JwtPayload -Token $tokenResponse.access_token
10951025
1096-
$extUrl = $cfg.ExtensionsConfig
1026+
Write-Host "[+] Token acquired"
1027+
$claims | Select-Object tid, appid, oid, xms_mirid | Format-List
10971028
1098-
Write-Host "[*] Getting ExtensionsConfig..." -ForegroundColor Cyan
1029+
Write-Host "[*] Trying to read the VM identity property through ARM..."
1030+
$compute = Invoke-Imds -Uri "$imds/instance/compute?api-version=$apiVersion"
1031+
$armUri = "https://management.azure.com$($compute.resourceId)?api-version=2024-07-01"
10991032
11001033
try {
1101-
$ext = Invoke-WebRequest -Uri $extUrl -Headers $h -UseBasicParsing -ErrorAction Stop
1102-
[xml]$extXml = $ext.Content
1034+
$vm = Invoke-RestMethod -Method GET -Uri $armUri -Headers @{ Authorization = "Bearer $($tokenResponse.access_token)" } -UseBasicParsing
1035+
$vm.identity | ConvertTo-Json -Depth 20
11031036
} catch {
1104-
Write-Host "[-] Error getting ExtensionsConfig" -ForegroundColor Red
1105-
return
1106-
}
1107-
1108-
# Extract Managed Identity info
1109-
$ids = $extXml.SelectNodes("//UserAssignedIdentity")
1110-
1111-
if (!$ids) {
1112-
Write-Host "[-] No User Assigned Identities found" -ForegroundColor Red
1113-
return
1114-
}
1115-
1116-
foreach ($id in $ids) {
1117-
$clientId = $id.IdentityClientId
1118-
$objectId = $id.IdentityObjectId
1119-
$resourceId = $id.IdentityResourceId
1120-
1121-
Write-Host "`n[+] Managed Identity:" -ForegroundColor Green
1122-
Write-Host " ClientId : $clientId"
1123-
Write-Host " ObjectId : $objectId"
1124-
Write-Host " ResourceId : $resourceId"
1037+
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."
11251038
}
11261039
```
11271040
@@ -1158,12 +1071,11 @@ foreach ($id in $ids) {
11581071
- [https://learn.microsoft.com/en-us/azure/virtual-machines/overview](https://learn.microsoft.com/en-us/azure/virtual-machines/overview)
11591072
- [https://hausec.com/2022/05/04/azure-virtual-machine-execution-techniques/](https://hausec.com/2022/05/04/azure-virtual-machine-execution-techniques/)
11601073
- [https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service](https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service)
1074+
- [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)
11611075
- [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)
11621076
- [https://learn.microsoft.com/en-us/azure/virtual-machines/linux/no-agent](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/no-agent)
11631077
- [https://learn.microsoft.com/en-us/azure/virtual-machines/run-command](https://learn.microsoft.com/en-us/azure/virtual-machines/run-command)
11641078
- [https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux](https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux)
11651079
- [https://www.cybercx.com.au/blog/azure-ssrf-metadata/](https://www.cybercx.com.au/blog/azure-ssrf-metadata/)
11661080
11671081
{{#include ../../../../banners/hacktricks-training.md}}
1168-
1169-

0 commit comments

Comments
 (0)