@@ -889,44 +889,17 @@ However, not every process will necessarily get the same practical result:
889889
890890Therefore, 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
936909set -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 "
958928fi
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]}")
975952PY
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"])
1001960PY
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))
1058969PY
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
11001033try {
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