Storm-3168 / JADEPUFFER: Sentinel Hunts for Azure Service Principal Destruction

Threat Overview

Microsoft Security Research published a detailed September 25, 2026 account of Storm-3168, linked to the JADEPUFFER activity first documented by Sysdig. In an early-June incident, two compromised service principals in one Azure tenant divided work between reconnaissance and destructive/credential-collection activity. One identity performed more than 300 successful reads over roughly 15.5 hours. The second later enumerated App Service configuration, then generated more than 150 destructive or credential-related operations in 35 minutes. A seven-minute burst included more than 100 storage-account deletion attempts; most targeted accounts were deleted. The actor also deleted a Key Vault, Function App, and App Service plan, attempted Azure SQL database deletion, targeted Site Recovery and Backup protection locks, and performed more than 30 successful ListKeys operations.

Microsoft confirmed the activity and assessed its destructive and recovery-inhibiting behavior as ransomware-aligned, but did not observe a ransom note or confirm successful exfiltration. The organization had previously exposed the service principal’s client ID, client secret, and tenant ID in a public GitHub issue; Microsoft could not confirm that this specific secret enabled the intrusion. Automation is a high-confidence researcher assessment based on concurrent tokens, rapid sequences, repeated probing, and division of work. Observed requests used python-requests/2.34.2; three published IPs are included below.

References

Impacted Systems

  • Vendor/platform: Microsoft Azure and Microsoft Entra workload identities/service principals.
  • Deployment: Customer Azure tenants; the observed incident crossed two subscriptions.
  • Targeted resources: Azure Storage accounts and access keys, Virtual Machines, subscriptions/resource groups, App Service configuration, Key Vault, Function Apps, App Service plans, Azure SQL databases, Site Recovery locks, and Azure Backup protection locks.
  • Prerequisite: A compromised service principal with sufficient Azure RBAC rights. Observed effective roles included group-granted Storage Account Contributor and direct Contributor and SQL DB Contributor assignments.
  • Exposure condition: Long-lived workload secrets exposed publicly or otherwise stolen; public App Services were also probed, but Microsoft did not establish an App Service-to-ARM credential path for the impacted tenant.
  • Versions: Cloud control-plane activity is not tied to a product build. Resource locks and storage deletion protection blocked some deletion attempts.

Why this matters

The campaign converts a compromised non-human identity into rapid tenant-scale resource discovery, credential access, data destruction, and recovery impairment. The observed operation used ordinary Azure APIs and valid RBAC, so prevention and detection depend heavily on workload-identity hygiene and behavioral control-plane analytics rather than malware signatures.

Exploitation Status

Microsoft directly observed and investigated the destructive Azure incident. Published facts include two compromised service principals, 300+ reads, 150+ destructive/credential operations, 100+ storage-deletion attempts, resource deletions, lock-deletion attempts, and successful key retrievals. Initial access remains unconfirmed. The earlier JADEPUFFER/Langflow agentic-ransomware activity provides context, but this hunt focuses on Microsoft’s confirmed Azure control-plane observations.

What this hunt looks for

Published infrastructure, unusual service-principal sign-ins, high-volume resource discovery, App Service configuration reads, storage-key retrieval, deletion bursts, recovery-lock tampering, and tightly sequenced discovery-to-destruction.

Required logs

AzureActivity is required for Azure Resource Manager behavior. AADServicePrincipalSignInLogs is required for workload-identity authentication. AzureDiagnostics or resource-specific App Service logs and Microsoft Defender for Cloud alerts add coverage.

Hunt 1 — High-likelihood published infrastructure across Azure control-plane logs

let Storm3168IPs = dynamic(["45.131.66.106","34.153.223.102","64.20.53.230"]);
AzureActivity
| where TimeGenerated > ago(30d)
| extend SrcIp = tostring(coalesce(CallerIpAddress, tostring(parse_json(Properties).clientIpAddress)))
| where SrcIp in (Storm3168IPs)
| project TimeGenerated, SubscriptionId, Caller, SrcIp, OperationNameValue, ActivityStatusValue, ResourceGroup, ResourceId, Properties
| order by TimeGenerated desc

Hunt 2 — Service-principal sign-ins from published IPs or observed user agent

let Storm3168IPs = dynamic(["45.131.66.106","34.153.223.102","64.20.53.230"]);
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(30d)
| where IPAddress in (Storm3168IPs) or UserAgent has "python-requests/2.34.2"
| project TimeGenerated, ServicePrincipalId, ServicePrincipalName, IPAddress, UserAgent, ResourceDisplayName, ResultType, ResultDescription, CorrelationId
| order by TimeGenerated desc

Hunt 3 — Sustained high-volume discovery by one caller

AzureActivity
| where TimeGenerated > ago(30d)
| where ActivityStatusValue =~ "Success"
| where OperationNameValue endswith "/read" or OperationNameValue has_any ("list", "get")
| summarize Reads=count(), ResourceTypes=dcount(ResourceProviderValue), Resources=dcount(ResourceId), SampleOperations=make_set(OperationNameValue, 20), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by bin(TimeGenerated, 1h), Caller, CallerIpAddress, SubscriptionId
| where Reads >= 50 and Resources >= 10
| order by Reads desc

Hunt 4 — Bulk resource deletion bursts

AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue endswith "/delete" or OperationNameValue has "delete"
| summarize DeleteAttempts=count(), SuccessfulDeletes=countif(ActivityStatusValue =~ "Success"), ResourceGroups=dcount(ResourceGroup), Resources=dcount(ResourceId), Operations=make_set(OperationNameValue, 25) by bin(TimeGenerated, 10m), Caller, CallerIpAddress, SubscriptionId
| where DeleteAttempts >= 10 or Resources >= 5
| order by DeleteAttempts desc

Hunt 5 — Storage account deletion and access-key collection

AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue has_any ("Microsoft.Storage/storageAccounts/delete", "Microsoft.Storage/storageAccounts/listKeys/action", "listkeys")
| project TimeGenerated, SubscriptionId, Caller, CallerIpAddress, OperationNameValue, ActivityStatusValue, ResourceGroup, ResourceId, CorrelationId
| order by TimeGenerated desc

Hunt 6 — Recovery protection or resource-lock deletion

AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue has "delete"
| where OperationNameValue has_any ("Microsoft.Authorization/locks", "Microsoft.RecoveryServices", "backup", "siteRecovery") or ResourceId has_any ("/locks/", "/Microsoft.RecoveryServices/")
| project TimeGenerated, SubscriptionId, Caller, CallerIpAddress, OperationNameValue, ActivityStatusValue, ResourceGroup, ResourceId, Properties
| order by TimeGenerated desc

Hunt 7 — App Service configuration enumeration by workload identities

AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue has_any ("Microsoft.Web/sites/config/list", "Microsoft.Web/sites/config/read", "Microsoft.Web/sites/host/listkeys/action")
| project TimeGenerated, SubscriptionId, Caller, CallerIpAddress, OperationNameValue, ActivityStatusValue, ResourceGroup, ResourceId, CorrelationId
| order by TimeGenerated desc

Hunt 8 — Discovery followed by destructive activity from the same caller

let lookback = 30d;
let Discovery = AzureActivity
| where TimeGenerated > ago(lookback) and ActivityStatusValue =~ "Success"
| where OperationNameValue endswith "/read" or OperationNameValue has_any ("list", "get")
| summarize DiscoveryStart=min(TimeGenerated), DiscoveryEnd=max(TimeGenerated), Reads=count(), Resources=dcount(ResourceId) by Caller, SubscriptionId;
let Destruction = AzureActivity
| where TimeGenerated > ago(lookback)
| where OperationNameValue has "delete"
| summarize DestructionStart=min(TimeGenerated), Deletes=count(), DeleteOps=make_set(OperationNameValue, 20) by Caller, SubscriptionId;
Discovery
| join kind=inner Destruction on Caller, SubscriptionId
| where DestructionStart between (DiscoveryStart .. DiscoveryEnd + 24h)
| where Reads >= 50 and Deletes >= 5
| project Caller, SubscriptionId, DiscoveryStart, DiscoveryEnd, Reads, Resources, DestructionStart, Deletes, DeleteOps
| order by DestructionStart desc

Hunt 9 — Rare service-principal source IP and user-agent combinations

let baseline = AADServicePrincipalSignInLogs
| where TimeGenerated between (ago(30d) .. ago(1d)) and tostring(ResultType) == "0"
| summarize Baseline=count() by ServicePrincipalId, IPAddress, UserAgent;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(1d) and tostring(ResultType) == "0"
| summarize Recent=count(), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Resources=make_set(ResourceDisplayName, 20) by ServicePrincipalId, ServicePrincipalName, IPAddress, UserAgent
| join kind=leftanti baseline on ServicePrincipalId, IPAddress, UserAgent
| order by Recent desc

Detection Notes

  • Hunts 4–6 are highest signal because mass deletions, key retrieval, and recovery-control deletion are rare and directly mirror observed activity.
  • Caller may be an application ID, object ID, UPN, or claim-derived value depending on Azure Activity export. Resolve it against Entra inventory before acting.
  • The thresholds reflect campaign scale but must be reduced for small tenants and automation-heavy identities. Approved infrastructure-as-code teardown can closely resemble destruction.
  • AADServicePrincipalSignInLogs requires Entra diagnostic export and sufficient licensing. Token acquisition may precede retention, while control-plane use remains visible in AzureActivity.
  • The published IPs are high-confidence but not exhaustive and may be reused or changed. User-agent strings are spoofable.
  • Data-plane deletions may require Storage, SQL, Key Vault, or Defender telemetry; AzureActivity primarily covers ARM control-plane operations.