Query Details

Suspicious Sign In After Phishing Link Click

Query

# *Suspicious Sign-in After Phishing Link Click*

## Query Information

#### MITRE ATT&CK Technique(s)

| Technique ID | Title    | Link    |
| ---  | --- | --- |
| T1566 | Phishing | https://attack.mitre.org/techniques/T1566 |

#### Description

This query detects suspicious sign-in activity from a user who has recently clicked on a phishing link. It correlates email click events with sign-in logs, identifying sign-ins from new or infrequent IP addresses after a user has interacted with a suspicious email. The rule uses configurable parameters for sender email addresses, sender domains, and subject keywords to identify phishing emails.

#### Author <Optional>
- **Name: Benjamin Zulliger**
- **Github: https://github.com/benscha/KQLAdvancedHunting**
- **LinkedIn: https://www.linkedin.com/in/benjamin-zulliger/**


## Defender XDR
```KQL
// Parameter
let CampaignStartUtc = datetime(2026-01-13T00:00:00Z);
let LookbackHistory = 29d;
let HistoryStart = CampaignStartUtc - LookbackHistory;
// Filter options - populate these as needed
let SenderEmailAddresses = dynamic([]);  // e.g., ["[email protected]", "[email protected]"]
let SenderDomains = dynamic([]);         // e.g., ["evil.com", "badactor.net"]
let SubjectKeywords = dynamic([          
    "bring me some beers",
    "craft beer please",
    "i prefer a juicy New England IPA ;-)"
]);
// Use and Logic for SenderEmailAddresses, SenderDomains and SubjectKeywords
let UseAndLogic = false;  // true = ALL criteria must match, false = AT LEAST ONE must match
let ExcludedApps = dynamic(["TestAppTEST1", "TestAppTEST2"]);
// Affected Users
let PhishClicks = materialize(
    EmailEvents
    | where TimeGenerated >= CampaignStartUtc
    | extend 
        // We check if a filter is active (not empty)
        EmailFilterActive = array_length(SenderEmailAddresses) > 0,
        DomainFilterActive = array_length(SenderDomains) > 0,
        SubjectFilterActive = array_length(SubjectKeywords) > 0
    | extend 
        // Actual matches
        EmailMatches = SenderFromAddress has_any (SenderEmailAddresses),
        DomainMatches = SenderFromDomain has_any (SenderDomains),
        SubjectMatches = Subject has_any (SubjectKeywords)
    | where 
        (UseAndLogic == true and 
            (not(EmailFilterActive) or EmailMatches) and 
            (not(DomainFilterActive) or DomainMatches) and 
            (not(SubjectFilterActive) or SubjectMatches)
        )
        or 
        (UseAndLogic == false and 
            (
                // Logic: If multiple filters are provided, they must ALL match (Targeted Search)
                // If only one is provided, only that one must match.
                (not(EmailFilterActive) or EmailMatches) and 
                (not(DomainFilterActive) or DomainMatches) and 
                (not(SubjectFilterActive) or SubjectMatches)
            )
        )
    | project NetworkMessageId, Subject, SenderFromAddress, SenderFromDomain, RecipientEmailAddress
    | join kind=inner (
        UrlClickEvents 
        | where TimeGenerated >= CampaignStartUtc
        | project NetworkMessageId, ClickTime=TimeGenerated, Url, AccountUpn
    ) on NetworkMessageId
    | extend TargetUser = coalesce(AccountUpn, RecipientEmailAddress)
    | where isnotempty(TargetUser)
    | summarize 
        MinClickTime = min(ClickTime), 
        ClickedUrl = any(Url),
        Sender = any(SenderFromAddress),
        SenderDomain = any(SenderFromDomain)  
        by TargetUser, Subject
);
// Historical IPs
let HistoricalIPs = 
    SigninLogs
    | where TimeGenerated between (HistoryStart .. CampaignStartUtc)
    | where ResultType == 0
    | join kind=inner hint.strategy=broadcast (
        PhishClicks | project UserPrincipalName = TargetUser | distinct UserPrincipalName
    ) on UserPrincipalName
    | summarize IPSeenCount = count() by UserPrincipalName, IPAddress;
// Suspicious Sign-ins
SigninLogs
| where TimeGenerated >= CampaignStartUtc
| where ResultType == 0 
| where AppDisplayName !in (ExcludedApps)
| join kind=inner hint.strategy=broadcast PhishClicks on $left.UserPrincipalName == $right.TargetUser
| where TimeGenerated >= MinClickTime
| join kind=leftouter hint.strategy=broadcast HistoricalIPs on UserPrincipalName, IPAddress
| extend IPRiskClassification = case(
    isempty(IPSeenCount), "High Risk - New IP",
    IPSeenCount < 3,      "Medium Risk - Infrequent IP",
    "Low Risk"
)
| where IPRiskClassification != "Low Risk"
// Final Aggregation and Reporting
| summarize 
    FirstSignIn       = min(TimeGenerated),
    SignInCountBefore       = count(),
    AccessedApps      = make_set(AppDisplayName, 100),
    City              = any(tostring(LocationDetails.city)),
    Country           = any(tostring(LocationDetails.countryOrRegion)),
    PhishSubject      = any(Subject),
    PhishUrl          = any(ClickedUrl),
    PhishSender       = any(Sender),
    ClickTime         = any(MinClickTime)
    by TargetUser, IPAddress, IPRiskClassification
| project TargetUser, IPAddress, Country, City, IPRiskClassification, FirstSignIn, ClickTime, SignInCountBefore, AccessedApps, PhishSubject, PhishSender
// Filter for rarely used IPs. to reduce only to NEW used IPAdresses Filter to <1
| where SignInCountBefore <2
```

Explanation

This query is designed to detect suspicious sign-in activities following a user clicking on a phishing link. Here's a simplified breakdown of what the query does:

  1. Setup and Parameters:

    • The query starts by defining a campaign start date and a lookback period to analyze historical data.
    • It allows for filtering based on specific sender email addresses, domains, and subject keywords to identify potential phishing emails. These filters can be combined using "AND" or "OR" logic.
  2. Identifying Phishing Clicks:

    • It searches for email events where users received emails matching the specified criteria (sender, domain, subject).
    • It then correlates these emails with URL click events to identify users who clicked on links in these suspicious emails.
  3. Historical IP Analysis:

    • The query examines historical sign-in logs to determine the IP addresses from which users have previously signed in successfully.
    • It counts how often each IP address has been used by the user in the past.
  4. Detecting Suspicious Sign-ins:

    • It looks for sign-in attempts after the user clicked on a phishing link.
    • The query excludes certain applications from the analysis.
    • It classifies IP addresses as "High Risk" if they are new, "Medium Risk" if they are infrequently used, and "Low Risk" if they are commonly used.
  5. Final Reporting:

    • The query aggregates data to report on suspicious sign-ins, including details like the user's identity, IP address, location, risk classification, and the phishing email details.
    • It filters the results to show only those sign-ins from rarely used IP addresses.

Overall, this query helps identify potentially compromised accounts by correlating phishing email interactions with unusual sign-in activities.

Details

Benjamin Zulliger profile picture

Benjamin Zulliger

Released: January 15, 2026

Tables

EmailEventsUrlClickEventsSigninLogs

Keywords

EmailEventsUrlClickEventsSigninLogsNetworkMessageIdSubjectSenderFromAddressSenderFromDomainRecipientEmailAddressAccountUpnUserPrincipalNameIpAddressAppDisplayNameLocationDetailsCityCountryOrRegion

Operators

letdatetime()dynamic()materialize()extendarray_length()has_anywhereprojectjoinkind=innerkind=leftouterhint.strategy=broadcastcoalesce()isnotempty()summarizemin()any()between()case()isempty()tostring()make_set()!in!=

Actions