RULE 13 Storage Anonymous Public Access
Query
// Rule : Azure - Storage Account Accessed Anonymously or via SAS from Public Internet
// Severity: Medium
// Tactics : Collection, Exfiltration
// MITRE : T1530
// Freq : PT1H Period: PT2H
//==========================================================================================
let PrivateRanges = dynamic(["10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.",
"172.21.", "172.22.", "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.",
"172.29.", "172.30.", "172.31.", "192.168.", "168.63.", "169.254."]);
let SensitiveOps = dynamic(["GetBlob", "ListBlobs", "GetBlobProperties",
"GetContainerProperties", "ListContainers", "GetContainerACL"]);
// Minimum thresholds — tune per environment
// Tuned: anon threshold 5 -> 10 (very small anonymous read volumes were noisy on legacy public
// containers still hosting static assets); SAS threshold 50 MB -> 500 MB (backup/CDN prefetch
// commonly crosses 50 MB); or alternatively >=50 distinct blobs which catches broad enumeration
// that still stays under the byte threshold.
let AnonAccessThreshold = 10;
let SASBytesGBThreshold = 0.5;
let SASDistinctBlobsThreshold = 50;
StorageBlobLogs
| where TimeGenerated > ago(2h)
| where AuthenticationType in~ ("Anonymous", "SasToken")
| where OperationName in (SensitiveOps)
| where StatusCode == 200
| where isnotempty(CallerIpAddress)
| where not(CallerIpAddress has_any (PrivateRanges))
| summarize
AccessCount = count(),
DistinctBlobs = dcount(ObjectKey),
DistinctContainers = dcount(tostring(split(ObjectKey, "/")[0])),
TotalBytesGB = round(sum(ResponseBodySize) / 1073741824.0, 3),
Operations = make_set(OperationName, 5),
StorageAccounts = make_set(AccountName, 5),
FirstAccess = min(TimeGenerated),
LastAccess = max(TimeGenerated)
by CallerIpAddress, AuthenticationType
| where (AuthenticationType =~ "Anonymous" and AccessCount >= AnonAccessThreshold)
or (AuthenticationType =~ "SasToken" and (TotalBytesGB >= SASBytesGBThreshold or DistinctBlobs >= SASDistinctBlobsThreshold))
| extend
CallerIP = CallerIpAddress,
IsAnonymous = AuthenticationType =~ "Anonymous"Explanation
This query is designed to detect potentially suspicious access to Azure Storage Accounts from the public internet. It focuses on access that is either anonymous or uses a Shared Access Signature (SAS) token. Here's a simplified breakdown of what the query does:
-
Private IP Ranges and Sensitive Operations: It defines lists of private IP address ranges and sensitive operations related to accessing storage blobs, such as getting blob properties or listing blobs and containers.
-
Thresholds: It sets thresholds to filter out noise:
- Anonymous access must occur at least 10 times to be considered suspicious.
- SAS token access must either transfer at least 0.5 GB of data or involve at least 50 distinct blobs.
-
Data Filtering: The query looks at storage blob logs from the last 2 hours, focusing on successful operations (status code 200) that are either anonymous or use a SAS token. It excludes access from private IP ranges.
-
Data Aggregation: For each unique caller IP address and authentication type, it summarizes:
- The number of accesses.
- The number of distinct blobs and containers accessed.
- The total data transferred in gigabytes.
- The types of operations performed.
- The storage accounts accessed.
- The first and last access times.
-
Suspicious Activity Detection: It filters the results to identify:
- Anonymous access that meets or exceeds the threshold of 10 accesses.
- SAS token access that meets or exceeds either the data transfer threshold of 0.5 GB or the distinct blob threshold of 50.
-
Output: It extends the results with additional fields indicating the caller's IP address and whether the access was anonymous.
Overall, this query helps identify potentially unauthorized or suspicious access patterns to Azure Storage Accounts from the public internet, which could indicate data collection or exfiltration activities.
