Detection rules › Kusto

A potentially malicious web request was executed against a web server

Status
available
Severity
medium
Time window
1d
Group by
ClientIp, Hostname, SessionBlockedStarted
Source
github.com/Azure/Azure-Sentinel

'Detects unobstructed Web Application Firewall (WAF) activity in sessions where the WAF blocked incoming requests by computing the ratio between blocked requests and unobstructed WAF requests in these sessions (BlockvsSuccessRatio metric). A high ratio value for a given client IP and hostname calls for further investigation of the WAF data in that session, due to the significantly high number of blocked requests and a few unobstructed logs that may be malicious but have passed undetected through the WAF. The successCode variable defines what the detection thinks is a successful status code and should be altered to fit the environment.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1190 Exploit Public-Facing Application

Rule body kusto

id: 46ac55ae-47b8-414a-8f94-89ccd1962178
name: A potentially malicious web request was executed against a web server
description: |
  'Detects unobstructed Web Application Firewall (WAF) activity in sessions where the WAF blocked incoming requests by computing the ratio between blocked requests and unobstructed WAF requests in these sessions (BlockvsSuccessRatio metric).
  A high ratio value for a given client IP and hostname calls for further investigation of the WAF data in that session, due to the significantly high number of blocked requests and a few unobstructed logs that may be malicious but have passed undetected through the WAF. The successCode variable defines what the detection thinks is a successful status code and should be altered to fit the environment.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: WAF
    dataTypes:
      - AzureDiagnostics
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
query: |
  let queryperiod = 1d;
  let mode = dynamic(['Blocked', 'Detected']);
  let successCode = dynamic(['200', '101','204', '400','504','304','401','500']);
  let sessionBin = 30m;
  AGWFirewallLogs
  | where TimeGenerated > ago(queryperiod)
  | where Action  in (mode)
  | sort by Hostname asc, ClientIp asc, TimeGenerated asc
  | extend SessionBlockedStarted = row_window_session(TimeGenerated, queryperiod, 10m, ((ClientIp != prev(ClientIp)) or (Hostname != prev(Hostname))))
  | summarize SessionBlockedEnded = max(TimeGenerated), SessionBlockedCount = count() by Hostname, ClientIp, SessionBlockedStarted
  | extend TimeKey = range(bin(SessionBlockedStarted, sessionBin), bin(SessionBlockedEnded, sessionBin), sessionBin)
  | mv-expand TimeKey to typeof(datetime)
  | join kind = inner(
      AGWAccessLogs
      | where TimeGenerated > ago(queryperiod)
      | where  (isempty(HttpStatus) or HttpStatus in (successCode))
      | extend TimeKey = bin(TimeGenerated, sessionBin)
      | extend Hostname = coalesce(Host,OriginalHost)
      | extend ClientIp = tostring(ClientIp)
  ) on TimeKey, Hostname, ClientIp
  | where TimeGenerated between (SessionBlockedStarted..SessionBlockedEnded)
  | extend
      OriginalRequestUriWithArgs = column_ifexists("OriginalRequestUriWithArgs", ""),
      ServerStatus = column_ifexists("ServerStatus", "")
  | summarize
      SuccessfulAccessCount = count(),
      UserAgents = make_set(UserAgent, 250),
      RequestURIs = make_set(RequestUri, 250),
      OriginalRequestURIs = make_set(OriginalRequestUriWithArgs, 250),
      SuccessCodes = make_set(HttpStatus, 250),
      SuccessCodes_BackendServer = make_set(ServerStatus, 250),
      take_any(SessionBlockedEnded, SessionBlockedCount)
      by Hostname, ClientIp, SessionBlockedStarted 
  | where SessionBlockedCount > SuccessfulAccessCount
  | extend BlockvsSuccessRatio = SessionBlockedCount/toreal(SuccessfulAccessCount)
  | sort by BlockvsSuccessRatio desc, SessionBlockedStarted asc
  | project-reorder SessionBlockedStarted, SessionBlockedEnded, Hostname, ClientIp, SessionBlockedCount, SuccessfulAccessCount, BlockvsSuccessRatio, SuccessCodes, RequestURIs, OriginalRequestURIs, UserAgents
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let queryperiod = 1d;
let mode = dynamic(['Blocked', 'Detected']);
let successCode = dynamic(['200', '101','204', '400','504','304','401','500']);
let sessionBin = 30m;

Stage 1: source

AGWFirewallLogs

Stage 2: where

| where TimeGenerated > ago(queryperiod)

Stage 3: where

| where Action  in (mode)

Stage 4: sort

| sort by Hostname asc, ClientIp asc, TimeGenerated asc

Stage 5: extend

| extend SessionBlockedStarted = row_window_session(TimeGenerated, queryperiod, 10m, ((ClientIp != prev(ClientIp)) or (Hostname != prev(Hostname))))

Stage 6: summarize

| summarize SessionBlockedEnded = max(TimeGenerated), SessionBlockedCount = count() by Hostname, ClientIp, SessionBlockedStarted

Stage 7: extend

| extend TimeKey = range(bin(SessionBlockedStarted, sessionBin), bin(SessionBlockedEnded, sessionBin), sessionBin)

Stage 8: mv-expand

| mv-expand TimeKey to typeof(datetime)

Stage 9: join

| join kind = inner(
    AGWAccessLogs
    | where TimeGenerated > ago(queryperiod)
    | where  (isempty(HttpStatus) or HttpStatus in (successCode))
    | extend TimeKey = bin(TimeGenerated, sessionBin)
    | extend Hostname = coalesce(Host,OriginalHost)
    | extend ClientIp = tostring(ClientIp)
) on TimeKey, Hostname, ClientIp

Stage 10: where

| where TimeGenerated between (SessionBlockedStarted..SessionBlockedEnded)

Stage 11: extend

| extend
    OriginalRequestUriWithArgs = column_ifexists("OriginalRequestUriWithArgs", ""),
    ServerStatus = column_ifexists("ServerStatus", "")

Stage 12: summarize

| summarize
    SuccessfulAccessCount = count(),
    UserAgents = make_set(UserAgent, 250),
    RequestURIs = make_set(RequestUri, 250),
    OriginalRequestURIs = make_set(OriginalRequestUriWithArgs, 250),
    SuccessCodes = make_set(HttpStatus, 250),
    SuccessCodes_BackendServer = make_set(ServerStatus, 250),
    take_any(SessionBlockedEnded, SessionBlockedCount)
    by Hostname, ClientIp, SessionBlockedStarted

Stage 13: where

| where SessionBlockedCount > SuccessfulAccessCount

Stage 14: extend

| extend BlockvsSuccessRatio = SessionBlockedCount/toreal(SuccessfulAccessCount)

Stage 15: sort

| sort by BlockvsSuccessRatio desc, SessionBlockedStarted asc

Stage 16: project-reorder

| project-reorder SessionBlockedStarted, SessionBlockedEnded, Hostname, ClientIp, SessionBlockedCount, SuccessfulAccessCount, BlockvsSuccessRatio, SuccessCodes, RequestURIs, OriginalRequestURIs, UserAgents

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
Actionin
  • Blocked transforms: cased
  • Detected transforms: cased
HttpStatusin
  • 101 transforms: cased
  • 200 transforms: cased
  • 204 transforms: cased
  • 304 transforms: cased
  • 400 transforms: cased
  • 401 transforms: cased
  • 500 transforms: cased
  • 504 transforms: cased
HttpStatusis_null
  • (no value, null check)
SessionBlockedCountgt
  • SuccessfulAccessCount transforms: cased
TimeGeneratedge
  • SessionBlockedStarted
TimeGeneratedle
  • SessionBlockedEnded

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
ClientIpsummarize
Hostnamesummarize
OriginalRequestURIssummarize
RequestURIssummarize
SessionBlockedStartedsummarize
SuccessCodessummarize
SuccessCodes_BackendServersummarize
SuccessfulAccessCountsummarize
UserAgentssummarize
BlockvsSuccessRatioextend