Detection rules › Kusto
T1566.002 Spearphishing Link - Rare URL Clicks
Below query analyzes URLs that are opened from applications like Outlook, Word, Excel, Powerpoint, and Adobe PDF apps. It finds rare URLs that might be a phishing attempt.
It is strongly recommended to enrich results with prevalence information using firewall or proxy logs. You can reduce the noise by filtering specific parent processes according to your needs.
You can further improve the results using logic apps or scripting to get extra information about the URL(age, certificate, VT score etc.) Keep in mind that there ways to bypass controls by hosting the phishing links inside a document stored in the cloud. You don't have any visibility with Sysmon in this scenario.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1566.002 Phishing: Spearphishing Link |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Sysmon | Event ID 1 | Process creation |
Rule body kusto
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// https://posts.bluraven.io/hunting-for-phishing-links-using-sysmon-and-kql-e87d1118ce5e
//
//
// Query parameters:
// Define how manys days of data you want to analyze.
// Consider covering weekends
let lookback = 3d;
// Define how many user might receive the same phishing URL(based on URL or URLHost).
let PhishingTargetMax = 5;
// Get all URLs that were clicked
let PotentialPhishingLinks = materialize (
Event
| where TimeGenerated > ago(lookback)
| where Source == "Microsoft-Windows-Sysmon" and EventID == 1
// Get only the relevant events to improve the query performance during parsing
| where RenderedDescription has_any ("http://", "https://") and RenderedDescription has_any ("msedge.exe", "chrome.exe", "firefox.exe","brave.exe")
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type)
| extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""),
TechniqueName = column_ifexists("TechniqueName", ""),
ParentImage = tostring(ParentImage),
OriginalFileName = tostring(OriginalFileName),
CommandLine = tostring(CommandLine),
Computer = tostring(Computer)
| parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName
// Extract URL and URLHost
| extend URL = extract("((http|https):\\/\\/.*)\\s?",1,tostring(CommandLine))
| extend URLHost = tostring(parse_url(URL).Host)
)
;
// Perform frequency analysis.
// WARNING!!: Phishing URLs can be customized per target user or not.
// Perform 2 different analysis (one for URL, one for URLHost)
//// Frequency analysis by URLHost ////
PotentialPhishingLinks
| summarize Prevalence = dcount(Computer) by URLHost, ParentImage
| where Prevalence <= PhishingTargetMax
//// Get event details back. ////
| join kind=inner PotentialPhishingLinks on URLHost
// Filter only the last 1 day of events (if you perform analysis everyday)
| where TimeGenerated > ago(1d)
| project-reorder TimeGenerated, Prevalence, Computer, ParentImage, OriginalFileName , URLHost, URL, CommandLine
//// Frequency analysis by URL (comment out the above 8 lines, uncomment the below 8 lines) ////
// PotentialPhishingLinks
// | summarize Prevalence = dcount(Computer) by URL, ParentImage
// | where Prevalence <= PhishingTargetMax
// //// Get event details back. ////
// | join kind=inner PotentialPhishingLinks on URL
// // Filter only the last 1 day of events (if you perform analysis everyday)
// | where TimeGenerated > ago(1d)
// | project-reorder TimeGenerated, Prevalence, Computer, ParentImage, OriginalFileName, URLHost , URL, CommandLine
Stages and Predicates
Parameters
let lookback = 3d;
let PhishingTargetMax = 5;
The stages below define let PotentialPhishingLinks (the rule's main pipeline source).
Stage 1: source
let PotentialPhishingLinks
Stage 2: source
Event
Stage 3: where
| where TimeGenerated > ago(lookback)
Stage 4: where
| where Source == "Microsoft-Windows-Sysmon" and EventID == 1
Stage 5: where
| where RenderedDescription has_any ("http://", "https://") and RenderedDescription has_any ("msedge.exe", "chrome.exe", "firefox.exe","brave.exe")
Stage 6: extend
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
Stage 7: extend
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
Stage 8: mv-expand
| mv-expand bagexpansion=array EventData
Stage 9: evaluate
| evaluate bag_unpack(EventData)
Stage 10: extend
| extend Key=tostring(['@Name']), Value=['#text']
Stage 11: evaluate
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type)
Stage 12: extend
| extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""),
TechniqueName = column_ifexists("TechniqueName", ""),
ParentImage = tostring(ParentImage),
OriginalFileName = tostring(OriginalFileName),
CommandLine = tostring(CommandLine),
Computer = tostring(Computer)
Stage 13: parse
| parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName
Stage 14: extend
| extend URL = extract("((http|https):\\/\\/.*)\\s?",1,tostring(CommandLine))
Stage 15: extend
| extend URLHost = tostring(parse_url(URL).Host)
The stages below run on PotentialPhishingLinks (the outer pipeline).
Stage 16: summarize
PotentialPhishingLinks
| summarize Prevalence = dcount(Computer) by URLHost, ParentImage
Stage 17: where
| where Prevalence <= PhishingTargetMax
Stage 18: join
| join kind=inner PotentialPhishingLinks on URLHost
Stage 19: where
| where TimeGenerated > ago(1d)
Stage 20: project-reorder
| project-reorder TimeGenerated, Prevalence, Computer, ParentImage, OriginalFileName , URLHost, URL, CommandLine
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
EventID | eq |
|
Prevalence | le |
|
RenderedDescription | match |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
ParentImage | summarize |
Prevalence | summarize |
URLHost | summarize |