← Back to SOC feed Coverage →

Malicious Email Campaigns by Recipient URL Clicks

kql MEDIUM Azure-Sentinel
T1566
EmailEventsEmailUrlInfoUrlClickEvents
backdoorhuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-09-07T23:00:00Z · Confidence: medium

Hunt Hypothesis

This hypothesis targets adversaries leveraging phishing campaigns (T1566) that achieve high user engagement through malicious URL clicks, indicating successful initial access or credential harvesting. Proactively hunting for these high-interaction campaigns allows the SOC to identify compromised endpoints and lateral movement paths before attackers fully establish a foothold in the Azure environment.

KQL Query

//This query ranks malicious inbound email campaigns from the last 30 days by recipient URL clicks, not by size.
//A small campaign that reached mailboxes and got clicked is realised risk; a large one blocked at delivery is not,
//so ranking by engagement inverts the triage order compared with ranking by message volume.
//Campaigns carrying QR codes are flagged, since the URL is not visible to the user or to link inspection.
//Phishing simulation and SecOps mailbox traffic are excluded so internal exercises do not inflate the ranking.
let ClicksByCluster = UrlClickEvents
    | where Timestamp > ago(30d)
    | join kind=inner (EmailEvents | where Timestamp > ago(30d) | where EmailClusterId > 0 | distinct NetworkMessageId, EmailClusterId) on NetworkMessageId
    | summarize UrlClicks = count(), ClickingUsers = dcount(AccountUpn) by EmailClusterId;
let QrClusters = EmailUrlInfo
    | where Timestamp > ago(30d)
    | where UrlLocation == "QRCode"
    | join kind=inner (EmailEvents | where Timestamp > ago(30d) | where EmailClusterId > 0 | distinct NetworkMessageId, EmailClusterId) on NetworkMessageId
    | summarize QrMessages = dcount(NetworkMessageId) by EmailClusterId;
EmailEvents
| where Timestamp > ago(30d)
| where EmailDirection == "Inbound" and isnotempty(ThreatTypes) and EmailClusterId > 0
| where OrgLevelPolicy !in ("Phishing simulation", "SecOps Mailbox")
| extend Key = strcat(NetworkMessageId, "-", RecipientEmailAddress)
| summarize arg_max(Timestamp, *) by Key
| summarize Messages = count(),
            Recipients = dcount(RecipientEmailAddress),
            SenderDomains = dcount(SenderFromDomain),
            SampleSenderDomains = make_set(SenderFromDomain, 5),
            ThreatTypesRaw = make_set(ThreatTypes, 50),
            SampleSubjects = make_set(Subject, 5),
            FirstSeen = min(Timestamp),
            LastSeen = max(Timestamp)
        by EmailClusterId
| join kind=inner (ClicksByCluster) on EmailClusterId
| where UrlClicks > 0
| join kind=leftouter (QrClusters) on EmailClusterId
| extend CarriedQrCode = iif(coalesce(QrMessages, 0) > 0, "QR code", "")
| extend ThreatMix = array_sort_asc(set_union(split(strcat_array(ThreatTypesRaw, ", "), ", "), dynamic([])))
| top 20 by UrlClicks
| project ['Email Cluster ID']=EmailClusterId, ['URL Clicks']=UrlClicks, ['Clicking Users']=ClickingUsers,
          ['Messages']=Messages,
          ['Recipients']=Recipients, ['Sender Domains']=SenderDomains,
          ['Sample Sender Domains']=SampleSenderDomains, ['Threat Mix']=ThreatMix,
          ['QR Code']=CarriedQrCode, ['Sample Subjects']=SampleSubjects,
          ['First Seen']=FirstSeen, ['Last Seen']=LastSeen

Analytic Rule Definition

id: 28ce9655-f35f-4733-af43-8266b39a87f1
name: Malicious Email Campaigns by Recipient URL Clicks
description: |
  This query ranks malicious inbound email campaigns by the number of URL clicks they attracted and by how many distinct users clicked, rather than by campaign size.
description-detailed: |
  Microsoft Defender for Office 365 groups related malicious messages into campaigns through EmailClusterId. This query ranks those campaigns over the last 30 days by the number of recipient URL clicks they attracted, using Advanced hunting in Microsoft Defender XDR, and flags campaigns that carried QR codes. Both the total number of clicks and the number of distinct users behind them are returned, so one repeat clicker is not mistaken for broad engagement. Ranking by clicks rather than by message volume answers a different question from campaign size: a small campaign that reached mailboxes and was clicked represents realised risk, whereas a large campaign that was blocked at delivery did not. For triage that inverts the priority order, because the campaigns worth investigating first are the ones users actually engaged with. Messages are de-duplicated to the latest record per message and recipient, and phishing simulation and SecOps mailbox traffic are excluded so internal exercises do not inflate the ranking.
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
  dataTypes:
  - EmailEvents
  - EmailUrlInfo
  - UrlClickEvents
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  //This query ranks malicious inbound email campaigns from the last 30 days by recipient URL clicks, not by size.
  //A small campaign that reached mailboxes and got clicked is realised risk; a large one blocked at delivery is not,
  //so ranking by engagement inverts the triage order compared with ranking by message volume.
  //Campaigns carrying QR codes are flagged, since the URL is not visible to the user or to link inspection.
  //Phishing simulation and SecOps mailbox traffic are excluded so internal exercises do not inflate the ranking.
  let ClicksByCluster = UrlClickEvents
      | where Timestamp > ago(30d)
      | join kind=inner (EmailEvents | where Timestamp > ago(30d) | where EmailClusterId > 0 | distinct NetworkMessageId, EmailClusterId) on NetworkMessageId
      | summarize UrlClicks = count(), ClickingUsers = dcount(AccountUpn) by EmailClusterId;
  let QrClusters = EmailUrlInfo
      | where Timestamp > ago(30d)
      | where UrlLocation == "QRCode"
      | join kind=inner (EmailEvents | where Timestamp > ago(30d) | where EmailClusterId > 0 | distinct NetworkMessageId, EmailClusterId) on NetworkMessageId
      | summarize QrMessages = dcount(NetworkMessageId) by EmailClusterId;
  EmailEvents
  | where Timestamp > ago(30d)
  | where EmailDirection == "Inbound" and isnotempty(ThreatTypes) and EmailClusterId > 0
  | where OrgLevelPolicy !in ("Phishing simulation", "SecOps Mailbox")
  | extend Key = strcat(NetworkMessageId, "-", Recipient

Required Data Sources

Sentinel TableNotes
EmailEventsEnsure this data connector is enabled
EmailUrlInfoEnsure this data connector is enabled
UrlClickEventsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/Microsoft 365 Defender/Email and Collaboration Queries/Top Attacks/Malicious Email Campaigns by Recipient URL Clicks.yaml