Query Details

HUNT 19 Email O Auth Consent Phishing 30d

Query

// Hunt    : Workload Identity - OAuth Consent Phishing Email and App Correlation (30d)
// Purpose : Hunts for phishing campaigns targeting OAuth consent grants by correlating:
//           (1) EmailUrlInfo URLs matching Microsoft OAuth2 / consent patterns,
//           (2) EmailEvents delivery metadata for recipient and delivery status,
//           (3) AuditLogs for application registrations matching extracted client_id values,
//           (4) EmailPostDeliveryEvents for remediation actions (ZAP, moved to junk).
//           Surfaces multi-stage illicit consent grant attempts where attackers register
//           a new app then deliver phishing emails containing the consent URL to target users.
// Tables  : EmailUrlInfo, EmailEvents, EmailAttachmentInfo, EmailPostDeliveryEvents, AuditLogs
// Period  : P30D
// Tactics : InitialAccess, Persistence
// MITRE   : T1566.002, T1078.004, T1098.003
//==========================================================================================

let LookbackDays   = 30d;
let NewAppLookback = 14d;

// Pattern matches both v1 and v2 OAuth endpoints and admin consent flows
let OAuth2Pattern  = @"(?i)(login\.microsoftonline\.com|login\.microsoft\.com)[^?]*\?(.*&)?client_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";
let ConsentPattern = @"(?i)(adminConsent=true|prompt=consent|response_type=code|scope=.*\.all)";

// New app registrations
let NewApps = AuditLogs
    | where TimeGenerated > ago(NewAppLookback)
    | where OperationName in~ ("Add application", "Add service principal")
    | where Result =~ "success"
    | extend AppId      = tostring(TargetResources[0].id)
    | extend AppName    = tostring(TargetResources[0].displayName)
    | extend CreatedBy  = tostring(InitiatedBy.user.userPrincipalName)
    | extend CreatorIP  = tostring(InitiatedBy.user.ipAddress)
    | summarize
        AppName   = any(AppName),
        CreatedBy = any(CreatedBy),
        CreatorIP = any(CreatorIP),
        CreatedAt = min(TimeGenerated)
        by AppId;

// URLs matching OAuth2 consent patterns in defender email telemetry
let ConsentUrls = union isfuzzy=true
    EmailUrlInfo,
    (datatable(NetworkMessageId:string, Url:string, UrlDomain:string,
               TimeGenerated:datetime)[])
    | where TimeGenerated > ago(LookbackDays)
    | where Url matches regex OAuth2Pattern
    | extend ExtractedClientId  = extract(OAuth2Pattern, 3, Url)
    | extend HasConsentSignal   = Url matches regex ConsentPattern
    | where isnotempty(ExtractedClientId)
    | project NetworkMessageId, Url, ExtractedClientId, HasConsentSignal,
              UrlSeen = TimeGenerated;

// Email delivery metadata
let EmailMeta = union isfuzzy=true
    EmailEvents,
    (datatable(NetworkMessageId:string, RecipientEmailAddress:string,
               SenderFromAddress:string, SenderFromDomain:string,
               Subject:string, DeliveryAction:string,
               DeliveryLocation:string, Timestamp:datetime)[])
    | where TimeGenerated > ago(LookbackDays)
    | project NetworkMessageId, RecipientEmailAddress, SenderFromAddress,
              SenderFromDomain, Subject, DeliveryAction, DeliveryLocation,
              EmailTime = Timestamp;

// Post-delivery remediation (ZAP, quarantine, moved to junk)
let PostDelivery = union isfuzzy=true
    EmailPostDeliveryEvents,
    (datatable(NetworkMessageId:string, ActionType:string,
               ActionResult:string, Timestamp:datetime)[])
    | where TimeGenerated > ago(LookbackDays)
    | summarize
        RemediationActions  = make_set(ActionType, 5),
        RemediationResults  = make_set(ActionResult, 5),
        Remediated          = countif(ActionResult =~ "Success")
        by NetworkMessageId;

// Attachment analysis for context
let Attachments = union isfuzzy=true
    EmailAttachmentInfo,
    (datatable(NetworkMessageId:string, FileName:string,
               FileType:string, SHA256:string)[])
    | where TimeGenerated > ago(LookbackDays)
    | summarize
        AttachmentFiles  = make_set(FileName, 5),
        AttachmentTypes  = make_set(FileType, 5)
        by NetworkMessageId;

// Combine everything
ConsentUrls
| join kind=leftouter EmailMeta      on NetworkMessageId
| join kind=leftouter PostDelivery   on NetworkMessageId
| join kind=leftouter Attachments    on NetworkMessageId
| join kind=leftouter NewApps on $left.ExtractedClientId == $right.AppId
| extend AppRegistered    = isnotempty(AppName)
| extend DaysSinceCreated = iff(AppRegistered, datetime_diff("day", UrlSeen, CreatedAt), int(null))
| summarize
    TotalEmails       = dcount(NetworkMessageId),
    Recipients        = make_set(RecipientEmailAddress, 30),
    RecipientCount    = dcount(RecipientEmailAddress),
    Senders           = make_set(SenderFromAddress, 5),
    SenderDomains     = make_set(SenderFromDomain, 5),
    Subjects          = make_set(Subject, 5),
    DeliveryActions   = make_set(DeliveryAction, 5),
    Remediated        = sum(Remediated),
    RemediationActions = make_set(RemediationActions, 5),
    ConsentUrls       = make_set(Url, 3),
    HasConsentSignal  = any(HasConsentSignal),
    AppName           = any(AppName),
    CreatedBy         = any(CreatedBy),
    DaysSinceCreated  = any(DaysSinceCreated),
    FirstSeen         = min(UrlSeen),
    LastSeen          = max(UrlSeen)
    by ExtractedClientId, AppRegistered
| extend Severity = case(
    AppRegistered and DaysSinceCreated <= 1 and RecipientCount > 5,  "Critical",
    AppRegistered and DaysSinceCreated <= 7,                          "High",
    AppRegistered,                                                     "Medium",
    HasConsentSignal and RecipientCount > 10,                          "Medium",
    "Low")
| order by Severity asc, RecipientCount desc

Explanation

This query is designed to detect phishing campaigns that target OAuth consent grants over the past 30 days. It does this by correlating data from several sources to identify suspicious activities. Here's a simplified breakdown of what the query does:

  1. Identify New Applications: It looks for new applications registered in the last 14 days, which could be used by attackers to request OAuth consent illicitly.

  2. Extract Consent URLs: It searches for URLs in emails that match patterns associated with Microsoft OAuth2 consent requests, extracting client IDs from these URLs.

  3. Gather Email Metadata: It collects metadata from emails, such as recipients, senders, and delivery actions, to understand how these consent URLs are being distributed.

  4. Check Post-Delivery Actions: It examines any remediation actions taken after email delivery, such as moving emails to junk or quarantining them, to see if the emails were flagged as suspicious.

  5. Analyze Attachments: It looks at email attachments to provide additional context, such as file names and types.

  6. Correlate Data: It combines all this information to identify cases where a new application was registered and then used in a phishing campaign to send emails with consent URLs to multiple recipients.

  7. Assess Severity: It assigns a severity level to each case based on factors like how recently the app was registered, the number of recipients, and whether consent signals were detected.

  8. Output: The results are ordered by severity and the number of recipients, helping prioritize investigation efforts.

Overall, the query helps identify and prioritize potential OAuth consent phishing attacks by correlating email and application registration data.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

EmailUrlInfoEmailEventsEmailAttachmentInfoEmailPostDeliveryEventsAuditLogs

Keywords

EmailUrlInfoEmailEventsEmailAttachmentInfoEmailPostDeliveryEventsAuditLogs

Operators

letagoin~=~tostringsummarizeanybyunionisfuzzymatchesregexextractisnotemptyprojectextendmake_setcountifjoinkindleftouteriffdatetime_diffintdcountminmaxcaseorderascdesc

Actions