Adversaries may use unique identifiers and session aggregation to enumerate system resources and establish persistence within the environment. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential lateral movement and unauthorized access attempts.
KQL Query
// ==========================================================
// Detect Enumeration Activity Using Unique User Identifiers
// ==========================================================
// ---------- Tuning parameters ----------
let maxTimeBetweenRequests = 30s; // gap that keeps events in the same session
let maxWindowTime = 12h; // hard cap on session duration
let lookback = 30d; // history window
let authTypes = dynamic(['Anonymous']); // adjust if needed
let minDistinctObjects = 10; // minimum objects per session
let minFailureRatio = 0.5; // minimum failure rate (0.5 = 50 %)
// ---------------------------------------
// ---------- Core query ----------
StorageBlobLogs
| where TimeGenerated > ago(lookback)
| where AuthenticationType has_any(authTypes)
| where Category == 'StorageRead'
| where Uri !endswith 'favicon.ico'
// Extract full blob path
| extend FilePathElems = array_slice(split(split(Uri,'?')[0], '/'), 3, -1)
| extend FullPath = strcat('/', strcat_array(FilePathElems, '/'))
// Normalise caller IP
| extend CallerIp = tostring(split(CallerIpAddress, ':')[0])
| where not(ipv4_is_private(CallerIp))
// Build identity key resilient to IP hopping
| extend IdentityKey = strcat(AccountName, '|', UserAgentHeader)
| project TimeGenerated, AccountName, FullPath,
CallerIp, StatusCode, UserAgentHeader, IdentityKey
| order by TimeGenerated asc
| serialize
// Build activity sessions
| extend SessionId = row_window_session(
TimeGenerated,
maxWindowTime,
maxTimeBetweenRequests,
IdentityKey != prev(IdentityKey))
// Status code as integer for numeric comparisons
| extend StatusCodeInt = toint(StatusCode)
| summarize
DistinctObjCount = dcount(FullPath),
RequestCount = count(),
FailCount = countif(StatusCodeInt >= 400),
CallerIPs = make_set(CallerIp, 64),
SessionStart = min(TimeGenerated),
SessionEnd = max(TimeGenerated),
UserAgent = any(UserAgentHeader)
by SessionId, IdentityKey, AccountName
| extend FailureRatio = todouble(FailCount) / RequestCount,
DurationMins = datetime_diff('minute', SessionEnd, SessionStart)
// ----------- Detection logic ----------
| where DistinctObjCount >= minDistinctObjects
and FailureRatio >= minFailureRatio
and DistinctObjCount == RequestCount // touch-once rule
// ----------- Output ----------
| project SessionStart, SessionEnd, DurationMins,
AccountName, IdentityKey, DistinctObjCount,
RequestCount, FailureRatio,
CallerIPCount = array_length(CallerIPs),
CallerIPs, UserAgent
| order by DistinctObjCount desc
id: b7b409df-af7b-4feb-9cc9-109beed37512
name: Detect Enumeration Activity Using Unique Identifiers and Session Aggregation
description: |
"This Kusto (KQL) hunting query detects blob-enumeration or file-spraying behaviour in Azure Storage by:
- Aggregating requests into time-bound sessions with row_window_session().
- Defining a "user" as the combination of AccountName | UserAgentHeader, which is tolerant of rapid IP rotation.
- Raising an alert when, within any single session:
- The actor touches at least 10 distinct objects, and
- At least 50 % of those requests return an HTTP 4xx/5xx status, and
- Each object is accessed exactly once ("touch-once" pattern typical of enumeration).
All tuning knobs (look-back window, session gap, thresholds, authentication type) are exposed at the top of the query.
Be sure to validate the query against both benign and malicious samples in your own environment before relying on it for production detection.
Reference: https://techcommunity.microsoft.com/blog/microsoftdefendercloudblog/protect-your-storage-resources-against-blob-hunting/3735238"
requiredDataConnectors: []
tactics:
- Reconnaissance
- Collection
relevantTechniques:
- T1595
- T1530
query: |
// ==========================================================
// Detect Enumeration Activity Using Unique User Identifiers
// ==========================================================
// ---------- Tuning parameters ----------
let maxTimeBetweenRequests = 30s; // gap that keeps events in the same session
let maxWindowTime = 12h; // hard cap on session duration
let lookback = 30d; // history window
let authTypes = dynamic(['Anonymous']); // adjust if needed
let minDistinctObjects = 10; // minimum objects per session
let minFailureRatio = 0.5; // minimum failure rate (0.5 = 50 %)
// ---------------------------------------
// ---------- Core query ----------
StorageBlobLogs
| where TimeGenerated > ago(lookback)
| where AuthenticationType has_any(authTypes)
| where Category == 'StorageRead'
| where Uri !endswith 'favicon.ico'
// Extract full blob path
| extend FilePathElems = array_slice(split(split(Uri,'?')[0], '/'), 3, -1)
| extend FullPath = strcat('/', strcat_array(FilePathElems, '/'))
// Normalise caller IP
| extend CallerIp = tostring(split(CallerIpAddress, ':')[0])
| where not(ipv4_is_private(CallerIp))
// Build identity key resilient to IP hopping
| extend IdentityKey = strcat(AccountName, '|', UserAgentHeader)
| project TimeGenerated, AccountName, FullPath,
CallerIp, StatusCode, UserAgentHeader, IdentityKey
| order by TimeGenerated asc
| serialize
// Build activity sessions
| extend SessionId = row_window_session(
TimeGenerated,
maxWindowTime,
maxTimeBetweenRequests,
IdentityKey != prev(IdentityKey))
// Status code as integer for nume
Scenario: System Inventory Scan Using systeminfo or wmic
Description: A system administrator runs a routine system inventory scan using systeminfo or wmic to collect hardware and software details.
Filter/Exclusion: Exclude processes initiated by Administrators group or system account, or filter by command line containing systeminfo or wmic.
Scenario: Scheduled Job for Log Collection or Backup
Description: A scheduled task runs a script to collect logs or perform a backup, which may include querying system identifiers or session data.
Filter/Exclusion: Exclude tasks with known job names (e.g., BackupJob, LogCollector) or filter by user account used (e.g., backupsvc).
Scenario: User Enumeration via PowerShell for User Management
Description: An admin uses PowerShell to enumerate users for reporting or auditing purposes, such as using Get-ADUser or Get-LocalUser.
Filter/Exclusion: Exclude processes with command lines containing Get-ADUser, Get-LocalUser, or Import-Csv with known user lists.
Scenario: Session Aggregation for Monitoring Purposes
Description: A security tool or SIEM system aggregates session data for monitoring, which may involve querying session identifiers or user activity.
Filter/Exclusion: Exclude processes from known monitoring tools (e.g., Splunk, ELK, SIEM) or filter by process name or parent process.
Scenario: Application Initialization with Unique Identifiers
Description: An application initializes with unique identifiers (e.g., UUIDs) during startup, which may be misinterpreted as enumeration activity.
Filter/Exclusion: Exclude processes associated with known applications (e.g., sqlservr.exe, java.exe)