SOCLIFE
SOCLIFE

Evidence-led security analysis

Published knowledge

Search SOC//LIFE

HuntsHUNT-004

Hypothesis-led threat hunting

Hunt for Illicit OAuth Consent and Persistent Cloud Access

Hunts for rare or sensitive OAuth consent, expands affected users and apps, checks permission escalation, and reviews sign-in activity tied to newly authorized applications.

Question

Hunt goal

One or more users may have authorized an unexpected OAuth application that retained access to Microsoft 365 resources through delegated permissions.

Why this hunt

Microsoft documents consent phishing as a technique that grants malicious cloud applications access to legitimate services and data. Normal password reset or MFA remediation does not remove the external application's consent grant.

Data sources

Where to look

  • IdentityEntra application-management audit events with user, target service principal, permission scope, operation, result, and correlation context.
  • AuthenticationEntra user sign-ins with user, app, app ID, source, resource, result, Conditional Access, authentication requirement, and correlation context.

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

    Find new consent grants with sensitive scopes

    Finding

    The starting search identifies newly observed service principals whose consent includes offline access or sensitive delegated scopes.

    This is the behavior-first candidate population and does not depend on a campaign IOC.

    View query
    Q-01First search

    Find new consent grants with sensitive scopes

    What this checks

    Use a wider hunt window to identify newly observed service principals receiving user consent with offline access or sensitive delegated permissions.

    KQL
    let detection_window = 7d;
    let baseline_window = 60d;
    
    let historical_apps =
        AuditLogs
        | where TimeGenerated between (ago(baseline_window) .. ago(detection_window))
        | where LoggedByService =~ "Core Directory"
        | where Category =~ "ApplicationManagement"
        | where OperationName =~ "Consent to application"
        | mv-apply TargetResource = TargetResources on (
            where TargetResource.type =~ "ServicePrincipal"
            | extend ServicePrincipalId = tostring(TargetResource.id)
        )
        | summarize HistoricalConsents=count() by ServicePrincipalId;
    
    AuditLogs
    | where TimeGenerated >= ago(detection_window)
    | where LoggedByService =~ "Core Directory"
    | where Category =~ "ApplicationManagement"
    | where OperationName =~ "Consent to application"
    | where Result =~ "success"
    | extend
        InitiatingUser = tolower(tostring(InitiatedBy.user.userPrincipalName)),
        InitiatingIP = tostring(InitiatedBy.user.ipAddress)
    | mv-apply TargetResource = TargetResources on (
        where TargetResource.type =~ "ServicePrincipal"
        | extend
            AppDisplayName = tostring(TargetResource.displayName),
            ServicePrincipalId = tostring(TargetResource.id),
            ModifiedProperties = TargetResource.modifiedProperties
    )
    | mv-apply Property = ModifiedProperties on (
        where Property.displayName =~ "ConsentAction.Permissions"
        | extend ConsentFull = trim(@'"', tostring(Property.newValue))
    )
    | parse ConsentFull with * "ConsentType: " ConsentType ", Scope: " GrantScope "]" *
    | extend SensitiveScope =
        GrantScope has "offline_access"
        or GrantScope has_any (
            "Mail.Read",
            "Mail.ReadWrite",
            "Mail.Send",
            "Files.Read.All",
            "Files.ReadWrite.All",
            "Contacts.Read",
            "Contacts.ReadWrite"
        )
    | join kind=leftouter historical_apps on ServicePrincipalId
    | extend HistoricalConsents = coalesce(HistoricalConsents, 0)
    | where HistoricalConsents == 0 and SensitiveScope
    | project
        TimeGenerated,
        InitiatingUser,
        InitiatingIP,
        AppDisplayName,
        ServicePrincipalId,
        ConsentType,
        GrantScope,
        HistoricalConsents,
        CorrelationId
    | order by TimeGenerated desc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-60d
    (OperationName="Consent to application" OR ActivityDisplayName="Consent to application")
    | eval
        user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
        src=coalesce(src, ipAddress, InitiatingIpAddress),
        app_name=lower(coalesce(app_name, TargetResourceDisplayName, AppDisplayName)),
        service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId),
        permissions=lower(coalesce(permissions, ConsentActionPermissions, GrantScope)),
        consent_type=coalesce(consent_type, ConsentType),
        correlation_id=coalesce(correlation_id, CorrelationId),
        result=lower(coalesce(result, Result)),
        is_current=if(_time>=relative_time(now(),"-7d"),1,0)
    | where result="success"
    | eval sensitive_scope=if(
        like(permissions,"%offline_access%")
        OR like(permissions,"%mail.read%")
        OR like(permissions,"%mail.readwrite%")
        OR like(permissions,"%mail.send%")
        OR like(permissions,"%files.read.all%")
        OR like(permissions,"%files.readwrite.all%")
        OR like(permissions,"%contacts.read%"),
        1,0
    )
    | eventstats count(eval(is_current=0)) as historical_consents by service_principal_id
    | where is_current=1 AND historical_consents=0 AND sensitive_scope=1
    | table _time user src app_name service_principal_id consent_type permissions historical_consents correlation_id
    | sort 0 - _time
    What to look for

    A population of new/sensitive consent grants suitable for app and user scoping.

    Technical details
    Tested signal

    New consent plus durable or sensitive permission scope.

    Assumptions
    • ApplicationManagement audit events are retained for at least sixty days.
    Data requirements and relevant fields
    identity

    Entra consent events with initiating identity, target service principal, permission scope, and correlation context.

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

    Uses documented Microsoft Entra/Azure Monitor fields; validate connector availability and local retention.

    SPL schema

    Replace index/sourcetype placeholders and map the documented concepts to the local Entra audit or sign-in source.

    Limitations
    • A sixty-day baseline can still miss very rarely used approved applications.

    This is the behavior-first starting point. It intentionally does not depend on the source-campaign app name.

  2. Step 2Pivot

    Hunt all high-value delegated grants

    Finding

    Sensitive consent and delegated-permission operations reveal applications that can access mail, files, contacts, or retain offline access.

    Permission scope determines potential impact and prioritizes applications for inventory validation.

    View query
    Q-02Pivot

    Hunt all high-value delegated grants

    What this checks

    Search both user consent and delegated-permission-grant operations for scopes that can provide durable or high-value access.

    KQL
    AuditLogs
    | where TimeGenerated >= ago(30d)
    | where LoggedByService =~ "Core Directory"
    | where Category =~ "ApplicationManagement"
    | where OperationName in~ ("Consent to application", "Add delegated permission grant")
    | where Result =~ "success"
    | extend InitiatingUser=tolower(tostring(InitiatedBy.user.userPrincipalName))
    | mv-expand TargetResource=TargetResources
    | extend
        TargetName=tostring(TargetResource.displayName),
        TargetType=tostring(TargetResource.type),
        TargetId=tostring(TargetResource.id),
        ModifiedProperties=TargetResource.modifiedProperties
    | mv-expand Property=ModifiedProperties
    | extend PropertyValue=tostring(Property.newValue)
    | where PropertyValue has "offline_access"
        or PropertyValue has_any (
            "Mail.Read",
            "Mail.ReadWrite",
            "Mail.Send",
            "Files.Read.All",
            "Files.ReadWrite.All",
            "Contacts.Read",
            "Contacts.ReadWrite"
        )
    | project TimeGenerated, OperationName, InitiatingUser, TargetName, TargetType, TargetId, PropertyValue, CorrelationId
    | order by TimeGenerated desc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-30d
    (OperationName="Consent to application"
     OR OperationName="Add delegated permission grant"
     OR ActivityDisplayName="Consent to application"
     OR ActivityDisplayName="Add delegated permission grant")
    | eval
        user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
        app_name=coalesce(app_name, TargetResourceDisplayName, AppDisplayName),
        service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId),
        permissions=lower(coalesce(permissions, Permissions, ConsentActionPermissions, GrantScope))
    | where
        like(permissions,"%offline_access%")
        OR like(permissions,"%mail.read%")
        OR like(permissions,"%mail.readwrite%")
        OR like(permissions,"%mail.send%")
        OR like(permissions,"%files.read.all%")
        OR like(permissions,"%files.readwrite.all%")
        OR like(permissions,"%contacts.read%")
    | table _time user src app_name service_principal_id permissions CorrelationId
    | sort 0 - _time
    What to look for

    Apps/users receiving offline access or sensitive mail, file, or contact scopes.

    Technical details
    Tested signal

    Sensitive delegated scope appears in application-management audit activity.

    Assumptions
    • Permission strings are retained in TargetResources or normalized local fields.
    Data requirements and relevant fields
    identity

    Application-management audit events containing permission-grant operations and modified-property details.

    • TimeGenerated
    • Category
    • OperationName
    • Result
    • InitiatedBy
    • TargetResources
    • CorrelationId
    KQL schema

    Uses documented Microsoft Entra/Azure Monitor fields; validate connector availability and local retention.

    SPL schema

    Replace index/sourcetype placeholders and map the documented concepts to the local Entra audit or sign-in source.

    Limitations
    • Scope strings can contain legitimate business permissions and require inventory context.

    KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.

  3. Step 3Pivot

    Find applications consented by many users

    Finding

    Applications with broad or rapid user consent define the likely blast radius of a consent-phishing campaign or risky rollout.

    Multi-user adoption is a prioritization signal, not proof; approved onboarding can look similar.

    View query
    Q-03Pivot

    Find applications consented by many users

    What this checks

    Rank service principals by unique consenting users and recent consent volume.

    KQL
    AuditLogs
    | where TimeGenerated >= ago(30d)
    | where OperationName =~ "Consent to application"
    | where Result =~ "success"
    | extend InitiatingUser=tolower(tostring(InitiatedBy.user.userPrincipalName))
    | mv-apply TargetResource=TargetResources on (
        where TargetResource.type =~ "ServicePrincipal"
        | extend
            AppDisplayName=tostring(TargetResource.displayName),
            ServicePrincipalId=tostring(TargetResource.id)
    )
    | summarize
        FirstSeen=min(TimeGenerated),
        LastSeen=max(TimeGenerated),
        ConsentEvents=count(),
        UniqueUsers=dcount(InitiatingUser),
        Users=make_set(InitiatingUser, 500)
        by AppDisplayName, ServicePrincipalId
    | order by UniqueUsers desc, LastSeen desc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-30d
    (OperationName="Consent to application" OR ActivityDisplayName="Consent to application")
    | eval
        user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
        app_name=coalesce(app_name, TargetResourceDisplayName, AppDisplayName),
        service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId)
    | stats
        min(_time) as first_seen
        max(_time) as last_seen
        count as consent_events
        dc(user) as unique_users
        values(user) as users
        by app_name service_principal_id
    | convert ctime(first_seen) ctime(last_seen)
    | sort - unique_users - last_seen
    What to look for

    New or unexpected apps with unusually broad user adoption.

    Technical details
    Tested signal

    One application accumulates consent across a wider user population.

    Assumptions
    • Consent audit coverage is consistent across the review window.
    Data requirements and relevant fields
    identity

    Consent events with user, target service principal, result, and correlation context.

    • TimeGenerated
    • OperationName
    • Result
    • InitiatedBy
    • TargetResources
    • CorrelationId
    KQL schema

    Uses documented Microsoft Entra/Azure Monitor fields; validate connector availability and local retention.

    SPL schema

    Replace index/sourcetype placeholders and map the documented concepts to the local Entra audit or sign-in source.

    Limitations
    • Enterprise rollouts can legitimately create rapid multi-user consent.

    KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.

  4. Step 4Pivot

    Find users consenting to several applications

    Finding

    Users with several new app grants can expose targeted consent abuse, developer activity, or weak application-governance controls.

    The threshold is intentionally exploratory and requires user-role context.

    View query
    Q-04Pivot

    Find users consenting to several applications

    What this checks

    Identify identities that granted consent to several distinct service principals in the hunt window.

    KQL
    AuditLogs
    | where TimeGenerated >= ago(30d)
    | where OperationName =~ "Consent to application"
    | where Result =~ "success"
    | extend InitiatingUser=tolower(tostring(InitiatedBy.user.userPrincipalName))
    | mv-apply TargetResource=TargetResources on (
        where TargetResource.type =~ "ServicePrincipal"
        | extend
            AppDisplayName=tostring(TargetResource.displayName),
            ServicePrincipalId=tostring(TargetResource.id)
    )
    | summarize
        ConsentEvents=count(),
        UniqueApps=dcount(ServicePrincipalId),
        Apps=make_set(AppDisplayName, 100),
        ServicePrincipals=make_set(ServicePrincipalId, 100)
        by InitiatingUser
    | where UniqueApps >= 3
    | order by UniqueApps desc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-30d
    (OperationName="Consent to application" OR ActivityDisplayName="Consent to application")
    | eval
        user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
        app_name=coalesce(app_name, TargetResourceDisplayName, AppDisplayName),
        service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId)
    | stats
        count as consent_events
        dc(service_principal_id) as unique_apps
        values(app_name) as apps
        values(service_principal_id) as service_principals
        by user
    | where unique_apps>=3
    | sort - unique_apps
    What to look for

    Users with several distinct newly consented applications who warrant focused review.

    Technical details
    Tested signal

    One user authorizes multiple distinct apps.

    Assumptions
    • User identifiers and service-principal IDs are normalized.
    Data requirements and relevant fields
    identity

    Consent events keyed by initiating user and service-principal identifier.

    • TimeGenerated
    • OperationName
    • Result
    • InitiatedBy
    • TargetResources
    KQL schema

    Uses documented Microsoft Entra/Azure Monitor fields; validate connector availability and local retention.

    SPL schema

    Replace index/sourcetype placeholders and map the documented concepts to the local Entra audit or sign-in source.

    Limitations
    • Power users and developers can legitimately integrate multiple SaaS apps.
    • The threshold is a hunt starting point, not a malicious verdict.

    KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.

  5. Step 5Pivot

    Find delegated permission and app-role grants

    Finding

    Delegated permission grants and service-principal app-role assignments identify cases where application access expanded or became privileged.

    These events require change-control validation because legitimate administrators generate the same operations.

    View query
    Q-05Pivot

    Find delegated permission and app-role grants

    What this checks

    Search for delegated permission grants and application role assignments that can expand an app's access beyond a simple user consent event.

    KQL
    AuditLogs
    | where TimeGenerated >= ago(7d)
    | where LoggedByService =~ "Core Directory"
    | where Category =~ "ApplicationManagement"
    | where OperationName in~ (
        "Add delegated permission grant",
        "Add app role assignment to the service principal"
    )
    | where Result =~ "success"
    | where tostring(TargetResources) has candidate_service_principal
    | extend
        InitiatingUser=tolower(tostring(InitiatedBy.user.userPrincipalName)),
        InitiatingApp=tostring(InitiatedBy.app.displayName),
        InitiatingIP=tostring(InitiatedBy.user.ipAddress)
    | project TimeGenerated, OperationName, InitiatingUser, InitiatingApp, InitiatingIP, TargetResources, AdditionalDetails, CorrelationId
    | order by TimeGenerated asc
    SPL
    index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
    earliest=-7d
    (OperationName="Add delegated permission grant"
     OR OperationName="Add app role assignment to the service principal"
     OR ActivityDisplayName="Add delegated permission grant"
     OR ActivityDisplayName="Add app role assignment to the service principal")
    | eval
        user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
        src=coalesce(src, ipAddress, InitiatingIpAddress),
        operation=coalesce(OperationName, ActivityDisplayName),
        service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId),
        correlation_id=coalesce(correlation_id, CorrelationId)
    | table _time user src operation app_name service_principal_id permissions correlation_id
    | sort 0 _time
    What to look for

    Privileged, tenant-wide, or otherwise unexpected application grants requiring owner/change validation.

    Technical details
    Tested signal

    Add delegated permission grant or app-role assignment to a service principal.

    Assumptions
    • ApplicationManagement audit events are retained.
    Data requirements and relevant fields
    identity

    Entra application-management audit events for delegated permission grants and service-principal app-role assignments.

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

    Uses documented Microsoft Entra/Azure Monitor fields; validate connector availability and local retention.

    SPL schema

    Replace index/sourcetype placeholders and map the documented concepts to the local Entra audit or sign-in source.

    Limitations
    • Legitimate administrator-led application onboarding produces the same audit activities.

    KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.

  6. Step 6Pivot

    Review sign-ins to newly consented applications

    Finding

    Sign-in telemetry helps distinguish dormant grants from app/user combinations that show actual authentication activity during the hunt window.

    A negative sign-in result does not fully clear an app because token/API use may require additional SaaS telemetry.

    View query
    Q-06Pivot

    Review sign-ins to newly consented applications

    What this checks

    Use the candidate app/user population to review when, where, and against which resources newly consented applications were used.

    KQL
    let suspicious_apps = dynamic(["Share-File Point Document"]);
    SigninLogs
    | where TimeGenerated >= ago(30d)
    | where AppDisplayName in~ (suspicious_apps)
    | project
        TimeGenerated,
        UserPrincipalName,
        AppDisplayName,
        AppId,
        IPAddress,
        ResourceDisplayName,
        ResultType,
        ConditionalAccessStatus,
        AuthenticationRequirement,
        CorrelationId
    | order by TimeGenerated asc
    SPL
    index=<identity_index> sourcetype=<entra_user_signin_sourcetype>
    earliest=-30d
    | eval
        user=lower(coalesce(user, UserPrincipalName, user_principal_name)),
        app_name=coalesce(app_name, AppDisplayName),
        app_id=coalesce(app_id, AppId),
        src=coalesce(src, IPAddress, ip_address),
        resource=coalesce(resource, ResourceDisplayName),
        result=coalesce(result, ResultType),
        correlation_id=coalesce(correlation_id, CorrelationId)
    | where app_name IN ("Share-File Point Document")
    | table _time user app_name app_id src resource result ConditionalAccessStatus AuthenticationRequirement correlation_id
    | sort 0 _time
    What to look for

    App/user sign-in activity that aligns with suspicious consent and helps prioritize active grants for containment.

    Technical details
    Tested signal

    User sign-ins associated with apps surfaced by the consent hunt.

    Assumptions
    • SigninLogs is available and the candidate app/user list is populated from earlier steps.
    Data requirements and relevant fields
    authentication

    Entra user sign-ins with user, app, app ID, source, resource, result, Conditional Access, authentication requirement, and correlation ID.

    • TimeGenerated
    • UserPrincipalName
    • AppDisplayName
    • AppId
    • IPAddress
    • ResourceDisplayName
    • ResultType
    • ConditionalAccessStatus
    • AuthenticationRequirement
    • CorrelationId
    KQL schema

    Uses documented Microsoft Entra/Azure Monitor fields; validate connector availability and local retention.

    SPL schema

    Replace index/sourcetype placeholders and map the documented concepts to the local Entra audit or sign-in source.

    Limitations
    • Token/API activity may not produce an interactive sign-in for every downstream operation.

    The example begins with the source-reported app name only as a source-scoped pivot. Replace/expand it with service principals returned by the behavior-first consent searches.

Blast radius

Wider-compromise pivots

  • Compare suspicious service principals with approved enterprise-application inventory.
  • Review publisher verification and ownership metadata for every candidate app.
  • Search the same permission scopes across all consent and delegated-grant events.
  • Identify users who authorized multiple unrelated applications.
  • Review admin consent and tenant-wide permission changes.
  • Scope Microsoft Graph, Exchange, SharePoint, and other sensitive resource grants.
  • Review downstream SaaS/API activity where app identity is preserved.
  • Audit user-consent policy and admin-consent workflow for preventable exposure.

Evidence threshold

What would increase confidence

  • Service principal is absent from approved application inventory.
  • Publisher is unverified or unexpected.
  • Grant includes offline_access.
  • Grant includes sensitive mail, file, contact, or send permissions.
  • Several users rapidly authorize the same new app.
  • One user authorizes several unusual apps.
  • Delegated permission or app-role assignments expand the candidate app's access.
  • Sign-in context shows the app/user combination becoming active after consent.
  • No approved owner or change record explains the integration.

Conclusion

Result and next action

The hunt demonstrates a reusable path from rare consent to sensitive permissions, affected users, privilege expansion, and application-linked sign-in context without relying on one named malicious app.

  • Revoke confirmed illicit consent grants.
  • Disable or remove malicious enterprise applications/service principals where appropriate.
  • Identify every user who authorized the same app.
  • Review exposed resources according to the granted scopes.
  • Review downstream app/SaaS activity when telemetry preserves app identity.
  • Reset credentials only when independent credential compromise is present.
  • Restrict user consent to approved/verified low-risk applications.
  • Use admin-consent workflow for higher-risk requests.

Illicit OAuth consent is a cloud-authorization problem rather than a password-only problem. The hunt begins with rare and sensitive consent events, expands applications across users, checks users across applications, reviews delegated and app-role grants, and then inspects sign-in context for the strongest candidates.

The source-reported Share-File Point Document application is retained only as a source-scoped pivot. The main hunt is behavior-first so that changed application names or infrastructure do not defeat the workflow.

A confirmed illicit grant should move to application-focused containment: revoke consent, remove or disable the malicious service principal when appropriate, scope every affected user and resource, and then address separate credential compromise if evidence supports it.

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.