Detection rules › Kusto

Request for single resource on domain

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

'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'

MITRE ATT&CK coverage

TacticTechniques
Command & ControlT1071 Application Layer Protocol, T1102 Web Service

Rule body kusto

id: 4d500e6d-c984-43a3-9f39-7edec8dcc04d
name: Request for single resource on domain
description: |
  'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1102
  - T1071
query: |
  let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
  //The number of URI's seen to be suspicious, higher = less likely to be suspicious
  let uriThreshold = 1;
  CommonSecurityLog
  // Only look at connections that were allowed through the web proxy
  | where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"
  // Only look where some data was exchanged.
  | where SentBytes > 0 and ReceivedBytes > 0
  // Extract the Domain
  | extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
  | extend GetData=iff(RequestURL == "?", 1, 0)
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain
  // Determine the number of URIs that have been visited for the domain
  | extend destinationURI = arraylength(list_RequestURL)
  | where destinationURI <= uriThreshold
  | where tostring(list_RequestURL) has_any(scriptExtensions)
  //Remove matches with referer
  | where max_RequestContext == ""
  //Keep requests where data was trasferred either in a GET with parameters or a POST
  | where set_RequestMethod in~ ("POST") or max_GetData == 1
  //Defeat email click tracking, may increase FN's while decreasing FP's
  | where list_RequestURL !has "click" and set_RequestMethod !has "GET"
  | mvexpand list_RequestURL, list_DestinationIP
  | extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)
  //Extend custom entitites for incidents
  | project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
let uriThreshold = 1;

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"

Stage 3: where

| where SentBytes > 0 and ReceivedBytes > 0

Stage 4: extend

| extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
Domain =
if/* macro: (countof(DestinationHostName, '.') >= 2) */strcat(split(DestinationHostName, '.')[(- 2)], '.', split(DestinationHostName, '.')[(- 1)])
elseDestinationHostName

Stage 5: extend

| extend GetData=iff(RequestURL == "?", 1, 0)
GetData =
ifRequestURL matches regex "."1
else0

Stage 6: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain

Stage 7: extend

| extend destinationURI = arraylength(list_RequestURL)

Stage 8: where

| where destinationURI <= uriThreshold

Stage 9: where

| where tostring(list_RequestURL) has_any(scriptExtensions)

Stage 10: where

| where max_RequestContext == ""

Stage 11: where

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

Stage 12: where

| where list_RequestURL !has "click" and set_RequestMethod !has "GET"

Stage 13: mv-expand

| mvexpand list_RequestURL, list_DestinationIP

Stage 14: extend

| extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)

Stage 15: project-away

| project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext

Exclusions

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

FieldKindExcluded values
list_RequestURLmatchclick
set_RequestMethodmatchGET

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
DeviceActioneq
  • Allowed
DeviceVendoreq
  • Zscaler
ReceivedBytesgt
  • 0 transforms: cased
SentBytesgt
  • 0 transforms: cased
destinationURIle
  • 1 transforms: cased
list_RequestURLmatch
  • .asp transforms: tostring
  • .aspx transforms: tostring
  • .cfml transforms: tostring
  • .php transforms: tostring
max_GetDataeq
  • 1 transforms: cased
set_RequestMethodin
  • POST

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
numOfConnectionssummarize
ClientIPextend
DestinationIPextend
RequestURLextend