From b0cd651c8d4a376e6d007cc5548e405f21c14d18 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 5 May 2026 14:25:14 +0200 Subject: [PATCH 1/7] Add WireServer & GoalState --- .../azure-security/az-services/vms/README.md | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) diff --git a/src/pentesting-cloud/azure-security/az-services/vms/README.md b/src/pentesting-cloud/azure-security/az-services/vms/README.md index 248ce6bd8..c667f9ce7 100644 --- a/src/pentesting-cloud/azure-security/az-services/vms/README.md +++ b/src/pentesting-cloud/azure-security/az-services/vms/README.md @@ -837,6 +837,317 @@ Invoke-AzureRmVMBulkCMD -Script Mimikatz.ps1 -Verbose -output Output.txt {{#endtab }} {{#endtabs }} +## Azure WireServer & GoalState + +Azure VMs expose **internal platform endpoints** that are used for configuration, metadata retrieval and identity management. Understanding the difference between them is critical for **enumeration, privilege escalation and post-exploitation**. + +--- + +### Wire Server (Azure Fabric Endpoint) + +The **Azure WireServer** is an internal Azure IP (`168.63.129.16`) used by the platform to communicate with the VM. + +It is responsible for: + +- Communication with the **VM Agent** +- Delivering: + - **GoalState** + - **ExtensionsConfig** + - Internal VM configuration (including identities) +- DHCP & DNS services +- Health monitoring + +--- + +### GoalState & ExtensionsConfig + +The **GoalState** represents the **desired configuration of the VM** as defined by Azure. It may include: + +- Extensions configuration +- Managed identities +- Provisioning state +- Agent instructions + +The **ExtensionsConfig** contains detailed configuration of VM extensions and may include: + +- **User Assigned Managed Identities** +- Extension settings +- Secrets (depending on extension) + +These endpoints are typically accessed via: + +```bash +curl -H "x-ms-version: 2012-11-30" http://168.63.129.16/?comp=goalstate +``` + +### Access Restrictions + +Although the endpoint is reachable from the VM network, **it is not equally accessible from all contexts**. + +**Accessible from**: + +- Azure **VM Agent** +- Azure **Run Command** +- **VM Extensions** + +**Not reliably accessible from**: + +- Interactive SSH sessions (e.g., `azureuser`) +- Unprivileged processes inside the VM + +This is because: + +- The WireServer is designed for **platform-agent communication** +- Requests may require **specific headers, timing, or context** +- Some responses are only available to the **VM Agent execution environment** + +--- + +### Run Command vs SSH Context + +Azure provides multiple ways to execute commands inside a VM, but **they do not run in the same context**. + +--- + +#### Run Command + +Run Command is an Azure feature that executes scripts via the **VM Agent**. + +- Uses: `Microsoft.Compute/virtualMachines/runCommand/action` +- Runs with **agent-level privileges** +- Has access to: + - WireServer + - GoalState + - ExtensionsConfig + +Example: + +```bash +az vm run-command invoke \ + --resource-group \ + --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 agent-level access** + +As a result: + +- Requests to `168.63.129.16` may fail or return incomplete data +- GoalState may not be accessible + +**Script Examples:** + +{{#tabs }} +{{#tab name="Linux" }} + +```bash +#!/usr/bin/env bash +set -euo pipefail + +ws="http://168.63.129.16" + +echo "[*] Getting Goal State..." + +goal_urls=( + "$ws/?comp=goalstate" + "$ws/machine?comp=goalstate" + "$ws/machine/?comp=goalstate" +) + +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 +fi + +ext_url="$( + GOAL_XML="$goal_xml" python3 - <<'PY' +import os +import xml.etree.ElementTree as ET + +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 +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 + +xml = os.environ["GOAL_XML"].strip() +root = ET.fromstring(xml) + +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}") + +if not found: + print(" (none)") +PY + exit 0 +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}") + else: + print(f" <{name}>{text}") + 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" +} + +Write-Host "[*] Getting Goal State..." -ForegroundColor Cyan + +$goalUrls = @( + "$ws/?comp=goalstate", + "$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 {} +} + +if (-not $gs) { + Write-Host "[-] No GoalState endpoint responded" -ForegroundColor Red + return +} + +[xml]$xml = $gs.Content +$cfg = $xml.GoalState.Container.RoleInstanceList.RoleInstance.Configuration + +$extUrl = $cfg.ExtensionsConfig + +Write-Host "[*] Getting ExtensionsConfig..." -ForegroundColor Cyan + +try { + $ext = Invoke-WebRequest -Uri $extUrl -Headers $h -UseBasicParsing -ErrorAction Stop + [xml]$extXml = $ext.Content +} 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" +} +``` + +{{#endtab }} +{{#endtabs }} + + ## Privilege Escalation {{#ref}} @@ -866,6 +1177,9 @@ Invoke-AzureRmVMBulkCMD -Script Mimikatz.ps1 -Verbose -output Output.txt - [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/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/run-command](https://learn.microsoft.com/en-us/azure/virtual-machines/run-command) +- [https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux](https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux) {{#include ../../../../banners/hacktricks-training.md}} From 6f461640c2e32f542e2278dfa074ba4c540d0464 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 5 May 2026 14:33:29 +0200 Subject: [PATCH 2/7] Add WireServer & GoalState --- src/pentesting-cloud/azure-security/az-services/vms/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pentesting-cloud/azure-security/az-services/vms/README.md b/src/pentesting-cloud/azure-security/az-services/vms/README.md index c667f9ce7..c413ed64a 100644 --- a/src/pentesting-cloud/azure-security/az-services/vms/README.md +++ b/src/pentesting-cloud/azure-security/az-services/vms/README.md @@ -943,7 +943,7 @@ As a result: - Requests to `168.63.129.16` may fail or return incomplete data - GoalState may not be accessible -**Script Examples:** +**Script Examples to get attached managed identities:** {{#tabs }} {{#tab name="Linux" }} From 2f2df45366b55dc5a5d74022ae5195a0f51601a3 Mon Sep 17 00:00:00 2001 From: SirBroccoli Date: Tue, 5 May 2026 15:31:53 +0200 Subject: [PATCH 3/7] Update README.md --- .../azure-security/az-services/vms/README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pentesting-cloud/azure-security/az-services/vms/README.md b/src/pentesting-cloud/azure-security/az-services/vms/README.md index c413ed64a..241f71b05 100644 --- a/src/pentesting-cloud/azure-security/az-services/vms/README.md +++ b/src/pentesting-cloud/azure-security/az-services/vms/README.md @@ -841,8 +841,6 @@ Invoke-AzureRmVMBulkCMD -Script Mimikatz.ps1 -Verbose -output Output.txt Azure VMs expose **internal platform endpoints** that are used for configuration, metadata retrieval and identity management. Understanding the difference between them is critical for **enumeration, privilege escalation and post-exploitation**. ---- - ### Wire Server (Azure Fabric Endpoint) The **Azure WireServer** is an internal Azure IP (`168.63.129.16`) used by the platform to communicate with the VM. @@ -857,8 +855,6 @@ It is responsible for: - DHCP & DNS services - Health monitoring ---- - ### GoalState & ExtensionsConfig The **GoalState** represents the **desired configuration of the VM** as defined by Azure. It may include: @@ -901,14 +897,10 @@ This is because: - Requests may require **specific headers, timing, or context** - Some responses are only available to the **VM Agent execution environment** ---- - ### Run Command vs SSH Context Azure provides multiple ways to execute commands inside a VM, but **they do not run in the same context**. ---- - #### Run Command Run Command is an Azure feature that executes scripts via the **VM Agent**. From 393c6997b1db404e75b5c6134588dd1d5f6688f2 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 5 May 2026 16:16:00 +0200 Subject: [PATCH 4/7] Clarify Azure WireServer access contexts --- .../azure-security/az-services/vms/README.md | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/src/pentesting-cloud/azure-security/az-services/vms/README.md b/src/pentesting-cloud/azure-security/az-services/vms/README.md index 241f71b05..4f08f6e7f 100644 --- a/src/pentesting-cloud/azure-security/az-services/vms/README.md +++ b/src/pentesting-cloud/azure-security/az-services/vms/README.md @@ -873,29 +873,21 @@ The **ExtensionsConfig** contains detailed configuration of VM extensions and ma These endpoints are typically accessed via: ```bash -curl -H "x-ms-version: 2012-11-30" http://168.63.129.16/?comp=goalstate +curl -H "x-ms-version: 2012-11-30" http://168.63.129.16/machine?comp=goalstate ``` -### Access Restrictions +### Access considerations -Although the endpoint is reachable from the VM network, **it is not equally accessible from all contexts**. +The WireServer IP is generally reachable from inside the VM through the guest network stack. It is not restricted only to the Azure VM Agent, Run Command, or VM extensions. Microsoft even documents agentless Linux provisioning examples where ordinary in-guest scripts query GoalState directly from `168.63.129.16`. -**Accessible from**: +However, not every process will necessarily get the same practical result: -- Azure **VM Agent** -- Azure **Run Command** -- **VM Extensions** +- Some endpoints require Azure-specific headers, such as `x-ms-version: 2012-11-30` for GoalState. +- Local guest controls can block or alter access, including host firewall rules, proxies, routes, network namespaces, containers, or endpoint protection. +- VM extensions and Run Command commonly execute as `root`/`SYSTEM` through the VM Agent, so they may bypass local OS restrictions that affect an interactive user. +- Some data is agent/extension-specific and may depend on the VM's provisioning state, installed agent, configured extensions, or managed identity configuration. -**Not reliably accessible from**: - -- Interactive SSH sessions (e.g., `azureuser`) -- Unprivileged processes inside the VM - -This is because: - -- The WireServer is designed for **platform-agent communication** -- Requests may require **specific headers, timing, or context** -- Some responses are only available to the **VM Agent execution environment** +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 @@ -906,11 +898,9 @@ Azure provides multiple ways to execute commands inside a VM, but **they do not Run Command is an Azure feature that executes scripts via the **VM Agent**. - Uses: `Microsoft.Compute/virtualMachines/runCommand/action` -- Runs with **agent-level privileges** -- Has access to: - - WireServer - - GoalState - - ExtensionsConfig +- 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: @@ -928,12 +918,13 @@ When connecting via SSH: - Runs as a **regular OS user** - Uses standard network stack -- Does **NOT have agent-level access** +- Does **not** have VM Agent privileges by default As a result: -- Requests to `168.63.129.16` may fail or return incomplete data -- GoalState may not be accessible +- 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:** @@ -949,7 +940,6 @@ ws="http://168.63.129.16" echo "[*] Getting Goal State..." goal_urls=( - "$ws/?comp=goalstate" "$ws/machine?comp=goalstate" "$ws/machine/?comp=goalstate" ) @@ -1081,7 +1071,6 @@ $h = @{ Write-Host "[*] Getting Goal State..." -ForegroundColor Cyan $goalUrls = @( - "$ws/?comp=goalstate", "$ws/machine?comp=goalstate", "$ws/machine/?comp=goalstate" ) @@ -1169,11 +1158,12 @@ 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/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/run-command](https://learn.microsoft.com/en-us/azure/virtual-machines/run-command) -- [https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux](https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux) +- [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) +- [https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux](https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux) +- [https://www.cybercx.com.au/blog/azure-ssrf-metadata/](https://www.cybercx.com.au/blog/azure-ssrf-metadata/) {{#include ../../../../banners/hacktricks-training.md}} - From 406b2549aa858eab719db01a68093c267bb44f5e Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 5 May 2026 16:27:01 +0200 Subject: [PATCH 5/7] Replace brittle managed identity enumeration examples --- .../azure-security/az-services/vms/README.md | 296 ++++++------------ 1 file changed, 104 insertions(+), 192 deletions(-) diff --git a/src/pentesting-cloud/azure-security/az-services/vms/README.md b/src/pentesting-cloud/azure-security/az-services/vms/README.md index 4f08f6e7f..ff2aadbe4 100644 --- a/src/pentesting-cloud/azure-security/az-services/vms/README.md +++ b/src/pentesting-cloud/azure-security/az-services/vms/README.md @@ -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 \ - --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=' +# export MSI_SELECTOR='object_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}") - -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}") - else: - print(f" <{name}>{text}") - 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=" +# $env:MSI_SELECTOR = "object_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}} - - From 2a4cc7c42883dc22721cc1072eba3136cab1defe Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 5 May 2026 17:45:43 +0200 Subject: [PATCH 6/7] Document Azure MI token discovery limits --- .../azure-security/az-services/vms/README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pentesting-cloud/azure-security/az-services/vms/README.md b/src/pentesting-cloud/azure-security/az-services/vms/README.md index ff2aadbe4..b269d5d9b 100644 --- a/src/pentesting-cloud/azure-security/az-services/vms/README.md +++ b/src/pentesting-cloud/azure-security/az-services/vms/README.md @@ -889,6 +889,8 @@ 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`. +In lab testing this distinction was visible: Linux/Windows VM Agent execution through Run Command or Custom Script extensions could reach GoalState on `168.63.129.16`, while a normal SSH session on another Linux VM could still reach IMDS but timed out when querying GoalState. Treat WireServer/GoalState as useful but environment-dependent; do not rely on it as the canonical way to enumerate managed identities. + ### Managed Identity Access From Inside the VM 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: @@ -897,7 +899,19 @@ The reliable way to use a VM's managed identities is the **IMDS managed identity - 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. -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`. +Microsoft's documented security model is that **all code running on the VM can request tokens for the managed identities available on that VM**. This was confirmed from: + +- Linux SSH as a regular VM user. +- Linux Run Command through the VM Agent. +- Linux Custom Script extension through the VM Agent. +- Windows Custom Script extension as `NT AUTHORITY\SYSTEM`. + +In all of those contexts, IMDS could mint tokens for Management, Microsoft Graph/Entra ID, Key Vault, and Storage when the requested identity was available to the VM. + +There are two different problems that are easy to mix up: + +- **Getting a token for a known identity:** If the identity is assigned to the VM, IMDS can issue tokens for different audiences such as `https://management.azure.com/`, `https://graph.microsoft.com/`, `https://vault.azure.net`, and `https://storage.azure.com/`. If several user-assigned identities exist, request a specific one with `client_id`, `object_id`, or `msi_res_id`. +- **Discovering every attached identity from inside the VM:** IMDS does not provide a simple "list all identities" endpoint. A practical method is to get a default Management token, read the VM resource through ARM, and inspect the `identity` property. This only works if that managed identity has permissions such as `Microsoft.Compute/virtualMachines/read` on the VM. If ARM returns `403`, the token can still be valid and useful, but it cannot enumerate the VM's full identity list. 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. From 2fe01e873a71f5b8eec00131f19ab360f0050194 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 5 May 2026 17:50:24 +0200 Subject: [PATCH 7/7] Mention WireServer MI selector fallback --- src/pentesting-cloud/azure-security/az-services/vms/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pentesting-cloud/azure-security/az-services/vms/README.md b/src/pentesting-cloud/azure-security/az-services/vms/README.md index b269d5d9b..0ce3e86b3 100644 --- a/src/pentesting-cloud/azure-security/az-services/vms/README.md +++ b/src/pentesting-cloud/azure-security/az-services/vms/README.md @@ -913,6 +913,8 @@ There are two different problems that are easy to mix up: - **Getting a token for a known identity:** If the identity is assigned to the VM, IMDS can issue tokens for different audiences such as `https://management.azure.com/`, `https://graph.microsoft.com/`, `https://vault.azure.net`, and `https://storage.azure.com/`. If several user-assigned identities exist, request a specific one with `client_id`, `object_id`, or `msi_res_id`. - **Discovering every attached identity from inside the VM:** IMDS does not provide a simple "list all identities" endpoint. A practical method is to get a default Management token, read the VM resource through ARM, and inspect the `identity` property. This only works if that managed identity has permissions such as `Microsoft.Compute/virtualMachines/read` on the VM. If ARM returns `403`, the token can still be valid and useful, but it cannot enumerate the VM's full identity list. +If ARM discovery fails, you can still try WireServer/HostGAPlugin sources such as GoalState and `http://168.63.129.16:32526/vmSettings` to look for identity-looking fields (`clientId`, `IdentityClientId`, `msi_res_id`, user-assigned identity resource IDs) and then ask IMDS for tokens with those selectors. This is a fallback, not a guarantee: those endpoints are context-dependent and may expose no managed identity selectors at all. + 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 }}