During a security breach, critical evidence may surface in unexpected places, such as Azure Storage. Although often overlooked, Azure Storage logs can provide invaluable insights for digital forensics, helping investigators reconstruct attacker activity, trace data access patterns, and detect anomalies. As part of our exploration of cloud forensics, we demonstrate how Azure Storage Logs can be leveraged for security investigations and emphasize their essential role in incident response and breach analysis.
Co-authors - Christoph Dreymann - Shiva P
Introduction
Azure Storage Accounts are frequently targeted by threat actors. Their goal is to exfiltrate sensitive data to an external infrastructure under their control. Because diagnostic logging is not always fully enabled by default, valuable evidence of their malicious actions may be lost.
With this blog, we will explore realistic attack scenarios and demonstrate the types of artifacts those activities generate. By properly enabling Microsoft Azure Storage Account logs investigators gain a better understanding of the scope of the incident. The information can also provide guidance for remediating the environment and on preventing data theft from occurring again.
Storage Account
A Storage Account provides scalable, secure, and highly available storage for storing and managing data objects. Due to the variety of sensitive data that can be stored, it is another highly valued target by a threat actor.
Threat actors exploit misconfigurations, weak access controls, and leaked credentials to gain unauthorized access. Key risks include Shared Access Signature token (SAS) misuse that allows threat actors to access or modify exposed blob storages. Storage Account key exposure could grant privileged access to the data plane.
Investigating storage-related security incidents requires familiarity with Azure activity logs and Diagnostic logs. Diagnostic log types for Storage accounts are StorageBlob, StorageFile, StorageQueue, and StorageTable. These logs can help identify unusual access patterns, role changes, and unauthorized SAS token generation. This blog is centered around StorageBlob activity logs.
Storage Account logging
The logs for a Storage Account aren’t enabled by default. These logs capture operations, requests, and use such as read, write, and delete actions/requests on storage objects like blobs, queues, files, or tables.
NOTE: There are no license requirements to enable Storage Account logging, but Log Analytics charges based on ingestion and retention (Pricing - Azure Monitor | Microsoft Azure)
For more information on enabling logging for a Storage Account can be found here.
Notable fields
The log entries contain various fields which are of use not only during or after an incident, but for general monitoring of a storage account during normal operations (for a full list, see what data is available in the Storage Logs).
Once the storage log is enabled, one of the key tables within Log Analytics is StorageBlobLogs, which provides details about blob storage operations, including read, write, and delete actions. Key columns such as OperationName, AuthenticationType, StatusText, and UserAgentHeader capture essential information about these activities.
The OperationName field identifies operations on a storage account, such as “PutBlob” for uploads or “DeleteBlob” and “DeleteFile” for deletions.
The UserAgentHeader fields offer valuable insights into the tools used to access a Blob storage. Accessing blob storages through the Azure portal is typically logged with a generic user agent, which indicates the application used to perform the access, such as a web browser like Mozilla Firefox. In contrast, tools like AzCopy or Microsoft Azure Storage Explorer are explicitly identified in the logs. Analyzing the UserAgentHeader provides crucial details about the access method, helping determine how the blob storage was accessed.
The following table includes additional investigation fields,
Field name |
Description |
TimeGenerated [UTC] |
The date and time of the operation request. |
AccountName |
Name of the Storage account. |
OperationName |
Name of the operation. A detailed list of for StorageBlob operation can be found here. |
AuthenticationType |
The type of authentication that was used to make this request. |
StatusCode |
The HTTP status code for the request. If the request is interrupted, this value might be set to Unknown. |
StatusText |
The status of the requested operation. |
Uri |
Uniform resource identifier that is requested. |
CallerIpAddress |
The IP address of the requester, including the port number. |
UserAgentHeader |
The User-Agent header value. |
ObjectKey |
Provides the path of the object requested. |
RequesterUpn |
User Principal Name of the requester. |
AuthenticationHash |
Hash of the authentication token used during a request. Request authenticated with SAS token includes a SAS signature specifying the hash derived from the signature part of the SAS token. |
For a full list, see what data is available in the Storage Logs.
How a threat actor can access a Storage Account
Threat actors can access the Storage Account through Azure-assigned RBAC, a SAS token (including User delegated SAS token), Azure Storage Account Keys and Anonymous Access (if configured).
Storage Account Access Keys
Azure Storage Account Access Keys are shared secrets that enable full access to Azure storage resources. When creating a storage account, Azure generates two access keys, both can be used for authentication with the storage account. These keys are permanent and do not have an expiration date.
Both Storage Account Owners and roles such as Contributor or any other role with the assigned action of Microsoft.Storage/storageAccounts/listKeys/action can retrieve and use these credentials to access the storage account.
Account Access Keys can be rotated/regenerated but if done unintentionally, it could disrupt applications or services dependent on the key for authentication. Additionally, this action invalidates any SAS tokens derived from that key, potentially revoking access to dependent workflows.
Monitoring key rotations can help detect unexpected changes and mitigate disruptions.
Query: This query can help identify instances of account key rotations in the logs
AzureActivity
| where OperationNameValue has "MICROSOFT.STORAGE/STORAGEACCOUNTS/REGENERATEKEY/ACTION"
| where ActivityStatusValue has "Start"
| extend resource = parse_json(todynamic(Properties).resource)
| extend requestBody = parse_json(todynamic(Properties).requestbody)
| project TimeGenerated, OperationNameValue, resource, requestBody, Caller, CallerIpAddress
Shared Access Signature
SAS tokens offer a granular method for controlling access to Azure storage resources. SAS tokens enable specific permitted actions on a resource and their duration. They can be generated for blobs, queues, tables, and file shares within a storage account, providing precise control over data access. A SAS token allows access via a signed URL.
A Storage Account Owner can generate a SAS token and connection strings for various resources within the storage account (e.g., blobs, containers, tables) without restrictions. Additionally, roles with Microsoft.Storage/storageAccounts/listKeys/action rights can also generate SAS tokens.
SAS tokens enable access to storage resources using tools such as Azure Storage Explorer, Azure CLI, or PowerShell.
It is important to note that the logs do not indicate when a SAS token was generated [How a shared access signature works]. However, their usage can be inferred by tracking configuration changes that enable the use of storage account keys option which is disabled by default.
Figure 1: Configuration setting to enable account key access
Query: This query is designed to detect configuration changes made to enable access via storage account keys
AzureActivity
| where OperationNameValue has "MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE"
| where ActivityStatusValue has "Success"
| extend allowSharedKeyAccess = parse_json(tostring(parse_json(tostring(parse_json(Properties).responseBody)).properties)).allowSharedKeyAccess
| where allowSharedKeyAccess == "true"
User delegated Shared Access Signature
A User Delegation SAS is a type of SAS token that is secured using Microsoft Entra ID credentials rather than the storage account key.
For more details see Authorize a user delegation SAS. To request a SAS token using the user delegation key, the identity must possess the Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey action (see Assign permissions with RBAC).
Azure Role-Based Access Control
A threat actor must identify a target (an identity) that can assign roles or already holds specific RBAC roles.
To assign Azure RBAC roles, an identity must have Microsoft.Authorization/roleAssignments/write, which allows the assignment of roles necessary for accessing storage accounts.
Some examples of roles that provide permissions to access data within Storage Account (see Azure built-in roles for blob):
- Storage Account Contributor (Read, Write, Manage Access)
- Storage Blob Data Contributor (Read, Write)
- Storage Blob Data Owner (Read, Write, Manage Access)
- Storage Blob Data Reader (Read Only)
Additionally, to access blob data in the Azure portal, a user must also be assigned the Reader role (see Assign an Azure role). More information about Azure built-in roles for a Storage Account can be found here Azure built-in roles for Storage.
Anonymous Access
If the storage account configuration 'Allow Blob anonymous access' is set to enabled and a container is created with anonymous read access, a threat actor could access the storage contents from the internet without any authorization.
Figure 2: Configuration settings for Blob anonymous access and container-level anonymous access.
Query: This query helps identify successful configuration changes to enable anonymous access
AzureActivity
| join kind=rightouter (AzureActivity | where TimeGenerated > ago(30d) | where OperationNameValue has "MICROSOFT.STORAGE/STORAGEACCOUNTS/WRITE" | where Properties has "allowBlobPublicAccess" | extend ProperTies = parse_json(Properties) | evaluate bag_unpack(ProperTies) | extend allowBlobPublicAccess = todynamic(requestbody).properties["allowBlobPublicAccess"] | where allowBlobPublicAccess has "true" | summarize by CorrelationId) on CorrelationId
| extend ProperTies = parse_json(Properties)
| evaluate bag_unpack(ProperTies)
| extend allowBlobPublicAccess_req = todynamic(requestbody).properties["allowBlobPublicAccess"]
| extend allowBlobPublicAccess_res = todynamic(responseBody).properties["allowBlobPublicAccess"]
| extend allowBlobPublicAccess = case (allowBlobPublicAccess_req!="", allowBlobPublicAccess_req, allowBlobPublicAccess_res!="", allowBlobPublicAccess_res, "")
| project OperationNameValue, ActivityStatusValue, ResourceGroup, allowBlobPublicAccess, Caller, CallerIpAddress, ResourceProviderValue
Key notes regarding the authentication methods
When a user accesses Azure Blob Storage via the Azure portal, the interaction is authenticated using OAuth and is authorized by the Azure RBAC roles configuration for the given user. In contrast, authentication using Azure Storage Explorer and AzCopy depends on the method used:
- If a user interactively signs in via the Azure portal or utilizes the Device code flow, authentication appears as OAuth based.
- When using a SAS token, authentication is recorded as SAS-based for both tools.
Access via Azure RBAC is logged in Entra ID Sign-in Logs, however, activity related to SAS token usage does not appear in the sign-in logs, as it provides pre-authorized access. Log analysis should consider all operations, since initial actions can reveal the true authentication method even OAuth-based access may show as SAS in logs.
The screenshot below illustrates three distinct cases, each showcasing different patterns of authentication types used when accessing storage resources.
- A SAS token is consistently used across various operations, where the SAS token is the primary access method.
- The example below highlighted as ‘2’ demonstrates a similar pattern, with OAuth (using assigned Azure RBAC role) serving as the primary authentication method for all listed operations.
- Lastly, example number ‘3’, Operations start with OAuth authentication (using an assigned Azure RBAC role for authorization) and then uses a SAS token, indicating mixed authentication types.
Figure 3: Different patterns of authentication types
Additionally, when using certain applications such as Azure Storage Explorer with Account Access Keys authentication, the initial operations such as ListContainers and ListBlob are logged with the authentication type reported as “AccountKey”. However, for subsequent actions like file uploads or downloads, the authentication type switches to SAS in the logs.
To accurately determine whether an Account Access Keys or SAS was used, it's important to correlate these actions with the earlier enumeration or sync activity within the logs.
With this understanding, let’s proceed to analyze specific attack scenarios by utilizing the log analytics, such as the StorageBlobLogs table.
Attack scenario
This section will examine the typical steps that a threat actor might take when targeting a Storage Account. We will specifically focus on the Azure Resource Manager layer, where Azure RBAC initially dictates what a threat actor can discover.
Enumeration
During enumeration, a threat actor’s goal is to map out the available storage accounts. The range of this discovery is decided by the access privileges of a compromised identity. If that identity holds at least a minimum level of access (similar to a Reader) at the subscription level, it can view storage account resources without making any modifications. Importantly, this permission level does not grant access to the actual data stored within the Azure Storage itself. Hence, a threat actor is limited to interacting only with those storage accounts that are visible to them.
To access and download files from Blob Storage, a threat actor must be aware of the names of containers (Operation: ListContainers) and the files within those containers (Operation: ListBlobs). All interactions with these storage elements are recorded in the StorageBlobLogs table.
Containers or blobs in a container can be listed by a threat actor with the appropriate access rights. If access is not authorized, attempts to do so will result in error codes shown in the StatusCode field. A high number of unauthorized attempts resulting in errors would be a key indicator of suspicious activity or misconfiguration.
Figure 4: Failure attempts to list blobs/containers
Query: This query serves as a starting point for detecting a spike in unauthorized attempts to enumerate containers, blobs, files, or queues
union Storage*
| extend StatusCodeLong = tolong(StatusCode)
| where OperationName has_any ("ListBlobs", "ListContainers", "ListFiles", "ListQueues")
| summarize MinTime = min(TimeGenerated), MaxTime = max(TimeGenerated), OperationCount = count(), UnauthorizedAccess = countif(StatusCodeLong >= 400), OperationNames = make_set(OperationName), ErrorStatusCodes = make_set_if(StatusCode, StatusCodeLong >= 400), StorageAccountName = make_set(AccountName) by CallerIpAddress
| where UnauthorizedAccess > 0
Note: The UnauthorizedAccess filter attribute must be adjusted based on your environment.
Data exfiltration
Let’s use the StorageBlobLogs to analyze two different attack scenarios.
Scenario 1: Compromised user has access to a storage account
In this scenario, the threat actor either compromises a user account with access to one or more storage accounts or alternatively, obtains a leaked Account Access Key or SAS token. With a compromised identity, the threat actor can either enumerate all storage accounts the user has permissions to (as covered in enumeration) or directly access a specific blob or container if the leaked key grants scoped access.
Account Access Keys (AccountKey)/SAS tokens
The threat actor might either use the storage account’s access keys or SAS token retrieved through the compromised user account provided they have the appropriate permissions or the leaked key itself may already be either an Account access key or SAS token. Access keys grant complete control while SAS key can generate a time-bound access, to authorize data transfers enabling them to view, upload, or download data at will.
Figure 5: Account key used to download/view data
Figure 6: SAS token used to download/view data
Query: This query helps identify cases where an AccountKey/SAS was used to download/view data from a storage account
StorageBlobLogs
| where OperationName has "GetBlob"
| where AuthenticationType in~ ("AccountKey", "SAS")
| where StatusText in~ ("Success", "AnonymousSuccess", "SASSuccess")
| project TimeGenerated, AccountName, OperationName, RequesterUpn, AuthenticationType, Uri, ObjectKey, StatusText, UserAgentHeader, CallerIpAddress, AuthenticationHash
User Delegation SAS
Available for Blob Storage only, a User Delegation SAS functions similar to a SAS but is protected with Microsoft Entra ID credentials rather than the storage account key.
The creation of a User Delegation SAS is tracked as a corresponding "GetUserDelegationKey" log entry in StorageBlobLogs table.
Figure 7: User-Delegation Key created
Query: This query helps identify creation of a User-Delegation Key. The RequesterUpn provides the identity of the user account creating this key.
StorageBlobLogs
| where OperationName has "GetUserDelegationKey"
| where StatusText in~ ("Success", "AnonymousSuccess", "SASSuccess")
| project TimeGenerated, AccountName, OperationName, RequesterUpn, Uri, CallerIpAddress, ObjectKey, AuthenticationType, StatusCode, StatusText
Figure 8: User-Delegation activity to download/read
Query: This query helps identify cases where a download/read action was initiated while authenticated via a User delegation key
StorageBlobLogs
| where AuthenticationType has "DelegationSas"
| where OperationName has "GetBlob"
| where StatusText in~ ("Success", "AnonymousSuccess", "SASSuccess")
| project Type, TimeGenerated, OperationName, AccountName, UserAgentHeader, ObjectKey, AuthenticationType, StatusCode, CallerIpAddress, Uri
The operation "GetUserDelegationKey" within the StorageBlobLogs captures the identity responsible for generating a User Delegation SAS token. The AuthenticationHash field shows the Key used to sign the SAS token.
When the SAS token is used, any operations will include the same SAS signature hash enabling you to correlate various actions performed using this token even if the originating IP addresses differ.
Query: The following query extracts a SAS signature hash from the AuthenticationHash field. This helps to track the token's usage, providing an audit trail to identify potentially malicious activity.
StorageBlobLogs
| where AuthenticationType has "DelegationSas"
| extend SasSHASignature = extract(@"SasSignature\((.*?)\)", 1, AuthenticationHash)
| project Type, TimeGenerated, OperationName, AccountName, UserAgentHeader, ObjectKey, AuthenticationType, StatusCode, CallerIpAddress
In the next scenario, we examine how a threat actor already in control of a compromised identity uses Azure RBAC to assign permissions. With administrative privileges over a storage account, the threat actor can grant access to additional accounts and establish long-term access to the storage accounts.
Scenario 2: A user account is controlled by the threat actor and has elevated access to the Storage Account
An identity named Bob was identified as compromised due to an unauthorized IP login.
The investigation triggers when Azure Sign-in logs reveal logins originating from an unexpected location. This account has owner permissions for a resource group, allowing full access and role assignments in Azure RBAC. The threat actor grants access to another account they control, as shown in the AzureActivity logs.
The AzureActivity logs in the figure below show that Reader, Data Access, and Storage Account Contributor roles were assigned to Hacker2 for a Storage Account within Azure:
Figure 9: Assigning a role to a user
Query: This query helps identify if a role has been assigned to a user
AzureActivity
| where Caller has "Bob"
| where OperationNameValue has "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
| extend RoleDefintionIDProperties = parse_json(Properties)
| evaluate bag_unpack(RoleDefintionIDProperties)
| extend RoleDefinitionIdExtracted = tostring(todynamic(requestbody).Properties.RoleDefinitionId)
| extend RoleDefinitionIdExtracted = extract(@"roleDefinitions/([a-f0-9-]+)", 1, RoleDefinitionIdExtracted)
| extend RequestedRole = case( RoleDefinitionIdExtracted == "ba92f5b4-2d11-453d-a403-e96b0029c9fe", "Storage Blob Data Contributor", RoleDefinitionIdExtracted == "b7e6dc6d-f1e8-4753-8033-0f276bb0955b", "Storage Blob Data Owner", RoleDefinitionIdExtracted == "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1", "Storage Blob Data Reader", RoleDefinitionIdExtracted == "db58b8e5-c6ad-4a2a-8342-4190687cbf4a", "Storage Blob Delegator", RoleDefinitionIdExtracted == "c12c1c16-33a1-487b-954d-41c89c60f349", "Reader and Data Access", RoleDefinitionIdExtracted == "17d1049b-9a84-46fb-8f53-869881c3d3ab","Storage Account Contributor", "")
| extend roleAssignmentScope = tostring(todynamic(Authorization_d).evidence.roleAssignmentScope)
| extend AuthorizedFor = tostring(todynamic(requestbody).Properties.PrincipalId)
| extend AuthorizedType = tostring(todynamic(requestbody).Properties.PrincipalType)
| project TimeGenerated, RequestedRole, roleAssignmentScope, ActivityStatusValue, Caller, CallerIpAddress, CategoryValue, ResourceProviderValue, AuthorizedFor, AuthorizedType
Note: Refer to this resource for additional Azure in-built role IDs that can be used in this query.
The Sign-in logs indicate that Hacker2 successfully accessed Azure from the same malicious IP address.
We can examine StorageBlobLogs to determine if the user accessed data of the blob storage since specific roles related to the Storage Account were assigned to them.
The activities within the blob storage indicate several entries attributed to the Hacker2 user, as shown in the figure below.
Figure 10: User access to blob storage
Query: This query helps identify access to blob storage from a malicious IP
StorageBlobLogs
| where TimeGenerated > ago (30d)
| where CallerIpAddress has {{IPv4}}
| extend ObjectName= ObjectKey
| project TimeGenerated, AccountName, OperationName, AuthenticationType, StatusCode, StatusText, RequesterUpn, CallerIpAddress, UserAgentHeader, ObjectName, Category
An analysis of the StorageBlobLogs, as shown in the figure below, reveals that Hacker2 performed a "StorageRead" operation on three files. This indicates that data was accessed or downloaded from blob storage.
Figure 11: Blob Storage Read/Download activities
The UserAgentHeader suggests that the storage account was accessed through the Azure portal. Consequently, the SignInLogs can offer further detailed information.
Query: This query checks for read, write, or delete operations in blob storage and their access methods,
StorageBlobLogs
| where TimeGenerated > ago(30d)
| where CallerIpAddress has {{IPv4}}
| where OperationName has_any ("PutBlob", "GetBlob", "DeleteBlob") and StatusText == "Success"
| extend Notes = case( OperationName == "PutBlob" and Category == "StorageWrite" and UserAgentHeader has "Microsoft Azure Storage Explorer", "Blob was written through Azure Storage Explorer", OperationName == "PutBlob" and Category == "StorageWrite" and UserAgentHeader has "AzCopy", "Blob was written through AzCopy Command", OperationName == "PutBlob" and Category == "StorageWrite" and not(UserAgentHeader has_any("AzCopy","Microsoft Azure Storage Explorer")), "Blob was written through Azure portal", OperationName == "GetBlob" and Category == "StorageRead" and UserAgentHeader has "Microsoft Azure Storage Explorer", "Blob was Read/Download through Azure Storage Explorer", OperationName == "GetBlob" and Category == "StorageRead" and UserAgentHeader has "AzCopy", "Blob was Read/Download through AzCopy Command", OperationName == "GetBlob" and Category == "StorageRead" and not(UserAgentHeader has_any("AzCopy","Microsoft Azure Storage Explorer")), "Blob was Read/Download through Azure portal", OperationName == "DeleteBlob" and Category == "StorageDelete" and UserAgentHeader has "Microsoft Azure Storage Explorer", "Blob was deleted through Azure Storage Explorer", OperationName == "DeleteBlob" and Category == "StorageDelete" and UserAgentHeader has "AzCopy", "Blob was deleted through AzCopy Command", OperationName == "DeleteBlob" and Category == "StorageDelete" and not(UserAgentHeader has_any("AzCopy","Microsoft Azure Storage Explorer")), "Blob was deleted through Azure portal","")
| project TimeGenerated, AccountName, OperationName, AuthenticationType, StatusCode, CallerIpAddress, ObjectName=ObjectKey, Category, RequesterUpn, Notes
The log analysis confirms that the threat actor successfully extracted data from a storage account.
Storage Account summary
Detecting misuse within a Storage Account can be challenging, as routine operations may hide malicious activities. However, enabling logging is essential for investigation to help track accesses, especially when compromised identities or misused SAS tokens or keys are involved.
Unusual changes in user permissions and irregularities in role assignments which are documented in the Azure Activity Logs, can signal unauthorized access, while Microsoft Entra ID sign-in logs can help identify compromised UPNs and suspicious IP addresses that ties into OAuth-based storage account access. By thoroughly analyzing Storage Account logs which details operation types and access methods, investigators can identify abuse and determine the scope of compromise. That not only helps when remediating the environment but can also provide guidance on preventing unauthorized data theft from occurring again.