Skip to content

Commit d13c270

Browse files
authored
Merge pull request #286 from JaimePolop/master
Add WireServer & GoalState
2 parents 28a5f23 + 2fe01e8 commit d13c270

1 file changed

Lines changed: 227 additions & 3 deletions

File tree

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

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

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,227 @@ Invoke-AzureRmVMBulkCMD -Script Mimikatz.ps1 -Verbose -output Output.txt
837837
{{#endtab }}
838838
{{#endtabs }}
839839
840+
## Azure WireServer & GoalState
841+
842+
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**.
843+
844+
### Wire Server (Azure Fabric Endpoint)
845+
846+
The **Azure WireServer** is an internal Azure IP (`168.63.129.16`) used by the platform to communicate with the VM.
847+
848+
It is responsible for:
849+
850+
- Communication with the **VM Agent**
851+
- Delivering:
852+
- **GoalState**
853+
- **ExtensionsConfig**
854+
- Internal VM configuration (including identities)
855+
- DHCP & DNS services
856+
- Health monitoring
857+
858+
### GoalState & ExtensionsConfig
859+
860+
The **GoalState** represents the **desired configuration of the VM** as defined by Azure. It may include:
861+
862+
- Extensions configuration
863+
- Managed identities
864+
- Provisioning state
865+
- Agent instructions
866+
867+
The **ExtensionsConfig** contains detailed configuration of VM extensions and may include:
868+
869+
- **User Assigned Managed Identities**
870+
- Extension settings
871+
- Secrets (depending on extension)
872+
873+
These endpoints are typically accessed via:
874+
875+
```bash
876+
curl -H "x-ms-version: 2012-11-30" http://168.63.129.16/machine?comp=goalstate
877+
```
878+
879+
### Access considerations
880+
881+
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`.
882+
883+
However, not every process will necessarily get the same practical result:
884+
885+
- Some endpoints require Azure-specific headers, such as `x-ms-version: 2012-11-30` for GoalState.
886+
- Local guest controls can block or alter access, including host firewall rules, proxies, routes, network namespaces, containers, or endpoint protection.
887+
- 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.
888+
- Some data is agent/extension-specific and may depend on the VM's provisioning state, installed agent, configured extensions, or managed identity configuration.
889+
890+
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`.
891+
892+
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.
893+
894+
### Managed Identity Access From Inside the VM
895+
896+
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+
898+
- The VM's managed identity assignment is not guaranteed to be represented as `UserAssignedIdentity` nodes in extension XML.
899+
- They miss **system-assigned managed identities**.
900+
- They only find user-assigned identities if the current GoalState/extension data happens to expose the expected XML shape.
901+
902+
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:
903+
904+
- Linux SSH as a regular VM user.
905+
- Linux Run Command through the VM Agent.
906+
- Linux Custom Script extension through the VM Agent.
907+
- Windows Custom Script extension as `NT AUTHORITY\SYSTEM`.
908+
909+
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.
910+
911+
There are two different problems that are easy to mix up:
912+
913+
- **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`.
914+
- **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.
915+
916+
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.
917+
918+
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.
919+
920+
{{#tabs }}
921+
{{#tab name="Linux" }}
922+
923+
```bash
924+
#!/usr/bin/env bash
925+
set -euo pipefail
926+
927+
imds="http://169.254.169.254/metadata"
928+
api_version="2021-02-01"
929+
resource="${1:-https://management.azure.com/}"
930+
931+
# Optional. Examples:
932+
# export MSI_SELECTOR='client_id=<client-id>'
933+
# export MSI_SELECTOR='object_id=<principal-id>'
934+
# export MSI_SELECTOR='msi_res_id=/subscriptions/.../userAssignedIdentities/name'
935+
selector="${MSI_SELECTOR:-}"
936+
937+
urlencode() {
938+
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
939+
}
940+
941+
token_url="$imds/identity/oauth2/token?api-version=$api_version&resource=$(urlencode "$resource")"
942+
if [[ -n "$selector" ]]; then
943+
token_url="$token_url&$selector"
944+
fi
945+
946+
echo "[*] Requesting managed identity token for: $resource"
947+
token_json="$(curl -fsS --noproxy "*" -H "Metadata:true" "$token_url")"
948+
949+
access_token="$(
950+
TOKEN_JSON="$token_json" python3 - <<'PY'
951+
import json, os
952+
print(json.loads(os.environ["TOKEN_JSON"])["access_token"])
953+
PY
954+
)"
955+
956+
TOKEN="$access_token" python3 - <<'PY'
957+
import base64, json, os
958+
959+
token = os.environ["TOKEN"]
960+
payload = token.split(".")[1]
961+
payload += "=" * (-len(payload) % 4)
962+
claims = json.loads(base64.urlsafe_b64decode(payload))
963+
964+
print("[+] Token acquired")
965+
for key in ("tid", "appid", "oid", "xms_mirid"):
966+
if key in claims:
967+
print(f" {key}: {claims[key]}")
968+
PY
969+
970+
echo "[*] Trying to read the VM identity property through ARM..."
971+
compute_json="$(curl -fsS --noproxy "*" -H "Metadata:true" "$imds/instance/compute?api-version=$api_version")"
972+
vm_id="$(
973+
COMPUTE_JSON="$compute_json" python3 - <<'PY'
974+
import json, os
975+
print(json.loads(os.environ["COMPUTE_JSON"])["resourceId"])
976+
PY
977+
)"
978+
979+
arm_url="https://management.azure.com${vm_id}?api-version=2024-07-01"
980+
if vm_json="$(curl -fsS -H "Authorization: Bearer $access_token" "$arm_url" 2>/dev/null)"; then
981+
VM_JSON="$vm_json" python3 - <<'PY'
982+
import json, os
983+
vm = json.loads(os.environ["VM_JSON"])
984+
print(json.dumps(vm.get("identity", {}), indent=2))
985+
PY
986+
else
987+
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."
988+
fi
989+
```
990+
991+
{{#endtab }}
992+
993+
{{#tab name="Windows" }}
994+
995+
```powershell
996+
$imds = "http://169.254.169.254/metadata"
997+
$apiVersion = "2021-02-01"
998+
$resource = "https://management.azure.com/"
999+
1000+
# Optional. Examples:
1001+
# $env:MSI_SELECTOR = "client_id=<client-id>"
1002+
# $env:MSI_SELECTOR = "object_id=<principal-id>"
1003+
# $env:MSI_SELECTOR = "msi_res_id=/subscriptions/.../userAssignedIdentities/name"
1004+
$selector = $env:MSI_SELECTOR
1005+
1006+
function Invoke-Imds {
1007+
param([string]$Uri)
1008+
1009+
$params = @{
1010+
Method = "GET"
1011+
Uri = $Uri
1012+
Headers = @{ Metadata = "true" }
1013+
UseBasicParsing = $true
1014+
}
1015+
if ((Get-Command Invoke-RestMethod).Parameters.ContainsKey("NoProxy")) {
1016+
$params.NoProxy = $true
1017+
}
1018+
Invoke-RestMethod @params
1019+
}
1020+
1021+
function Decode-JwtPayload {
1022+
param([string]$Token)
1023+
1024+
$payload = $Token.Split(".")[1].Replace("-", "+").Replace("_", "/")
1025+
switch ($payload.Length % 4) {
1026+
2 { $payload += "==" }
1027+
3 { $payload += "=" }
1028+
}
1029+
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json
1030+
}
1031+
1032+
$encodedResource = [uri]::EscapeDataString($resource)
1033+
$tokenUri = "$imds/identity/oauth2/token?api-version=$apiVersion&resource=$encodedResource"
1034+
if ($selector) {
1035+
$tokenUri = "$tokenUri&$selector"
1036+
}
1037+
1038+
Write-Host "[*] Requesting managed identity token for: $resource"
1039+
$tokenResponse = Invoke-Imds -Uri $tokenUri
1040+
$claims = Decode-JwtPayload -Token $tokenResponse.access_token
1041+
1042+
Write-Host "[+] Token acquired"
1043+
$claims | Select-Object tid, appid, oid, xms_mirid | Format-List
1044+
1045+
Write-Host "[*] Trying to read the VM identity property through ARM..."
1046+
$compute = Invoke-Imds -Uri "$imds/instance/compute?api-version=$apiVersion"
1047+
$armUri = "https://management.azure.com$($compute.resourceId)?api-version=2024-07-01"
1048+
1049+
try {
1050+
$vm = Invoke-RestMethod -Method GET -Uri $armUri -Headers @{ Authorization = "Bearer $($tokenResponse.access_token)" } -UseBasicParsing
1051+
$vm.identity | ConvertTo-Json -Depth 20
1052+
} catch {
1053+
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."
1054+
}
1055+
```
1056+
1057+
{{#endtab }}
1058+
{{#endtabs }}
1059+
1060+
8401061
## Privilege Escalation
8411062
8421063
{{#ref}}
@@ -866,8 +1087,11 @@ Invoke-AzureRmVMBulkCMD -Script Mimikatz.ps1 -Verbose -output Output.txt
8661087
- [https://learn.microsoft.com/en-us/azure/virtual-machines/overview](https://learn.microsoft.com/en-us/azure/virtual-machines/overview)
8671088
- [https://hausec.com/2022/05/04/azure-virtual-machine-execution-techniques/](https://hausec.com/2022/05/04/azure-virtual-machine-execution-techniques/)
8681089
- [https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service](https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service)
1090+
- [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)
1091+
- [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)
1092+
- [https://learn.microsoft.com/en-us/azure/virtual-machines/linux/no-agent](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/no-agent)
1093+
- [https://learn.microsoft.com/en-us/azure/virtual-machines/run-command](https://learn.microsoft.com/en-us/azure/virtual-machines/run-command)
1094+
- [https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux](https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux)
1095+
- [https://www.cybercx.com.au/blog/azure-ssrf-metadata/](https://www.cybercx.com.au/blog/azure-ssrf-metadata/)
8691096
8701097
{{#include ../../../../banners/hacktricks-training.md}}
871-
872-
873-

0 commit comments

Comments
 (0)