Token Replay Of Workload Identity From Outside Of Azure Network Range
Query
// Requires PrivilegedWorkloadIdentityInfo function and watchlist WorkloadIdentityInfo. See Thomas Naunheim blog for more details.
// https://www.cloud-architekt.net/entra-workload-id-advanced-detection-enrichment/#publish-watchlist-workloadidentityinfo-with-sentinelenrichment
// https://github.com/Cloud-Architekt/AzureSentinel/blob/main/Functions/PrivilegedWorkloadIdentityInfo.yaml
let AzureRanges = externaldata(changeNumber: string, cloud: string, values: dynamic)
["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
| mv-expand values
| mv-expand values.properties.addressPrefixes
| mv-expand values_properties_addressPrefixes
| summarize by tostring(values_properties_addressPrefixes)
| extend isipv4 = parse_ipv4(values_properties_addressPrefixes)
| extend isipv6 = parse_ipv6(values_properties_addressPrefixes)
| extend AddressFamily = case(isnotnull(isipv4), "v4", "v6")
| summarize IPRange=make_set(values_properties_addressPrefixes) by AddressFamily;
let AzureRangesIPv4 = toscalar(AzureRanges
| where AddressFamily == "v4"
| project IPRange);
let AzureRangesIPv6 = toscalar(AzureRanges
| where AddressFamily == "v6"
| project IPRange);
AzureActivity
| where parse_json(tostring(Authorization_d.evidence)).principalType == "ServicePrincipal"
| extend ClaimsObjectIdentifier = parse_json(Claims).["http://schemas.microsoft.com/identity/claims/objectidentifier"]
| extend parsedClaims = parse_json(Claims_d)
| where ActivityStatusValue == "Success" and ActivitySubstatusValue == "OK"
| project
TimeGenerated,
CorrelationId,
OperationName,
ResourceProviderValue,
_ResourceId,
ActivityIpAddress = CallerIpAddress,
ApplicationId = tostring(parsedClaims.appid),
Uti = tostring(parsedClaims.uti),
ActivityStatus
| join kind=inner (AADManagedIdentitySignInLogs
| project
ConditionalAccessPolicies,
ConditionalAccessStatus,
ServicePrincipalCredentialKeyId,
UniqueTokenIdentifier
)
on $left.Uti == $right.UniqueTokenIdentifier
| extend IsInAzurev4Range = ipv4_is_in_any_range(ActivityIpAddress, AzureRangesIPv4)
| extend IsInAzurev6Range = ipv6_is_in_any_range(ActivityIpAddress, AzureRangesIPv6)
| extend IpAddressType = iff(IsInAzurev4Range or IsInAzurev6Range, "Azure Public IP", "None Azure IP")
| where IpAddressType == "None Azure IP"
| where isnotempty(ApplicationId)
| join kind=leftouter(
PrivilegedWorkloadIdentityInfo
| project
WorkloadIdentityName,
WorkloadIdentityType,
ApplicationObjectId,
ServicePrincipalObjectId,
ApplicationId,
IsFirstPartyApp,
EntraIdRoles,
AppRolePermissions,
WorkloadIdClassification = EnterpriseAccessModelTiering
)
on ApplicationIdExplanation
This query detects a token replay attack by checking if a workload identity is used to access Azure resources from an IP address outside of Azure's IP ranges. It retrieves Azure IP ranges, filters the activity logs for successful operations by service principals, and checks if the IP address is within Azure IP ranges. If the IP address is not within Azure IP ranges and the application ID is not empty, it joins the activity logs with privileged workload identity information. The query also includes incident configuration and entity mappings for incident creation and grouping.
Details

Fabian Bader
Released: January 23, 2024
Tables
AzureActivityAADManagedIdentitySignInLogsPrivilegedWorkloadIdentityInfo
Keywords
DevicesIntuneUserAzureIPIdentityTokenReplayAttackAzureActivityAADManagedIdentitySignInLogsPrivilegedWorkloadIdentityInfoWorkloadIdentityNameWorkloadIdentityTypeApplicationObjectIdServicePrincipalObjectIdApplicationIdIsFirstPartyAppEntraIdRolesAppRolePermissionsWorkloadIdClassification
Operators
externaldatawithsummarizeextendcasetoscalarwhereprojectjoiniffisnotemptyipv4_is_in_any_rangeipv6_is_in_any_rangeisnotnullparse_ipv4parse_ipv6make_setparse_json
Severity
HighTactics
PersistencePrivilegeEscalation
MITRE Techniques
Frequency: 1h
Period: 1h