SOCLIFE
SOCLIFE

Evidence-led security analysis

Published knowledge

Search SOC//LIFE

CasesCASE-006

SOC investigation

SSPR Abuse Turns a Cloud Identity into an Azure Breach

Social-engineered SSPR let an attacker replace MFA methods, take over a cloud identity, enumerate Microsoft Graph, attempt service-principal persistence, and expand into Azure.

Based on publicly reported attack activity. User identities and workstation names have been anonymized.

User
denis@example.com
Initial alert
Suspicious password recovery and authentication-method changes
Severity
Critical

Case story

What happened

The analyst begins with an unexpected self-service password reset and must determine whether the user's authentication methods and subsequent cloud activity were attacker-controlled.

  1. SSPR is socially engineered

    The attacker initiates password recovery and persuades the targeted user to approve MFA prompts that appear legitimate.

  2. Password and authentication methods are replaced

    The password is reset, existing security methods are removed, and attacker-controlled Authenticator registration is established.

  3. Microsoft Graph enumerates users and applications

    A custom Python workflow issues Graph requests to map users, roles, applications, and potential high-value targets.

  4. Service-principal credential addition is attempted

    The actor attempts to add credentials to a compromised service principal, but the action fails because the user lacks sufficient permissions.

  5. Microsoft 365 and Azure resources are targeted

    The actor uses the compromised identities and Azure RBAC to access data, publishing credentials, secrets, storage keys, SQL, and VM management paths.

Investigation

What was checked

Follow how the analyst tested and revised explanations. This is discovery order, not event chronology.
  1. Confirm the SSPR event

    The reset establishes the first reliable investigation anchor.

    Next pivot

    Review authentication-method changes immediately after the reset.

    View query
    Q-01

    Recover the SSPR reset sequence

    What this checks

    Find successful self-service password-reset activity for the affected identity and preserve the reset flow and correlation context.

    KQL
    let target_user = "denis@example.com";
    AuditLogs
    | where TimeGenerated >= ago(7d)
    | where LoggedByService =~ "Self-service Password Management"
    | where OperationName in~ (
        "Reset password (self-service)",
        "Self-service password reset flow activity progress",
        "Blocked from self-service password reset"
    )
    | extend
        InitiatingUser = tolower(tostring(InitiatedBy.user.userPrincipalName)),
        TargetUser = tolower(tostring(TargetResources[0].userPrincipalName)),
        TargetId = tostring(TargetResources[0].id)
    | where InitiatingUser == tolower(target_user) or TargetUser == tolower(target_user)
    | project
        TimeGenerated,
        OperationName,
        Result,
        ResultReason,
        InitiatingUser,
        TargetUser,
        TargetId,
        AdditionalDetails,
        CorrelationId
    | order by TimeGenerated asc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-7d
    (
        OperationName="Reset password (self-service)"
        OR OperationName="Self-service password reset flow activity progress"
        OR OperationName="Blocked from self-service password reset"
    )
    | eval
        user=lower(coalesce(user, userPrincipalName, InitiatingUserPrincipalName, TargetUserPrincipalName)),
        operation=coalesce(operation, OperationName, ActivityDisplayName),
        result=lower(coalesce(result, Result)),
        correlation_id=coalesce(correlation_id, CorrelationId)
    | where user="denis@example.com"
    | fields _time user operation result ResultReason correlation_id AdditionalDetails
    | sort 0 _time
    What to look for

    A successful self-service reset for denis@example.com together with the surrounding reset-flow events.

    Technical details
    Tested signal

    Successful self-service password reset and related flow-progress events for the affected user.

    Assumptions
    • Microsoft Entra AuditLogs includes Self-service Password Management events.
    • The target user can be recovered from the target-resource object or initiating user context.
    Data requirements and relevant fields
    identity

    Microsoft Entra audit events for self-service password reset and authentication-method changes.

    • TimeGenerated
    • LoggedByService
    • Category
    • OperationName
    • Result
    • ResultReason
    • InitiatedBy
    • TargetResources
    • AdditionalDetails
    • CorrelationId
    KQL schema

    Validate connector availability, nested-field shape, retention, and licensing before operational use.

    SPL schema

    Replace index/sourcetype placeholders and map the documented identity, Graph, and Azure control-plane concepts to local fields.

    Limitations
    • Legitimate users can complete SSPR during normal recovery.
    • The reset event alone does not establish social engineering.

    KQL uses Microsoft Entra / Azure Monitor telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

    More reasoning

    Observation

    The incident begins with an unexpected password-recovery event for a cloud identity.

    Working explanation

    The user may have been socially engineered into completing an attacker-initiated password reset.

    What was checked

    Recover the self-service reset and surrounding flow-progress events for the affected identity.

    Interpretation

    SSPR is legitimate; the security question is what changed around it and whether the user intended the recovery.

    Supporting evidence
  2. Review MFA and security-information changes

    Method churn immediately after SSPR materially raises confidence in account takeover.

    Next pivot

    Inspect the first successful sign-ins after the reset.

    View query
    Q-02

    Review authentication-method churn

    What this checks

    Determine whether existing authentication methods were removed and new security information was registered after the password reset.

    KQL
    let target_user = "denis@example.com";
    AuditLogs
    | where TimeGenerated >= ago(7d)
    | where OperationName in~ (
        "User deleted security information",
        "User registered security info",
        "Delete user authentication method",
        "Add user authentication method",
        "Update user authentication methods",
        "User changed default security information"
    )
    | extend
        InitiatingUser = tolower(tostring(InitiatedBy.user.userPrincipalName)),
        TargetUser = tolower(tostring(TargetResources[0].userPrincipalName)),
        TargetId = tostring(TargetResources[0].id),
        InitiatingIP = tostring(InitiatedBy.user.ipAddress)
    | where InitiatingUser == tolower(target_user) or TargetUser == tolower(target_user)
    | project
        TimeGenerated,
        OperationName,
        Result,
        ResultReason,
        InitiatingUser,
        TargetUser,
        TargetId,
        InitiatingIP,
        TargetResources,
        AdditionalDetails,
        CorrelationId
    | order by TimeGenerated asc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-7d
    (
        OperationName="User deleted security information"
        OR OperationName="User registered security info"
        OR OperationName="Delete user authentication method"
        OR OperationName="Add user authentication method"
        OR OperationName="Update user authentication methods"
        OR OperationName="User changed default security information"
    )
    | eval
        user=lower(coalesce(user, userPrincipalName, InitiatingUserPrincipalName, TargetUserPrincipalName)),
        operation=coalesce(operation, OperationName, ActivityDisplayName),
        src=coalesce(src, InitiatingIpAddress, ipAddress),
        result=lower(coalesce(result, Result)),
        correlation_id=coalesce(correlation_id, CorrelationId)
    | where user="denis@example.com"
    | fields _time user src operation result ResultReason correlation_id TargetResources AdditionalDetails
    | sort 0 _time
    What to look for

    A reset followed by deletion/replacement of security information for the same identity, matching Microsoft's documented takeover sequence.

    Technical details
    Tested signal

    Authentication-method deletion and registration events occurring shortly after the SSPR reset.

    Assumptions
    • Authentication-method changes are present in Entra AuditLogs.
    • The same target identity can be normalized across password-reset and authentication-method events.
    Data requirements and relevant fields
    identity

    Microsoft Entra audit events for self-service password reset and authentication-method changes.

    • TimeGenerated
    • LoggedByService
    • Category
    • OperationName
    • Result
    • ResultReason
    • InitiatedBy
    • TargetResources
    • AdditionalDetails
    • CorrelationId
    KQL schema

    Validate connector availability, nested-field shape, retention, and licensing before operational use.

    SPL schema

    Replace index/sourcetype placeholders and map the documented identity, Graph, and Azure control-plane concepts to local fields.

    Limitations
    • A legitimate lost-device recovery can also involve deleting and re-registering security information.
    • Registration audit data should be validated against helpdesk/identity-recovery context.

    KQL uses Microsoft Entra / Azure Monitor telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

    More reasoning

    Observation

    Microsoft observed existing methods being removed and Authenticator being enrolled on the attacker's device.

    Working explanation

    The attacker may have replaced the user's authentication methods after resetting the password.

    What was checked

    Search for deletion, registration, update, and default-security-information changes for the same identity.

    Interpretation

    A legitimate lost-device recovery can look similar, so helpdesk and recovery context remains necessary.

    Supporting evidence
  3. Establish attacker sign-in context

    A new source, device, or risk context identifies the session to follow into cloud activity.

    Next pivot

    Use the user object ID, session, and source to inspect Microsoft Graph activity.

    View query
    Q-03

    Identify the first post-reset sign-in

    What this checks

    Compare successful sign-ins after the reset with the user's previous source-IP and device baseline.

    KQL
    let target_user = "denis@example.com";
    let reset_time = datetime(<RESET_TIME_UTC>);
    let baseline_ips =
        SigninLogs
        | where TimeGenerated between (reset_time - 30d .. reset_time)
        | where UserPrincipalName =~ target_user
        | where tostring(ResultType) == "0"
        | summarize by IPAddress;
    SigninLogs
    | where TimeGenerated between (reset_time .. reset_time + 6h)
    | where UserPrincipalName =~ target_user
    | where tostring(ResultType) == "0"
    | join kind=leftanti baseline_ips on IPAddress
    | project
        TimeGenerated,
        UserPrincipalName,
        UserId,
        IPAddress,
        AppDisplayName,
        AppId,
        ResourceDisplayName,
        AuthenticationRequirement,
        RiskLevelDuringSignIn,
        RiskLevelAggregated,
        RiskState,
        ConditionalAccessStatus,
        DeviceDetail,
        UserAgent,
        AutonomousSystemNumber,
        CorrelationId,
        SessionId
    | order by TimeGenerated asc
    SPL
    index=<entra_signin_index> sourcetype=<entra_signin_sourcetype>
    earliest=-30d latest=<reset_plus_6h>
    | eval
        user=lower(coalesce(user, UserPrincipalName, user_principal_name)),
        src=coalesce(src, IPAddress, ip_address),
        result=coalesce(result, ResultType),
        app_name=coalesce(app_name, AppDisplayName),
        resource=coalesce(resource, ResourceDisplayName),
        session_id=coalesce(session_id, SessionId),
        correlation_id=coalesce(correlation_id, CorrelationId),
        is_post_reset=if(_time>=<reset_epoch> AND _time<=<reset_plus_6h_epoch>,1,0)
    | where user="denis@example.com" AND tostring(result)="0"
    | eventstats
        count(eval(is_post_reset=0)) as historical_events
        values(eval(if(is_post_reset=0,src,null()))) as historical_ips
        by user
    | where is_post_reset=1 AND mvfind(historical_ips,src)<0
    | fields _time user src app_name resource RiskLevelDuringSignIn RiskState DeviceDetail UserAgent session_id correlation_id
    | sort 0 _time
    What to look for

    A successful post-reset sign-in from a previously unseen source, especially when device, user agent, or risk context is also unusual.

    Technical details
    Tested signal

    Successful sign-in after SSPR from a source IP not seen for the user in the previous thirty days.

    Assumptions
    • The analyst supplies the reset time from Q-01.
    • SigninLogs retains at least thirty days of history for the user.
    Data requirements and relevant fields
    authentication

    Microsoft Entra sign-in events with source, app/resource, device, risk, and session/correlation context.

    • TimeGenerated
    • UserPrincipalName
    • UserId
    • IPAddress
    • AppDisplayName
    • AppId
    • ResourceDisplayName
    • ResultType
    • ResultDescription
    • ConditionalAccessStatus
    • AuthenticationRequirement
    • RiskLevelDuringSignIn
    • RiskLevelAggregated
    • RiskState
    • DeviceDetail
    • UserAgent
    • AutonomousSystemNumber
    • CorrelationId
    • SessionId
    KQL schema

    Validate connector availability, nested-field shape, retention, and licensing before operational use.

    SPL schema

    Replace index/sourcetype placeholders and map the documented identity, Graph, and Azure control-plane concepts to local fields.

    Limitations
    • Travel, VPN changes, mobile networks, and new corporate devices can create legitimate first-seen sources.
    • A known source IP does not clear the identity if the authentication methods were maliciously replaced.

    KQL uses Microsoft Entra / Azure Monitor telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

    More reasoning

    Observation

    The compromised account becomes usable from attacker-controlled infrastructure after password and MFA recovery.

    Working explanation

    A successful post-reset sign-in may originate from a source not previously associated with the user.

    What was checked

    Compare post-reset sign-ins with the user's thirty-day successful-sign-in source baseline.

    Interpretation

    New IP alone is weak; it becomes meaningful when joined with SSPR and authentication-method replacement.

    Supporting evidence
  4. Measure directory discovery

    A multi-endpoint enumeration burst shows the account being used for tenant discovery rather than normal end-user activity.

    Next pivot

    Check whether the same identity attempted to modify application or service-principal credentials.

    View query
    Q-04

    Measure Microsoft Graph directory enumeration

    What this checks

    Determine whether the compromised user rapidly enumerated users, applications, service principals, or role-related endpoints after takeover.

    KQL
    let target_user_id = "<USER_OBJECT_ID>";
    MicrosoftGraphActivityLogs
    | where TimeGenerated >= ago(7d)
    | where UserId == target_user_id
    | where RequestMethod =~ "GET"
    | where ResponseStatusCode between (200 .. 299)
    | where RequestUri has_any (
        "/users",
        "/applications",
        "/servicePrincipals",
        "/directoryRoles",
        "/roleManagement/directory"
    )
    | extend EndpointFamily = case(
        RequestUri has "/servicePrincipals", "servicePrincipals",
        RequestUri has "/applications", "applications",
        RequestUri has "/roleManagement/directory", "roleManagement",
        RequestUri has "/directoryRoles", "directoryRoles",
        RequestUri has "/users", "users",
        "other"
    )
    | summarize
        FirstSeen=min(TimeGenerated),
        LastSeen=max(TimeGenerated),
        Requests=count(),
        EndpointFamilies=dcount(EndpointFamily),
        Families=make_set(EndpointFamily, 10),
        SampleUris=make_set(RequestUri, 40),
        StatusCodes=make_set(ResponseStatusCode, 10)
        by UserId, IPAddress, AppId, SessionId, UserAgent
    | where Requests >= 20 and EndpointFamilies >= 3
    | order by Requests desc
    SPL
    index=<graph_activity_index> sourcetype=<microsoft_graph_activity_sourcetype>
    earliest=-7d
    | eval
        user_id=coalesce(user_id, UserId),
        src=coalesce(src, IPAddress),
        method=upper(coalesce(method, RequestMethod)),
        uri=coalesce(uri, RequestUri),
        status=coalesce(status, ResponseStatusCode),
        app_id=coalesce(app_id, AppId),
        session_id=coalesce(session_id, SessionId),
        user_agent=coalesce(user_agent, UserAgent)
    | where user_id="<USER_OBJECT_ID>" AND method="GET" AND status>=200 AND status<300
    | eval endpoint_family=case(
        like(uri,"%/servicePrincipals%"),"servicePrincipals",
        like(uri,"%/applications%"),"applications",
        like(uri,"%/roleManagement/directory%"),"roleManagement",
        like(uri,"%/directoryRoles%"),"directoryRoles",
        like(uri,"%/users%"),"users",
        true(),"other"
    )
    | where endpoint_family!="other"
    | stats
        min(_time) as first_seen
        max(_time) as last_seen
        count as requests
        dc(endpoint_family) as endpoint_families
        values(endpoint_family) as families
        values(uri) as sample_uris
        by user_id src app_id session_id user_agent
    | where requests>=20 AND endpoint_families>=3
    | convert ctime(first_seen) ctime(last_seen)
    | sort - requests
    What to look for

    A burst of successful Graph requests across users, applications, service principals, and role endpoints from the compromised session/source.

    Technical details
    Tested signal

    High-volume successful GET requests across several directory-discovery endpoints by the compromised user.

    Assumptions
    • Microsoft Graph Activity Logs are enabled and the user's Entra object ID is known.
    • The target identity performs little or no bulk Graph directory enumeration during normal work.
    Data requirements and relevant fields
    cloud control plane

    Microsoft Graph Activity Logs with caller identity, request URI/method, response status, source IP, app, session, and token context.

    • TimeGenerated
    • AppId
    • ClientAuthMethod
    • DeviceId
    • IPAddress
    • OperationId
    • RequestId
    • RequestMethod
    • RequestUri
    • ResponseSizeBytes
    • ResponseStatusCode
    • Roles
    • Scopes
    • ServicePrincipalId
    • SessionId
    • SignInActivityId
    • TokenIssuedAt
    • UniqueTokenId
    • UserAgent
    • UserId
    • Wids
    KQL schema

    Validate connector availability, nested-field shape, retention, and licensing before operational use.

    SPL schema

    Replace index/sourcetype placeholders and map the documented identity, Graph, and Azure control-plane concepts to local fields.

    Limitations
    • Legitimate admin tools, IAM automation, and inventory jobs can perform similar Graph enumeration.
    • Thresholds must be baselined by user role, application, and automation identity.

    KQL uses Microsoft Entra / Azure Monitor telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

    More reasoning

    Observation

    Microsoft observed custom Python automation enumerating users and applications through Microsoft Graph.

    Working explanation

    The compromised identity may be mapping high-value accounts, roles, applications, and service principals.

    What was checked

    Measure successful Graph GET requests across user, application, service-principal, and role-related endpoints.

    Interpretation

    Admin tools and inventory jobs can look similar, so user role, app ID, session, and automation ownership matter.

    Supporting evidence
  5. Check application persistence attempts

    The source-reported attempt failed, but the behavior establishes the persistence objective and narrows further hunting.

    Next pivot

    Scope high-impact Azure operations performed by the user or suspicious source.

    View query
    Q-05

    Check application and service-principal credential changes

    What this checks

    Identify attempts to add alternate credentials to applications or service principals after identity takeover.

    KQL
    let target_user = "denis@example.com";
    AuditLogs
    | where TimeGenerated >= ago(7d)
    | where Category =~ "ApplicationManagement"
    | where OperationName has_any (
        "Add service principal",
        "Certificates and secrets management"
    )
    | extend
        InitiatingUser = tolower(tostring(InitiatedBy.user.userPrincipalName)),
        InitiatingIP = tostring(InitiatedBy.user.ipAddress)
    | where InitiatingUser == tolower(target_user)
    | mv-apply TargetResource = TargetResources on (
        where TargetResource.type in~ ("Application", "ServicePrincipal")
        | extend
            TargetName = tostring(TargetResource.displayName),
            TargetId = tostring(TargetResource.id),
            TargetType = tostring(TargetResource.type),
            ModifiedProperties = TargetResource.modifiedProperties
    )
    | project
        TimeGenerated,
        OperationName,
        Result,
        ResultReason,
        InitiatingUser,
        InitiatingIP,
        TargetName,
        TargetId,
        TargetType,
        ModifiedProperties,
        CorrelationId
    | order by TimeGenerated asc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-7d
    (
        OperationName="Add service principal credentials"
        OR OperationName="Add service principal"
        OR OperationName="Update application - Certificates and secrets management"
        OR OperationName="Certificates and secrets management"
    )
    | eval
        user=lower(coalesce(user, InitiatingUserPrincipalName, userPrincipalName)),
        src=coalesce(src, InitiatingIpAddress, ipAddress),
        operation=coalesce(operation, OperationName, ActivityDisplayName),
        result=lower(coalesce(result, Result)),
        target_name=coalesce(target_name, TargetResourceDisplayName),
        target_id=coalesce(target_id, TargetResourceId),
        target_type=coalesce(target_type, TargetResourceType),
        correlation_id=coalesce(correlation_id, CorrelationId)
    | where user="denis@example.com"
    | fields _time user src operation result ResultReason target_name target_id target_type correlation_id ModifiedProperties
    | sort 0 _time
    What to look for

    A successful or failed attempt by the compromised user to add or modify application/service-principal credentials.

    Technical details
    Tested signal

    Credential-management changes to an application or service principal initiated by the compromised identity.

    Assumptions
    • Entra AuditLogs includes ApplicationManagement events.
    • The initiating user is known from the compromise investigation.
    Data requirements and relevant fields
    identity

    Microsoft Entra audit events for self-service password reset and authentication-method changes.

    • TimeGenerated
    • LoggedByService
    • Category
    • OperationName
    • Result
    • ResultReason
    • InitiatedBy
    • TargetResources
    • AdditionalDetails
    • CorrelationId
    KQL schema

    Validate connector availability, nested-field shape, retention, and licensing before operational use.

    SPL schema

    Replace index/sourcetype placeholders and map the documented identity, Graph, and Azure control-plane concepts to local fields.

    Limitations
    • Legitimate application owners and administrators routinely rotate credentials.
    • The source report states the Storm-2949 persistence attempt failed, so failed operations remain investigation-relevant.

    KQL uses Microsoft Entra / Azure Monitor telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

    More reasoning

    Observation

    Storm-2949 attempted to add credentials to a compromised service principal.

    Working explanation

    The actor may be trying to create cloud persistence that survives remediation of the user account.

    What was checked

    Review application-management audit events for credential-management operations initiated by the compromised identity.

    Interpretation

    Failed high-risk control-plane actions can be just as important as successful ones during incident scoping.

    Supporting evidence
  6. Scope cloud control-plane expansion

    Control-plane evidence defines which workloads, credentials, keys, and data stores require containment and rotation.

    Next pivot

    Contain the identity, rotate exposed secrets/keys, restore control-plane settings, and scope downstream data access.

    View query
    Q-06

    Scope high-risk Azure control-plane operations

    What this checks

    Search for source-reported Azure management operations that can expose publishing credentials, weaken network controls, list storage keys, or create VM-level access.

    KQL
    let target_user = "denis@example.com";
    let suspicious_ip = "<SUSPICIOUS_IP>";
    AzureActivity
    | where TimeGenerated >= ago(7d)
    | where Caller =~ target_user or CallerIpAddress == suspicious_ip
    | where OperationNameValue in~ (
        "Microsoft.Web/sites/publishxml/action",
        "Microsoft.Sql/servers/firewallRules/write",
        "Microsoft.Storage/storageAccounts/write",
        "Microsoft.Storage/storageAccounts/listKeys/action",
        "Microsoft.Compute/virtualMachines/extensions/write",
        "Microsoft.Compute/virtualMachines/runCommand/action"
    )
    | project
        TimeGenerated,
        Caller,
        CallerIpAddress,
        OperationNameValue,
        ActivityStatusValue,
        ActivitySubstatusValue,
        ResourceId,
        ResourceGroup,
        ResourceProviderValue,
        SubscriptionId,
        Authorization_d,
        Properties_d,
        CorrelationId
    | order by TimeGenerated asc
    SPL
    index=<azure_activity_index> sourcetype=<azure_activity_sourcetype>
    earliest=-7d
    | eval
        user=lower(coalesce(user, Caller)),
        src=coalesce(src, CallerIpAddress),
        operation=coalesce(operation, OperationNameValue, OperationName),
        status=coalesce(status, ActivityStatusValue),
        resource_id=coalesce(resource_id, ResourceId),
        correlation_id=coalesce(correlation_id, CorrelationId)
    | where user="denis@example.com" OR src="<SUSPICIOUS_IP>"
    | where operation IN (
        "Microsoft.Web/sites/publishxml/action",
        "Microsoft.Sql/servers/firewallRules/write",
        "Microsoft.Storage/storageAccounts/write",
        "Microsoft.Storage/storageAccounts/listKeys/action",
        "Microsoft.Compute/virtualMachines/extensions/write",
        "Microsoft.Compute/virtualMachines/runCommand/action"
    )
    | table _time user src operation status resource_id ResourceGroup SubscriptionId correlation_id
    | sort 0 _time
    What to look for

    One or more high-impact Azure control-plane operations that move the incident beyond identity compromise into workload or data-plane exposure.

    Technical details
    Tested signal

    High-impact Azure management operations performed by the compromised identity or from its suspicious source.

    Assumptions
    • AzureActivity is exported to the workspace.
    • The compromised user UPN or suspicious source IP is known.
    Data requirements and relevant fields
    cloud control plane

    Azure Activity control-plane events with caller, source IP, operation, target resource, status, authorization, and correlation context.

    • TimeGenerated
    • Caller
    • CallerIpAddress
    • Category
    • OperationName
    • OperationNameValue
    • ActivityStatusValue
    • ActivitySubstatusValue
    • Resource
    • ResourceGroup
    • ResourceId
    • ResourceProvider
    • ResourceProviderValue
    • SubscriptionId
    • Authorization_d
    • Properties_d
    • CorrelationId
    KQL schema

    Validate connector availability, nested-field shape, retention, and licensing before operational use.

    SPL schema

    Replace index/sourcetype placeholders and map the documented identity, Graph, and Azure control-plane concepts to local fields.

    Limitations
    • These operations can be legitimate for cloud administrators and automation.
    • The query is a source-backed scoping pivot, not a generic malicious-operation list.

    KQL uses Microsoft Entra / Azure Monitor telemetry. SPL is a normalized raw-event scaffold and requires local field mapping.

    More reasoning

    Observation

    The actor later abused Azure management features and high-value secrets to expand across App Service, Key Vault, Storage, SQL, and VMs.

    Working explanation

    The compromised identity's Azure RBAC permissions may have enabled workload and data-plane compromise.

    What was checked

    Search source-backed high-impact Azure management operations by the compromised user or suspicious source.

    Interpretation

    The breach is now cloud-wide; identity remediation alone is no longer sufficient.

    Supporting evidence

Response

Actions to take

Contain affected systems, preserve evidence, and scope the same behavior elsewhere.
  • Disable or otherwise contain the compromised cloud identity when active attacker use is suspected.
  • Reset the user's password through a trusted recovery path.
  • Remove attacker-controlled authentication methods and require re-registration of trusted methods.
  • Revoke active sessions and refresh tokens.
  • Review and remediate any application or service-principal credentials added by the compromised identity.
  • Review Microsoft Graph activity for directory, application, role, and privilege discovery.
  • Scope OneDrive, SharePoint, Azure App Service, Key Vault, Storage, SQL, and VM control-plane activity according to the user's permissions.
  • Rotate secrets, publishing credentials, storage keys, SAS material, or database credentials that were exposed.
  • Restore network/firewall policies and remove unauthorized VM access or remote-management persistence.
  • Require phishing-resistant MFA for privileged identities and harden SSPR/helpdesk recovery procedures.

Conclusion

What was concluded

This Case starts with identity recovery rather than malware. Microsoft assessed with high confidence that Storm-2949 used social engineering consistent with SSPR abuse, then removed existing authentication methods and enrolled Microsoft Authenticator on an attacker-controlled device.

The investigation follows the point where a legitimate cloud control becomes attacker-controlled: password recovery, MFA method replacement, unfamiliar sign-in context, Microsoft Graph discovery, attempted application persistence, and high-impact Azure management operations.

The wider Azure actions are source-backed Storm-2949 behaviors. They are not invented as if every compromised user performed every operation.

Technical detail

Technical evidence

Stable evidence anchors preserve the fields behind the investigation story.
E-01

E-01Identity telemetry

Microsoft assessed with high confidence that Storm-2949 used social engineering consistent with SSPR abuse, persuading targeted users to approve legitimate-looking MFA prompts so the attacker could reset passwords.

Account
denis@example.com
Entry point
Self-service password reset
Social engineering
Fraudulent MFA approval during recovery
Referenced by
E-02

E-02Identity telemetry

After gaining control, the actor removed existing authentication methods and enrolled Microsoft Authenticator on an attacker-controlled device.

Persistence
Authentication-method replacement
Method
Microsoft Authenticator
Effect
Persistent attacker-controlled MFA access
Referenced by
E-03

E-03Cloud control-plane event

The compromised identity was used with a custom Python script to enumerate users and applications through Microsoft Graph.

API
Microsoft Graph
Discovery
Users, applications, service principals and privileged targets
Client context
Custom Python automation
Referenced by
E-04

E-04Identity telemetry

The actor attempted to add credentials to a compromised service principal so access could continue independently of the compromised user accounts; the attempt failed because of insufficient permissions.

Target
Compromised service principal
Persistence attempt
Add alternate credential
Result
Failed due to insufficient permissions
Referenced by
E-05

E-05Cloud control-plane event

The attack expanded into Microsoft 365 and Azure, including OneDrive/SharePoint exfiltration, App Service publishing-profile access, Key Vault secret access, Storage key listing, SQL firewall manipulation, and VM management features.

Scope
Microsoft 365 + Azure SaaS/PaaS/IaaS
High value targets
Key Vault, App Service, Storage, SQL, VMs
Objective
Sensitive-data exfiltration and continued access
Referenced by

Detection engineering

Would your SOC catch this behavior?

See the detection built for this investigation.
View detection

Behavior context

ATT&CK and sources

Behavior mapping

MITRE ATT&CK

  • T1078.004 · Cloud AccountsEnterprise ATT&CK 19.2

    The actor used compromised Microsoft Entra cloud accounts to operate across Microsoft 365 and Azure.

  • T1098.005 · Device RegistrationEnterprise ATT&CK 19.2

    After the reset, the actor registered Microsoft Authenticator on an attacker-controlled device for persistence.

  • T1087.004 · Cloud AccountEnterprise ATT&CK 19.2

    The actor used Microsoft Graph to enumerate cloud users and identify privileged or high-value identities.

  • The actor attempted to add credentials to a compromised service principal to maintain independent cloud access.

These mappings describe the behavior examined here. They do not establish attribution.

Review boundary

Sources and limits

Last reviewed
External sources
6

This case documents the available evidence and analytical limits; control effectiveness is environment-specific.