mirror of
https://github.com/HackTricks-wiki/hacktricks-cloud.git
synced 2026-06-12 11:01:38 -07:00
Replace brittle managed identity enumeration examples
This commit is contained in:
@@ -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}}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user