Detection rules › Kusto

T1566.002 Spearphishing Link - Rare URL Clicks

Group by
ParentImage, URLHost
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

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

TacticTechniques
Initial AccessT1566.002 Phishing: Spearphishing Link

Event coverage

ProviderEventTitle
SysmonEvent ID 1Process 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
Threshold
le 5

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.

FieldKindValues
EventIDeq
  • 1 transforms: cased corpus 237 (splunk 224, kusto 13)
Prevalencele
  • 5 transforms: cased
RenderedDescriptionmatch
  • brave.exe
  • chrome.exe
  • firefox.exe
  • http://
  • https://
  • msedge.exe

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.

FieldSource
ParentImagesummarize
Prevalencesummarize
URLHostsummarize