Adversaries may be exfiltrating data through Azure Storage by leveraging compromised credentials to access sensitive files. SOC teams should proactively hunt for this behavior to detect and mitigate potential data breaches and unauthorized access in their Azure Sentinel environment.
KQL Query
SecurityAlert
| where AlertName has_any ('Access from a suspicious IP to a storage file share',' Access from a Tor exit node to a storage blob container')
| extend EntitiesDynamicArray = parse_json(Entities) | mv-expand EntitiesDynamicArray
// Parsing relevant entity column extract hostname and IP address
| extend EntityType = tostring(parse_json(EntitiesDynamicArray).Type), EntityAddress = tostring(EntitiesDynamicArray.Address)
| extend IpAddress = iif(EntityType == 'ip', EntityAddress, '')
| where isnotempty(IpAddress)
| join kind=inner (
CommonSecurityLog
| where DeviceVendor =~ "Fortinet"
| where ApplicationProtocol has_any ("SSL","RDP")
| where LogSeverity has_any ("2","3")
| where isnotempty(SourceIP) and isnotempty(DestinationIP) and SourceIP != "0.0.0.0"
| where DeviceAction !in ("close", "client-rst", "server-rst", "deny") and DestinationPort != 161
| project DeviceProduct,LogSeverity,DestinationPort,DestinationIP,Message,SourceIP,SourcePort,Activity,SentBytes,ReceivedBytes
) on $left.IpAddress == $right.DestinationIP
| join kind=inner (
AuditLogs
| where LoggedByService =~ "Core Directory"
| where Category =~ "RoleManagement"
| extend IpAddress = case(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
isnotempty(tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.app)).ipAddress),
'Not Available')
| extend InitiatedBy = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)),
tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName), tostring(parse_json(tostring(InitiatedBy.app)).displayName)), UserRoles = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
| extend TargetResourceName = tolower(tostring(TargetResources.[0].displayName))
)
on IpAddress
| summarize count () by TimeGenerated,IpAddress, UserRoles,SourcePort, DestinationPort, AccountCustomEntity =InitiatedBy
id: 860a8df2-8d19-4c60-bf61-de1c02422797
name: Storage Alerts Correlation with CommonSecurityLogs & AuditLogs
description: |
'This query combines different Storage alerts with CommonSecurityLogs and AuditLogs helping analysts investigate any possible storage related attacks faster
thus reducing Mean Time To Respond'
requiredDataConnectors:
- connectorId: AzureSecurityCenter
dataTypes:
- SecurityAlert (ASC)
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
tactics:
- InitialAccess
- Impact
relevantTechniques:
- T1190
- T1078
query: |
SecurityAlert
| where AlertName has_any ('Access from a suspicious IP to a storage file share',' Access from a Tor exit node to a storage blob container')
| extend EntitiesDynamicArray = parse_json(Entities) | mv-expand EntitiesDynamicArray
// Parsing relevant entity column extract hostname and IP address
| extend EntityType = tostring(parse_json(EntitiesDynamicArray).Type), EntityAddress = tostring(EntitiesDynamicArray.Address)
| extend IpAddress = iif(EntityType == 'ip', EntityAddress, '')
| where isnotempty(IpAddress)
| join kind=inner (
CommonSecurityLog
| where DeviceVendor =~ "Fortinet"
| where ApplicationProtocol has_any ("SSL","RDP")
| where LogSeverity has_any ("2","3")
| where isnotempty(SourceIP) and isnotempty(DestinationIP) and SourceIP != "0.0.0.0"
| where DeviceAction !in ("close", "client-rst", "server-rst", "deny") and DestinationPort != 161
| project DeviceProduct,LogSeverity,DestinationPort,DestinationIP,Message,SourceIP,SourcePort,Activity,SentBytes,ReceivedBytes
) on $left.IpAddress == $right.DestinationIP
| join kind=inner (
AuditLogs
| where LoggedByService =~ "Core Directory"
| where Category =~ "RoleManagement"
| extend IpAddress = case(
isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.user)).ipAddress),
isnotempty(tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.app)).ipAddress),
'Not Available')
| extend InitiatedBy = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)),
tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName), tostring(parse_json(tostring(InitiatedBy.app)).displayName)), UserRoles = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
| extend TargetResourceName = tolower(tostring(TargetResources.[0].displayName))
)
on IpAddress
| summarize count () by TimeGenerated,IpAddress, UserRoles,SourcePort, DestinationPort, AccountCustomEntity =InitiatedBy
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
- entityType: IP
f
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
CommonSecurityLog | Ensure this data connector is enabled |
SecurityAlert | Ensure this data connector is enabled |
Scenario: Scheduled Backup Job Execution
Description: A legitimate scheduled backup job is running and generating storage-related events that match the rule’s criteria.
Filter/Exclusion: Exclude events where the source is a known backup tool (e.g., Veeam, Commvault) or where the event ID corresponds to a backup task (e.g., Event ID 6008 in Windows for system shutdown, or specific job IDs in Azure Backup).
Example Filter: ProcessName != "VeeamBackup.exe" OR EventID != 6008
Scenario: Admin Performing Storage Configuration Changes
Description: A system administrator is making legitimate configuration changes to storage systems, such as resizing volumes or updating storage policies.
Filter/Exclusion: Exclude events where the user is a known admin account (e.g., Administrator, StorageAdmin) or where the action is part of a documented configuration task.
Example Filter: User != "Administrator" OR EventID != 41
Scenario: Log File Rotation or Archiving
Description: Automated log rotation or archiving processes are generating storage-related events that trigger the rule.
Filter/Exclusion: Exclude events related to log management tools (e.g., Logstash, Splunk, or Windows Event Log rotation scripts).
Example Filter: ProcessName != "logrotate" OR ProcessName != "Splunkd.exe"
Scenario: User-Initiated File Copy or Move Operations
Description: A user is copying or moving large files between storage locations, which may generate storage and audit log events.
Filter/Exclusion: Exclude events where the user is performing file operations within a known business process (e.g., file migration, data transfer).
Example Filter: `User != “User123” OR EventID