Improve Azure VM managed identity discovery

This commit is contained in:
Carlos Polop
2026-05-05 17:58:20 +02:00
parent 49bafa87a9
commit 15a244cb30
2 changed files with 486 additions and 6 deletions
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Diagnostics;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
namespace winPEAS.Info.CloudInfo
{
@@ -28,6 +30,7 @@ namespace winPEAS.Info.CloudInfo
const string AZURE_BASE_URL = "http://169.254.169.254/metadata/";
const string API_VERSION = "2021-12-13";
const string CONTAINER_API_VERSION = "2019-08-01";
const string ARM_VM_API_VERSION = "2024-07-01";
public static bool DoesProcessExist(string processName)
{
@@ -122,6 +125,8 @@ namespace winPEAS.Info.CloudInfo
IsAttackVector = tuple.Item3
});
}
AddAzureVmUserAssignedIdentityTokens(_endpointDataList);
}
else
{
@@ -164,6 +169,346 @@ namespace winPEAS.Info.CloudInfo
return _endpointData;
}
private void AddAzureVmUserAssignedIdentityTokens(List<EndpointData> endpointDataList)
{
endpointDataList.Add(new EndpointData()
{
EndpointName = "Managed identity discovery note",
Data = "winPEAS can request default managed identity tokens directly from IMDS. To discover every attached user-assigned identity, it tries to read the VM ARM identity block with the default Management token. If that token lacks Microsoft.Compute/virtualMachines/read, IMDS can still issue tokens for known client_id/object_id/msi_res_id values, but the full attached identity list cannot be discovered from IMDS alone.",
IsAttackVector = false
});
string instanceJson = CreateMetadataAPIRequest(
$"{AZURE_BASE_URL}instance?api-version={API_VERSION}",
"GET",
new WebHeaderCollection() { { "Metadata", "true" } });
string vmResourceId = GetJsonString(instanceJson, "compute", "resourceId");
string managementTokenJson = CreateMetadataAPIRequest(
$"{AZURE_BASE_URL}identity/oauth2/token?api-version={API_VERSION}&resource=https://management.azure.com/",
"GET",
new WebHeaderCollection() { { "Metadata", "true" } });
string managementToken = GetJsonString(managementTokenJson, "access_token");
if (string.IsNullOrEmpty(vmResourceId) || string.IsNullOrEmpty(managementToken))
{
endpointDataList.Add(new EndpointData()
{
EndpointName = "Attached user-assigned managed identities",
Data = "Could not obtain the VM resource ID or default Management token needed for ARM identity discovery.",
IsAttackVector = false
});
AddAzureVmWireServerIdentityTokens(endpointDataList);
return;
}
string armUrl = $"https://management.azure.com{vmResourceId}?api-version={ARM_VM_API_VERSION}";
string vmJson = CreateMetadataAPIRequest(
armUrl,
"GET",
new WebHeaderCollection() { { "Authorization", $"Bearer {managementToken}" } });
if (string.IsNullOrEmpty(vmJson))
{
endpointDataList.Add(new EndpointData()
{
EndpointName = "Attached user-assigned managed identities",
Data = "Could not read the VM identity block from ARM with the default managed identity token.",
IsAttackVector = false
});
AddAzureVmWireServerIdentityTokens(endpointDataList);
return;
}
JsonNode root;
try
{
root = JsonNode.Parse(vmJson);
}
catch
{
endpointDataList.Add(new EndpointData()
{
EndpointName = "Attached user-assigned managed identities",
Data = vmJson,
IsAttackVector = false
});
AddAzureVmWireServerIdentityTokens(endpointDataList);
return;
}
JsonNode identityNode = root?["identity"];
JsonObject userAssignedIdentities = identityNode?["userAssignedIdentities"] as JsonObject;
endpointDataList.Add(new EndpointData()
{
EndpointName = "VM ARM identity block",
Data = identityNode?.ToJsonString() ?? "No identity block found in ARM VM response.",
IsAttackVector = false
});
if (userAssignedIdentities == null || userAssignedIdentities.Count == 0)
{
AddAzureVmWireServerIdentityTokens(endpointDataList);
return;
}
foreach (var identity in userAssignedIdentities)
{
string identityResourceId = identity.Key;
string clientId = identity.Value?["clientId"]?.GetValue<string>();
string principalId = identity.Value?["principalId"]?.GetValue<string>();
if (string.IsNullOrEmpty(clientId))
{
continue;
}
endpointDataList.Add(new EndpointData()
{
EndpointName = $"User-assigned MI {clientId}",
Data = $"ResourceId: {identityResourceId}\nPrincipalId: {principalId}",
IsAttackVector = false
});
foreach (var tokenEndpoint in GetAzureVmTokenEndpoints($"&client_id={Uri.EscapeDataString(clientId)}"))
{
string result = CreateMetadataAPIRequest(
$"{AZURE_BASE_URL}{tokenEndpoint.Item2}",
"GET",
new WebHeaderCollection() { { "Metadata", "true" } });
endpointDataList.Add(new EndpointData()
{
EndpointName = $"{tokenEndpoint.Item1} for UAI {clientId}",
Data = result,
IsAttackVector = true
});
}
}
}
private void AddAzureVmWireServerIdentityTokens(List<EndpointData> endpointDataList)
{
endpointDataList.Add(new EndpointData()
{
EndpointName = "WireServer/HostGAPlugin managed identity fallback note",
Data = "ARM identity discovery failed or returned no user-assigned identities. Trying WireServer GoalState and HostGAPlugin /vmSettings for identity-looking selectors. These endpoints are environment-dependent and may expose no managed identity data.",
IsAttackVector = false
});
string wireData = "";
wireData += CreateMetadataAPIRequest(
"http://168.63.129.16/machine?comp=goalstate",
"GET",
new WebHeaderCollection() { { "x-ms-version", "2012-11-30" } });
wireData += "\n";
wireData += CreateMetadataAPIRequest(
"http://168.63.129.16/machine/?comp=goalstate",
"GET",
new WebHeaderCollection() { { "x-ms-version", "2012-11-30" } });
wireData += "\n";
wireData += CreateMetadataAPIRequest(
"http://168.63.129.16:32526/vmSettings",
"GET");
if (string.IsNullOrEmpty(wireData))
{
endpointDataList.Add(new EndpointData()
{
EndpointName = "WireServer/HostGAPlugin managed identity fallback",
Data = "WireServer/HostGAPlugin did not return data from this context.",
IsAttackVector = false
});
return;
}
HashSet<string> clientIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
HashSet<string> resourceIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
CollectWireServerIdentitySelectors(wireData, clientIds, resourceIds);
endpointDataList.Add(new EndpointData()
{
EndpointName = "WireServer/HostGAPlugin identity-looking hints",
Data = GetWireServerIdentityHints(wireData),
IsAttackVector = false
});
foreach (string clientId in clientIds)
{
endpointDataList.Add(new EndpointData()
{
EndpointName = $"WireServer-discovered client_id {clientId}",
Data = "Trying IMDS tokens for this client_id.",
IsAttackVector = false
});
foreach (var tokenEndpoint in GetAzureVmTokenEndpoints($"&client_id={Uri.EscapeDataString(clientId)}"))
{
string result = CreateMetadataAPIRequest(
$"{AZURE_BASE_URL}{tokenEndpoint.Item2}",
"GET",
new WebHeaderCollection() { { "Metadata", "true" } });
endpointDataList.Add(new EndpointData()
{
EndpointName = $"{tokenEndpoint.Item1} for WireServer client_id {clientId}",
Data = result,
IsAttackVector = true
});
}
}
foreach (string resourceId in resourceIds)
{
endpointDataList.Add(new EndpointData()
{
EndpointName = $"WireServer-discovered msi_res_id {resourceId}",
Data = "Trying IMDS tokens for this msi_res_id.",
IsAttackVector = false
});
foreach (var tokenEndpoint in GetAzureVmTokenEndpoints($"&msi_res_id={Uri.EscapeDataString(resourceId)}"))
{
string result = CreateMetadataAPIRequest(
$"{AZURE_BASE_URL}{tokenEndpoint.Item2}",
"GET",
new WebHeaderCollection() { { "Metadata", "true" } });
endpointDataList.Add(new EndpointData()
{
EndpointName = $"{tokenEndpoint.Item1} for WireServer msi_res_id",
Data = result,
IsAttackVector = true
});
}
}
}
private static void CollectWireServerIdentitySelectors(string wireData, HashSet<string> clientIds, HashSet<string> resourceIds)
{
TryCollectWireServerJsonSelectors(wireData, clientIds, resourceIds);
foreach (Match match in Regex.Matches(wireData, @"(?i)(clientId|IdentityClientId|client_id)[^0-9a-fA-F]{0,80}([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})"))
{
clientIds.Add(match.Groups[2].Value);
}
foreach (Match match in Regex.Matches(wireData, @"(?i)/subscriptions/[^""<>\s]+/resourceGroups/[^""<>\s]+/providers/Microsoft\.ManagedIdentity/userAssignedIdentities/[^""<>\s]+"))
{
resourceIds.Add(match.Value);
}
}
private static void TryCollectWireServerJsonSelectors(string wireData, HashSet<string> clientIds, HashSet<string> resourceIds)
{
try
{
JsonNode root = JsonNode.Parse(wireData);
CollectJsonIdentitySelectors(root, clientIds, resourceIds);
}
catch
{
}
}
private static void CollectJsonIdentitySelectors(JsonNode node, HashSet<string> clientIds, HashSet<string> resourceIds)
{
if (node == null)
{
return;
}
if (node is JsonObject obj)
{
foreach (var prop in obj)
{
string value = null;
try
{
value = prop.Value?.GetValue<string>();
}
catch
{
}
if (!string.IsNullOrEmpty(value))
{
if (Regex.IsMatch(prop.Key, @"(?i)(clientId|IdentityClientId)$") && Regex.IsMatch(value, @"^[0-9a-fA-F-]{36}$"))
{
clientIds.Add(value);
}
if (Regex.IsMatch(value, @"(?i)/subscriptions/.+/providers/Microsoft\.ManagedIdentity/userAssignedIdentities/"))
{
resourceIds.Add(value);
}
}
CollectJsonIdentitySelectors(prop.Value, clientIds, resourceIds);
}
}
else if (node is JsonArray arr)
{
foreach (JsonNode child in arr)
{
CollectJsonIdentitySelectors(child, clientIds, resourceIds);
}
}
}
private static string GetWireServerIdentityHints(string wireData)
{
List<string> hints = new List<string>();
foreach (Match match in Regex.Matches(wireData, @"(?i)([A-Za-z0-9_./:-]*Identity[A-Za-z0-9_./:-]*|Microsoft\.ManagedIdentity/userAssignedIdentities/[^""<>\s]+|clientId["":=\s]+[0-9a-fA-F-]{36}|IdentityClientId[^0-9a-fA-F]*[0-9a-fA-F-]{36})"))
{
if (!hints.Contains(match.Value))
{
hints.Add(match.Value);
}
if (hints.Count >= 80)
{
break;
}
}
return hints.Count > 0 ? string.Join("\n", hints) : "No identity-looking strings found in WireServer/HostGAPlugin responses.";
}
private static List<Tuple<string, string, bool>> GetAzureVmTokenEndpoints(string selectorSuffix = "")
{
return new List<Tuple<string, string, bool>>()
{
new Tuple<string, string, bool>("Management token", $"identity/oauth2/token?api-version={API_VERSION}&resource=https://management.azure.com/{selectorSuffix}", true),
new Tuple<string, string, bool>("Graph token", $"identity/oauth2/token?api-version={API_VERSION}&resource=https://graph.microsoft.com/{selectorSuffix}", true),
new Tuple<string, string, bool>("Vault token", $"identity/oauth2/token?api-version={API_VERSION}&resource=https://vault.azure.net/{selectorSuffix}", true),
new Tuple<string, string, bool>("Storage token", $"identity/oauth2/token?api-version={API_VERSION}&resource=https://storage.azure.com/{selectorSuffix}", true)
};
}
private static string GetJsonString(string json, params string[] path)
{
if (string.IsNullOrEmpty(json))
{
return null;
}
try
{
JsonNode current = JsonNode.Parse(json);
foreach (string key in path)
{
current = current?[key];
if (current == null)
{
return null;
}
}
return current.GetValue<string>();
}
catch
{
return null;
}
}
public override bool TestConnection()
{
if (IsContainer())