Detection rules › Kusto

Detect requests for an uncommon resources on the web (ASIM Web Session)

Status
available
Severity
low
Time window
1d
Group by
Domain
Source
github.com/Azure/Azure-Sentinel

'This detection mechanism examines connections made to a domain where only a single file is requested, which is considered unusual since most contemporary web applications require additional resources. Such activity is often associated with malware beaconing or tracking URLs delivered via emails. The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'

MITRE ATT&CK coverage

TacticTechniques
Command & ControlT1071 Application Layer Protocol, T1102 Web Service

Rule body kusto

id: c99cf650-c53b-4c4c-9671-7d7500191a10
name: Detect requests for an uncommon resources on the web (ASIM Web Session)
description: |
  'This detection mechanism examines connections made to a domain where only a single file is requested, which is considered unusual since most contemporary web applications require additional resources. Such activity is often associated with malware beaconing or tracking URLs delivered via emails. 
  The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'
severity: Low
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
  - RuleReference: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Zscaler%20Internet%20Access/Analytic%20Rules/Zscaler-LowVolumeDomainRequests.yaml
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1102
  - T1071
query: |
  let lookback = 1d;
  let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml", ".py", ".sh", ".bash", ".pl"]);
  //The number of URI's seen to be suspicious, higher = less likely to be suspicious
  let uriThreshold = 1;
  let ReqestCountThreshold = 10; // set minimum requests count to confirm repetitive connections
  // Only look at connections that were allowed through the web proxy
  _Im_WebSession (starttime=ago(lookback), eventresult='Success')
  | project
      SrcBytes,
      DstBytes,
      Url,
      TimeGenerated,
      DstIpAddr,
      SrcIpAddr,
      HttpRequestMethod,
      HttpReferrer
  | where not(ipv4_is_private(DstIpAddr)) // Only take traffic going to internet
  | extend DestHostName = tostring(parse_url(Url)["Host"])
  // Only look at connections where some data was exchanged.
  | where SrcBytes > 0 and DstBytes > 0
  // Extract Domain
  | extend Domain = iif(countof(DestHostName, '.') >= 2, strcat(split(DestHostName, '.')[-2], '.', split(DestHostName, '.')[-1]), DestHostName)
  | extend GetData = iff(Url contains "?", 1, 0)
  | summarize
      EventStartTime = min(TimeGenerated),
      EventEndTime = max(TimeGenerated),
      make_set(Url, 100),
      make_set(DstIpAddr, 100),
      make_set(SrcIpAddr, 100),
      EventCount = count(),
      make_set(HttpRequestMethod, 10),
      max(GetData),
      max(HttpReferrer)
      by Domain
  // Determine the number of URIs that have been visited for the domain
  | extend destinationURICount = array_length(set_Url)
  | where destinationURICount <= uriThreshold and EventCount > ReqestCountThreshold // check for repetitive requests for single resource only.
  | where tostring(set_Url) has_any(scriptExtensions)
  //Remove matches with referer
  | where max_HttpReferrer == ""
  //Keep requests where data was transferred either in a GET with parameters or a POST
  | where set_HttpRequestMethod in~ ("POST") or max_GetData == 1
  //Defeat email click tracking, may increase FN's while decreasing FP's
  | where set_Url !has "click" and set_HttpRequestMethod !has "GET"
  | mv-expand set_Url, set_DstIpAddr, set_SrcIpAddr
  | extend
      RequestURL = tostring(set_Url),
      DestinationIP = tostring(set_DstIpAddr),
      SourceIP = tostring(set_SrcIpAddr)
  | project
      EventStartTime,
      EventEndTime,
      SourceIP,
      DestinationIP,
      EventCount,
      RequestURL,
      set_HttpRequestMethod
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RequestURL
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
  EventCount: EventCount
alertDetailsOverride:
  alertDisplayNameFormat: "User with IP '{{SourceIP}}' has been observed making request for a rare resource"
  alertDescriptionFormat: "User requested (TotalEvents='{{EventCount}}') for URL '{{RequestURL}}' which contains a known script extension. The domain associated with this URL has not been accessed by any other user. This activity could be a potential beaconing activity to maintain control over compromised systems, receive instructions, or exfiltrate data"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 1d;
let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml", ".py", ".sh", ".bash", ".pl"]);
let uriThreshold = 1;
let ReqestCountThreshold = 10;

Stage 1: source

_Im_WebSession (starttime=ago(lookback), eventresult='Success')

Stage 2: project

| project
    SrcBytes,
    DstBytes,
    Url,
    TimeGenerated,
    DstIpAddr,
    SrcIpAddr,
    HttpRequestMethod,
    HttpReferrer

Stage 3: where

| where not(ipv4_is_private(DstIpAddr))

Stage 4: extend

| extend DestHostName = tostring(parse_url(Url)["Host"])

Stage 5: where

| where SrcBytes > 0 and DstBytes > 0

Stage 6: extend

| extend Domain = iif(countof(DestHostName, '.') >= 2, strcat(split(DestHostName, '.')[-2], '.', split(DestHostName, '.')[-1]), DestHostName)

Stage 7: extend

| extend GetData = iff(Url contains "?", 1, 0)
GetData =
ifUrl contains "?"1
else0

Stage 8: summarize

| summarize
    EventStartTime = min(TimeGenerated),
    EventEndTime = max(TimeGenerated),
    make_set(Url, 100),
    make_set(DstIpAddr, 100),
    make_set(SrcIpAddr, 100),
    EventCount = count(),
    make_set(HttpRequestMethod, 10),
    max(GetData),
    max(HttpReferrer)
    by Domain
Threshold
gt 10

Stage 9: extend

| extend destinationURICount = array_length(set_Url)

Stage 10: where

| where destinationURICount <= uriThreshold and EventCount > ReqestCountThreshold

Stage 11: where

| where tostring(set_Url) has_any(scriptExtensions)

Stage 12: where

| where max_HttpReferrer == ""

Stage 13: where

| where set_HttpRequestMethod in~ ("POST") or max_GetData == 1

Stage 14: where

| where set_Url !has "click" and set_HttpRequestMethod !has "GET"

Stage 15: mv-expand

| mv-expand set_Url, set_DstIpAddr, set_SrcIpAddr

Stage 16: extend

| extend
    RequestURL = tostring(set_Url),
    DestinationIP = tostring(set_DstIpAddr),
    SourceIP = tostring(set_SrcIpAddr)

Stage 17: project

| project
    EventStartTime,
    EventEndTime,
    SourceIP,
    DestinationIP,
    EventCount,
    RequestURL,
    set_HttpRequestMethod

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
DstIpAddrcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8
set_HttpRequestMethodmatchGET
set_Urlmatchclick

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
DstBytesgt
  • 0 transforms: cased
EventCountgt
  • 10 transforms: cased
SrcBytesgt
  • 0 transforms: cased
destinationURICountle
  • 1 transforms: cased
max_GetDataeq
  • 1 transforms: cased
set_HttpRequestMethodin
  • POST
set_Urlmatch
  • .asp transforms: tostring
  • .aspx transforms: tostring
  • .bash transforms: tostring
  • .cfml transforms: tostring
  • .php transforms: tostring
  • .pl transforms: tostring
  • .py transforms: tostring
  • .sh transforms: tostring

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
DestinationIPproject
EventCountproject
EventEndTimeproject
EventStartTimeproject
RequestURLproject
SourceIPproject
set_HttpRequestMethodproject