Detection rules › Kusto

Phishing link click observed in Network Traffic

Severity
medium
Time window
1d
Group by
ProductName, SystemAlertId
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'The purpose of this content is to identify successful phishing links accessed by users. Once a user clicks on a phishing link, we observe successful network activity originating from non-Microsoft network devices. These devices may include Palo Alto Networks, Fortinet, Check Point, and Zscaler devices.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1566 Phishing

Rule body kusto

id: 2fed0668-6d43-4c78-87e6-510f96f12145
name: Phishing link click observed in Network Traffic
description: |
  'The purpose of this content is to identify successful phishing links accessed by users. Once a user clicks on a phishing link, we observe successful network activity originating from non-Microsoft network devices. These devices may include Palo Alto Networks, Fortinet, Check Point, and Zscaler devices.'
severity: Medium
requiredDataConnectors:
  - connectorId:  OfficeATP
    dataTypes:
    - SecurityAlert
  - connectorId:  PaloAltoNetworks
    dataTypes:
    - CommonSecurityLog (PaloAlto)
  - connectorId:  Fortinet
    dataTypes:
    - CommonSecurityLog (Fortinet)
  - connectorId:  CheckPoint
    dataTypes:
    - CommonSecurityLog (CheckPoint)
  - connectorId:  Zscaler
    dataTypes:
    - CommonSecurityLog (Zscaler)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
    //Finding MDO Security alerts and extracting the Entities user, Domain, Ip, and URL.
    let Alert_List= dynamic([
    "Phishing link click observed in Network Traffic",
    "Phish delivered due to an IP allow policy",
    "A potentially malicious URL click was detected",
    "High Risk Sign-in Observed in Network Traffic",
    "A user clicked through to a potentially malicious URL",
    "Suspicious network connection to AitM phishing site",
    "Messages containing malicious entity not removed after delivery",
    "Email messages containing malicious URL removed after delivery",
    "Email reported by user as malware or phish",
    "Phish delivered due to an ETR override",
    "Phish not zapped because ZAP is disabled"]);
    SecurityAlert
    |where ProviderName in~ ("Office 365 Advanced Threat Protection", "OATP")
    | where AlertName in~ (Alert_List)
    //extracting Alert Entities
     | extend Entities = parse_json(Entities)
    | mv-apply Entity = Entities on
    (
    where Entity.Type == 'account'
    | extend EntityUPN = iff(isempty(Entity.UserPrincipalName), tostring(strcat(Entity.Name, "@", tostring (Entity.UPNSuffix))), tostring(Entity.UserPrincipalName))
    )
    | mv-apply Entity = Entities on
    (
    where Entity.Type == 'url'
    | extend EntityUrl = tostring(Entity.Url)
    )
    | summarize AccountUpn=tolower(tostring(take_any(EntityUPN))),Url=tostring(tolower(take_any(EntityUrl))),AlertTime= min(TimeGenerated)by SystemAlertId, ProductName
    // filtering 3pnetwork devices
    | join kind= inner (CommonSecurityLog
    | where DeviceVendor has_any  ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
    | where DeviceAction != "Block"
    | where DeviceProduct startswith "FortiGate" or DeviceProduct startswith  "PAN" or DeviceProduct startswith  "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith  "NSSWeblog" or DeviceProduct startswith "URL"
    | where isnotempty(RequestURL)
    | where isnotempty(SourceUserName)
    | extend SourceUserName = tolower(SourceUserName)
    | project
    3plogTime=TimeGenerated,
    DeviceVendor,
    DeviceProduct,
    Activity,
    DestinationHostName,
    DestinationIP,
    RequestURL=tostring(tolower(RequestURL)),
    MaliciousIP,
    Name = tostring(split(SourceUserName,"@")[0]),
    UPNSuffix =tostring(split(SourceUserName,"@")[1]),
    SourceUserName,
    IndicatorThreatType,
    ThreatSeverity,AdditionalExtensions,
    ThreatConfidence)on $left.Url == $right.RequestURL and $left.AccountUpn == $right.SourceUserName
    // Applied the condition where alert trigger 1st and then the 3p Network activity execution
    | where AlertTime between ((3plogTime - 1h) .. (3plogTime + 1h))
entityMappings:
    - entityType: Account
      fieldMappings:
      - identifier: FullName
        columnName: SourceUserName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
    - entityType: IP
      fieldMappings:
      - identifier: Address
        columnName: DestinationIP
    - entityType: DNS
      fieldMappings:
      - identifier: DomainName
        columnName: DestinationHostName
kind: Scheduled
version: 1.0.4
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Let binding: Alert_List

let Alert_List = dynamic([
"Phishing link click observed in Network Traffic",
"Phish delivered due to an IP allow policy",
"A potentially malicious URL click was detected",
"High Risk Sign-in Observed in Network Traffic",
"A user clicked through to a potentially malicious URL",
"Suspicious network connection to AitM phishing site",
"Messages containing malicious entity not removed after delivery",
"Email messages containing malicious URL removed after delivery",
"Email reported by user as malware or phish",
"Phish delivered due to an ETR override",
"Phish not zapped because ZAP is disabled"]);

Stage 1: source

SecurityAlert

Stage 2: where

| where ProviderName in~ ("Office 365 Advanced Threat Protection", "OATP")

Stage 3: where

| where AlertName in~ (Alert_List)

References Alert_List (defined above).

Stage 4: extend

| extend Entities = parse_json(Entities)

Stage 5: kusto:mv-apply

| mv-apply Entity = Entities on
(
where Entity.Type == 'account'
| extend EntityUPN = iff(isempty(Entity.UserPrincipalName), tostring(strcat(Entity.Name, "@", tostring (Entity.UPNSuffix))), tostring(Entity.UserPrincipalName))
)

Stage 6: kusto:mv-apply

| mv-apply Entity = Entities on
(
where Entity.Type == 'url'
| extend EntityUrl = tostring(Entity.Url)
)

Stage 7: summarize

| summarize AccountUpn=tolower(tostring(take_any(EntityUPN))),Url=tostring(tolower(take_any(EntityUrl))),AlertTime= min(TimeGenerated)by SystemAlertId, ProductName

Stage 8: join

| join kind= inner (CommonSecurityLog
| where DeviceVendor has_any  ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
| where DeviceAction != "Block"
| where DeviceProduct startswith "FortiGate" or DeviceProduct startswith  "PAN" or DeviceProduct startswith  "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith  "NSSWeblog" or DeviceProduct startswith "URL"
| where isnotempty(RequestURL)
| where isnotempty(SourceUserName)
| extend SourceUserName = tolower(SourceUserName)
| project
3plogTime=TimeGenerated,
DeviceVendor,
DeviceProduct,
Activity,
DestinationHostName,
DestinationIP,
RequestURL=tostring(tolower(RequestURL)),
MaliciousIP,
Name = tostring(split(SourceUserName,"@")[0]),
UPNSuffix =tostring(split(SourceUserName,"@")[1]),
SourceUserName,
IndicatorThreatType,
ThreatSeverity,AdditionalExtensions,
ThreatConfidence)on $left.Url == $right.RequestURL and $left.AccountUpn == $right.SourceUserName

Stage 9: where

| where AlertTime between ((3plogTime - 1h) .. (3plogTime + 1h))

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
AlertNamein
  • A potentially malicious URL click was detected
  • A user clicked through to a potentially malicious URL
  • Email messages containing malicious URL removed after delivery
  • Email reported by user as malware or phish
  • High Risk Sign-in Observed in Network Traffic
  • Messages containing malicious entity not removed after delivery
  • Phish delivered due to an ETR override
  • Phish delivered due to an IP allow policy
  • Phish not zapped because ZAP is disabled
  • Phishing link click observed in Network Traffic
  • Suspicious network connection to AitM phishing site
DeviceActionne
  • Block transforms: cased
DeviceProductstarts_with
  • FireWall
  • FortiGate
  • NSSWeblog
  • PAN
  • URL
  • VPN
DeviceVendormatch
  • Check Point
  • Fortinet
  • Palo Alto Networks
  • Zscaler
ProviderNamein
  • OATP
  • Office 365 Advanced Threat Protection
RequestURLis_not_null
  • (no value, null check)
SourceUserNameis_not_null
  • (no value, null check)
Typeeq
  • account transforms: cased
  • url transforms: cased

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
AccountUpnsummarize
AlertTimesummarize
ProductNamesummarize
SystemAlertIdsummarize
Urlsummarize