SOCLIFE
SOCLIFE

Evidence-led security analysis

Published knowledge

Search SOC//LIFE

HuntsHUNT-007

Hypothesis-led threat hunting

Hunt for SSPR Abuse and Cloud Control-Plane Expansion

Starts from self-service password reset, follows authentication-method replacement and unfamiliar sign-ins, then expands into Microsoft Graph discovery, application credentials, and Azure management operations.

Question

Hunt goal

One or more users may have been socially engineered through SSPR, had authentication methods replaced, and then been used to enumerate and expand through Microsoft 365 and Azure control planes.

Why this hunt

Microsoft's Storm-2949 investigation shows how a legitimate identity-recovery workflow can become the first step in a cloud-wide intrusion that relies on valid accounts and native management features rather than traditional malware.

Data sources

Where to look

  • IdentityMicrosoft Entra audit events for self-service password reset and authentication-method changes.
  • AuthenticationMicrosoft Entra sign-in events with source, app/resource, device, risk, and session/correlation context.
  • Cloud Control PlaneMicrosoft Graph Activity Logs and Azure Activity management-plane telemetry with caller/user identity, request or operation details, source IP, application/session context, target resource, status, authorization, and correlation identifiers.

Search path

Hunt steps

Each search broadens the view from patient zero to related users, devices, infrastructure, and follow-on activity.
  1. Step 1First search

    Inventory self-service password resets

    Finding

    The first search establishes who normally uses SSPR and highlights rare or sensitive-user resets.

    SSPR is a normal control, so the hunt starts with identity/population context rather than treating every reset as malicious.

    View query
    Q-01First search

    Inventory self-service password resets

    What this checks

    Build a baseline of successful SSPR usage by user, time, and surrounding recovery activity.

    KQL
    AuditLogs
    | where TimeGenerated >= ago(30d)
    | where LoggedByService =~ "Self-service Password Management"
    | where OperationName =~ "Reset password (self-service)"
    | where Result =~ "success"
    | extend
        TargetUser = tolower(tostring(TargetResources[0].userPrincipalName)),
        TargetUserId = tostring(TargetResources[0].id)
    | summarize
        FirstReset=min(TimeGenerated),
        LastReset=max(TimeGenerated),
        ResetCount=count(),
        Correlations=make_set(CorrelationId, 50)
        by TargetUser, TargetUserId
    | order by ResetCount asc, LastReset desc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-30d
    OperationName="Reset password (self-service)"
    | eval
        user=lower(coalesce(user, TargetUserPrincipalName, userPrincipalName)),
        result=lower(coalesce(result, Result)),
        correlation_id=coalesce(correlation_id, CorrelationId)
    | where result="success"
    | stats
        min(_time) as first_reset
        max(_time) as last_reset
        count as reset_count
        values(correlation_id) as correlations
        by user
    | convert ctime(first_reset) ctime(last_reset)
    | sort reset_count - last_reset
    What to look for

    Users with rare, clustered, or role-sensitive password-reset activity suitable for deeper review.

    Technical details
    Tested signal

    Successful self-service password resets across the tenant.

    Assumptions
    • Entra SSPR audit events are exported for the hunt window.
    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
    • SSPR is expected in many environments and baseline frequency varies by population.

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

  2. Step 2Pivot

    Find SSPR followed by authentication-method changes

    Finding

    Reset-plus-method-change sequences identify identities whose recovery materially changed account control.

    This is the closest reusable identity behavior to Microsoft's documented takeover path.

    View query
    Q-02Pivot

    Find SSPR followed by authentication-method changes

    What this checks

    Search for the account-control sequence that Microsoft observed after successful SSPR abuse.

    KQL
    let resets =
        AuditLogs
        | where TimeGenerated >= ago(7d)
        | where LoggedByService =~ "Self-service Password Management"
        | where OperationName =~ "Reset password (self-service)"
        | where Result =~ "success"
        | extend TargetUser=tolower(tostring(TargetResources[0].userPrincipalName))
        | project ResetTime=TimeGenerated, TargetUser, ResetCorrelationId=CorrelationId;
    let changes =
        AuditLogs
        | where TimeGenerated >= ago(7d)
        | where Result =~ "success"
        | 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
            TargetUser=tolower(tostring(TargetResources[0].userPrincipalName)),
            InitiatingIP=tostring(InitiatedBy.user.ipAddress)
        | project ChangeTime=TimeGenerated, TargetUser, OperationName, InitiatingIP, CorrelationId;
    resets
    | join kind=inner changes on TargetUser
    | where ChangeTime between (ResetTime .. ResetTime + 30m)
    | summarize
        Operations=make_set(OperationName, 20),
        ChangeCount=count(),
        FirstChange=min(ChangeTime),
        LastChange=max(ChangeTime),
        SourceIPs=make_set(InitiatingIP, 20)
        by TargetUser, ResetTime, ResetCorrelationId
    | order by ResetTime desc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-7d
    | eval
        user=lower(coalesce(user, TargetUserPrincipalName, userPrincipalName)),
        operation=coalesce(operation, OperationName, ActivityDisplayName),
        result=lower(coalesce(result, Result)),
        src=coalesce(src, InitiatingIpAddress, ipAddress),
        event_type=case(
            operation="Reset password (self-service)" AND result="success","reset",
            operation 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"
            ) AND result="success","auth_change",
            true(),"other"
        )
    | sort 0 user _time
    | streamstats current=f
        last(eval(if(event_type="reset",_time,null()))) as reset_time
        by user
    | where event_type="auth_change" AND isnotnull(reset_time)
        AND _time>=reset_time AND _time<=reset_time+1800
    | stats
        min(_time) as first_change
        max(_time) as last_change
        count as change_count
        values(operation) as operations
        values(src) as source_ips
        by user reset_time
    | convert ctime(reset_time) ctime(first_change) ctime(last_change)
    | sort - reset_time
    What to look for

    Users whose security information changed within thirty minutes of successful SSPR.

    Technical details
    Tested signal

    Successful SSPR followed by deletion, registration, or update of security information for the same user.

    Assumptions
    • Password-reset and authentication-method events share a normalized target user.
    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
    • Lost-device recovery can legitimately generate the same sequence.

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

  3. Step 3Pivot

    Find first-seen sign-in sources after SSPR

    Finding

    Unfamiliar post-reset sign-ins expose sessions that may belong to the attacker.

    Source novelty is supporting context and must be normalized for VPN/SWG/VDI/mobile behavior.

    View query
    Q-03Pivot

    Find first-seen sign-in sources after SSPR

    What this checks

    Identify successful sign-ins from sources not previously observed for users who recently reset their passwords.

    KQL
    let reset_users =
        AuditLogs
        | where TimeGenerated >= ago(7d)
        | where OperationName =~ "Reset password (self-service)"
        | where Result =~ "success"
        | extend TargetUser=tolower(tostring(TargetResources[0].userPrincipalName))
        | summarize ResetTime=max(TimeGenerated) by TargetUser;
    let history =
        SigninLogs
        | where TimeGenerated between (ago(30d) .. ago(7d))
        | where tostring(ResultType) == "0"
        | extend TargetUser=tolower(UserPrincipalName)
        | summarize by TargetUser, IPAddress;
    SigninLogs
    | where TimeGenerated >= ago(7d)
    | where tostring(ResultType) == "0"
    | extend TargetUser=tolower(UserPrincipalName)
    | join kind=inner reset_users on TargetUser
    | where TimeGenerated between (ResetTime .. ResetTime + 6h)
    | join kind=leftanti history on TargetUser, IPAddress
    | project
        TimeGenerated,
        ResetTime,
        TargetUser,
        UserId,
        IPAddress,
        AppDisplayName,
        ResourceDisplayName,
        RiskLevelDuringSignIn,
        RiskState,
        DeviceDetail,
        UserAgent,
        CorrelationId,
        SessionId
    | order by TimeGenerated desc
    SPL
    (
        index=<entra_audit_index> sourcetype=<entra_audit_sourcetype> earliest=-7d
        OperationName="Reset password (self-service)"
    )
    OR
    (
        index=<entra_signin_index> sourcetype=<entra_signin_sourcetype> earliest=-30d
    )
    | eval
        user=lower(coalesce(user, UserPrincipalName, TargetUserPrincipalName)),
        src=coalesce(src, IPAddress),
        result=coalesce(result, Result, ResultType),
        event_type=case(
            OperationName="Reset password (self-service)" AND lower(tostring(Result))="success","reset",
            tostring(result)="0","signin",
            true(),"other"
        ),
        is_current_signin=if(event_type="signin" AND _time>=relative_time(now(),"-7d"),1,0)
    | eventstats values(eval(if(event_type="signin" AND is_current_signin=0,src,null()))) as historical_ips by user
    | sort 0 user _time
    | streamstats current=f last(eval(if(event_type="reset",_time,null()))) as reset_time by user
    | where event_type="signin" AND is_current_signin=1 AND isnotnull(reset_time)
        AND _time>=reset_time AND _time<=reset_time+21600
        AND mvfind(historical_ips,src)<0
    | table _time reset_time user src AppDisplayName ResourceDisplayName RiskLevelDuringSignIn RiskState DeviceDetail UserAgent SessionId CorrelationId
    | sort - _time
    What to look for

    Post-reset sign-ins from unfamiliar source IPs or device/user-agent context.

    Technical details
    Tested signal

    New successful sign-in source after SSPR.

    Assumptions
    • SigninLogs retains a thirty-day baseline and the SSPR candidate population is known.
    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
    • VPN, SWG, VDI, travel, and mobile networks can create benign source novelty.

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

  4. Step 4Pivot

    Hunt Graph directory and application enumeration

    Finding

    Broad Graph enumeration can identify compromised human identities mapping users, applications, service principals, and roles.

    Approved IAM automation is the main benign lookalike and should be separated by app ID, source, user agent, and owner.

    View query
    Q-04Pivot

    Hunt Graph directory and application enumeration

    What this checks

    Identify users or sessions making unusually broad successful GET requests across users, applications, service principals, and role endpoints.

    KQL
    MicrosoftGraphActivityLogs
    | where TimeGenerated >= ago(7d)
    | where isnotempty(UserId)
    | 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, 50),
        SourceIPs=make_set(IPAddress, 20)
        by UserId, AppId, SessionId, UserAgent
    | where Requests >= 50 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),
        method=upper(coalesce(method, RequestMethod)),
        uri=coalesce(uri, RequestUri),
        status=coalesce(status, ResponseStatusCode),
        src=coalesce(src, IPAddress),
        app_id=coalesce(app_id, AppId),
        session_id=coalesce(session_id, SessionId),
        user_agent=coalesce(user_agent, UserAgent)
    | where isnotnull(user_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
        values(src) as source_ips
        by user_id app_id session_id user_agent
    | where requests>=50 AND endpoint_families>=3
    | convert ctime(first_seen) ctime(last_seen)
    | sort - requests
    What to look for

    Human user identities performing broad Graph enumeration inconsistent with their role or recent baseline.

    Technical details
    Tested signal

    High-volume, multi-endpoint Microsoft Graph discovery by a user identity.

    Assumptions
    • Microsoft Graph Activity Logs are enabled.
    • Legitimate IAM/admin automation can be identified by app ID, user agent, service principal, or known source.
    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
    • Large tenants can have approved inventory/admin workflows with high request volume.

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

  5. Step 5Pivot

    Hunt application and service-principal credential changes

    Finding

    Application credential activity identifies attempts to establish cloud persistence independent of the compromised user.

    Failed attempts still reveal attacker objectives and should guide further scoping.

    View query
    Q-05Pivot

    Hunt application and service-principal credential changes

    What this checks

    Find alternate credentials added or modified on applications and service principals, including failed attempts from recently compromised users.

    KQL
    AuditLogs
    | where TimeGenerated >= ago(7d)
    | where Category =~ "ApplicationManagement"
    | where OperationName has_any (
        "Add service principal",
        "Certificates and secrets management"
    )
    | extend
        InitiatingUser=tostring(InitiatedBy.user.userPrincipalName),
        InitiatingApp=tostring(InitiatedBy.app.displayName),
        InitiatingIP=iff(
            isnotempty(tostring(InitiatedBy.user.ipAddress)),
            tostring(InitiatedBy.user.ipAddress),
            tostring(InitiatedBy.app.ipAddress)
        )
    | 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,
        InitiatingApp,
        InitiatingIP,
        TargetName,
        TargetId,
        TargetType,
        ModifiedProperties,
        CorrelationId
    | order by TimeGenerated desc
    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
        initiating_user=lower(coalesce(initiating_user, InitiatingUserPrincipalName, userPrincipalName)),
        initiating_app=coalesce(initiating_app, InitiatingAppName),
        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)
    | table _time initiating_user initiating_app src operation result ResultReason target_name target_id target_type correlation_id ModifiedProperties
    | sort - _time
    What to look for

    Credential-management activity that is unusual for the initiating user, target application, source, or change window.

    Technical details
    Tested signal

    Application credential-management events from unexpected human identities or sources.

    Assumptions
    • AuditLogs includes ApplicationManagement credential events.
    • Approved app owners and secret/certificate rotation windows are available for validation.
    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
    • Credential rotation and application onboarding are common legitimate causes.

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

  6. Step 6Pivot

    Hunt high-impact Azure control-plane operations

    Finding

    High-impact Azure operations reveal whether the incident expanded into workloads, secrets, storage, databases, or VMs.

    Once these actions are tied to the compromised identity, the investigation becomes a cross-service cloud incident.

    View query
    Q-06Pivot

    Hunt high-impact Azure control-plane operations

    What this checks

    Search for the cloud-management operations Microsoft observed during expansion from compromised identity into App Service, Storage, SQL, and VMs.

    KQL
    AzureActivity
    | where TimeGenerated >= ago(7d)
    | 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"
    )
    | summarize
        FirstSeen=min(TimeGenerated),
        LastSeen=max(TimeGenerated),
        Operations=make_set(OperationNameValue, 20),
        Resources=make_set(ResourceId, 100),
        Statuses=make_set(ActivityStatusValue, 10),
        Correlations=make_set(CorrelationId, 50)
        by Caller, CallerIpAddress, SubscriptionId
    | extend OperationCount=array_length(Operations), ResourceCount=array_length(Resources)
    | order by OperationCount desc, ResourceCount desc
    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 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"
    )
    | stats
        min(_time) as first_seen
        max(_time) as last_seen
        dc(operation) as operation_count
        dc(resource_id) as resource_count
        values(operation) as operations
        values(resource_id) as resources
        values(status) as statuses
        values(correlation_id) as correlations
        by user src SubscriptionId
    | convert ctime(first_seen) ctime(last_seen)
    | sort - operation_count - resource_count
    What to look for

    Rare or unexpected high-impact Azure operations, especially when performed by recently reset users or from new sources.

    Technical details
    Tested signal

    High-impact Azure management actions performed by human identities or suspicious sources.

    Assumptions
    • AzureActivity is exported and caller/source fields are populated.
    • Expected infrastructure-as-code and cloud-administration identities are 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
    • Cloud administrators and automation legitimately perform many of these operations.

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

Blast radius

Wider-compromise pivots

  • Prioritize SSPR events involving privileged users, IT personnel, senior leadership, and break-glass-adjacent identities.
  • Compare authentication-method changes with registered device inventory and user-confirmed devices.
  • Track the suspicious source IP and session into Microsoft 365 workload activity.
  • Review OneDrive and SharePoint mass-download or unusual file-access behavior for compromised identities.
  • Inspect service-principal sign-ins after any successful credential addition.
  • Review Key Vault, Storage, SQL, App Service, and VM operations authorized by compromised identities.
  • Audit Azure RBAC paths that let user compromise turn into control-plane or data-plane access.
  • Harden SSPR/helpdesk workflows and require phishing-resistant authentication for high-value users.

Evidence threshold

What would increase confidence

  • User denies initiating the password reset.
  • Existing security information was removed immediately after SSPR.
  • A new authentication method was registered without user approval.
  • Successful sign-in follows from a source/device unfamiliar to the user.
  • Microsoft Graph directory enumeration occurs from the compromised user/session.
  • Application/service-principal credential changes are attempted or completed.
  • High-impact Azure operations occur from the same user/source.
  • No helpdesk ticket, change record, automation owner, or normal business workflow explains the sequence.

Conclusion

Result and next action

The hunt connects identity recovery abuse to cloud discovery and control-plane expansion without relying on malware or campaign-specific infrastructure.

  • Contain confirmed compromised identities and revoke active sessions.
  • Re-register trusted authentication methods.
  • Review and remediate application/service-principal credentials.
  • Rotate secrets, publishing credentials, storage keys, SAS material, and database credentials exposed by the attack path.
  • Restore altered Key Vault, Storage, SQL, App Service, and VM control-plane settings.
  • Scope Microsoft 365 and Azure data access/exfiltration.
  • Reduce standing Azure RBAC privileges and enforce phishing-resistant MFA for privileged users.
  • Add recovery-abuse monitoring to normal SOC identity triage.

The hunt begins with account recovery because the source campaign showed that a valid identity workflow can replace traditional malware as the first stage of a cloud intrusion.

It progresses from SSPR into authentication-method replacement, unfamiliar sign-in context, Graph directory discovery, application credentials, and Azure management actions.

The goal is not to label SSPR or Graph as malicious. The goal is to identify when a human identity suddenly moves through recovery, persistence, discovery, and control-plane behavior that does not match its normal role.

Context

ATT&CK and limits

Behavior mapping

MITRE ATT&CK

This mapping describes the valid-account behavior examined by the Hunt. It does not prove token theft, attribution, or technique-wide coverage.

Review boundary

Sources and limits

The conclusion stays bounded to the stated scope and available logs.