Detection rules › Kusto

High count of failed attempts from same client IP

Severity
medium
Time window
1m
Group by
Computer, cIP, sIP
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'Identifies when 20 or more failed attempts from a given client IP in 1 minute occur on the IIS server. This could be indicative of an attempted brute force. This could also simply indicate a misconfigured service or device. Recommendations: Validate that these are expected connections from the given Client IP. If the client IP is not recognized, potentially block these connections at the edge device. If these are expected connections, verify the credentials are properly configured on the system, service, application or device that is associated with the client IP. References: IIS status code mapping: https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0 Win32 Status code mapping: https://msdn.microsoft.com/library/cc231199.aspx'

MITRE ATT&CK coverage

TacticTechniques
Credential AccessT1110 Brute Force

Rule body kusto

id: 19e01883-15d8-4eb6-a7a5-3276cd668388
name: High count of failed attempts from same client IP
description: |
  'Identifies when 20 or more failed attempts from a given client IP in 1 minute occur on the IIS server.
  This could be indicative of an attempted brute force. This could also simply indicate a misconfigured service or device.
  Recommendations: Validate that these are expected connections from the given Client IP.  If the client IP is not recognized, potentially block these connections at the edge device.
  If these are expected connections, verify the credentials are properly configured on the system, service, application or device that is associated with the client IP.
  References:
  IIS status code mapping: https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0
  Win32 Status code mapping: https://msdn.microsoft.com/library/cc231199.aspx'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let timeBin = 1m;
  let failedThreshold = 20;
  W3CIISLog
  | where scStatus in ("401","403")
  | where csUserName != "-"
  | extend scStatusFull = strcat(scStatus, ".",scSubStatus)
  // Map common IIS codes
  | extend scStatusFull_Friendly = case(
  scStatusFull == "401.0", "Access denied.",
  scStatusFull == "401.1", "Logon failed.",
  scStatusFull == "401.2", "Logon failed due to server configuration.",
  scStatusFull == "401.3", "Unauthorized due to ACL on resource.",
  scStatusFull == "401.4", "Authorization failed by filter.",
  scStatusFull == "401.5", "Authorization failed by ISAPI/CGI application.",
  scStatusFull == "403.0", "Forbidden.",
  scStatusFull == "403.4", "SSL required.",
  "See - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0")
  // Mapping to Hex so can be mapped using website in comments above
  | extend scWin32Status_Hex = tohex(tolong(scWin32Status))
  // Map common win32 codes
  | extend scWin32Status_Friendly = case(
  scWin32Status_Hex =~ "775", "The referenced account is currently locked out and cannot be logged on to.",
  scWin32Status_Hex =~ "52e", "Logon failure: Unknown user name or bad password.",
  scWin32Status_Hex =~ "532", "Logon failure: The specified account password has expired.",
  scWin32Status_Hex =~ "533", "Logon failure: Account currently disabled.",
  scWin32Status_Hex =~ "2ee2", "The request has timed out.",
  scWin32Status_Hex =~ "0", "The operation completed successfully.",
  scWin32Status_Hex =~ "1", "Incorrect function.",
  scWin32Status_Hex =~ "2", "The system cannot find the file specified.",
  scWin32Status_Hex =~ "3", "The system cannot find the path specified.",
  scWin32Status_Hex =~ "4", "The system cannot open the file.",
  scWin32Status_Hex =~ "5", "Access is denied.",
  scWin32Status_Hex =~ "8009030e", "SEC_E_NO_CREDENTIALS",
  scWin32Status_Hex =~ "8009030C", "SEC_E_LOGON_DENIED",
  "See - https://msdn.microsoft.com/library/cc231199.aspx")
  // decode URI when available
  | extend decodedUriQuery = url_decode(csUriQuery)
  // Count of failed attempts from same client IP
  | summarize makeset(decodedUriQuery), makeset(csUserName), makeset(sSiteName), makeset(sPort), makeset(csUserAgent), makeset(csMethod), makeset(csUriQuery), makeset(scStatusFull), makeset(scStatusFull_Friendly), makeset(scWin32Status_Hex), makeset(scWin32Status_Friendly), FailedConnectionsCount = count() by bin(TimeGenerated, timeBin), cIP, Computer, sIP
  | where FailedConnectionsCount >= failedThreshold
  | project TimeGenerated, cIP, set_csUserName, set_decodedUriQuery, Computer, set_sSiteName, sIP, set_sPort, set_csUserAgent, set_csMethod, set_scStatusFull, set_scStatusFull_Friendly, set_scWin32Status_Hex, set_scWin32Status_Friendly, FailedConnectionsCount
  | order by FailedConnectionsCount
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: cIP
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Network", "Identity" ]

Stages and Predicates

Parameters

let timeBin = 1m;
let failedThreshold = 20;

Stage 1: source

W3CIISLog

Stage 2: where

| where scStatus in ("401","403")

Stage 3: where

| where csUserName != "-"

Stage 4: extend (5 consecutive steps)

| extend scStatusFull = strcat(scStatus, ".",scSubStatus)
| extend scStatusFull_Friendly = case(
scStatusFull == "401.0", "Access denied.",
scStatusFull == "401.1", "Logon failed.",
scStatusFull == "401.2", "Logon failed due to server configuration.",
scStatusFull == "401.3", "Unauthorized due to ACL on resource.",
scStatusFull == "401.4", "Authorization failed by filter.",
scStatusFull == "401.5", "Authorization failed by ISAPI/CGI application.",
scStatusFull == "403.0", "Forbidden.",
scStatusFull == "403.4", "SSL required.",
"See - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0")
| extend scWin32Status_Hex = tohex(tolong(scWin32Status))
| extend scWin32Status_Friendly = case(
scWin32Status_Hex =~ "775", "The referenced account is currently locked out and cannot be logged on to.",
scWin32Status_Hex =~ "52e", "Logon failure: Unknown user name or bad password.",
scWin32Status_Hex =~ "532", "Logon failure: The specified account password has expired.",
scWin32Status_Hex =~ "533", "Logon failure: Account currently disabled.",
scWin32Status_Hex =~ "2ee2", "The request has timed out.",
scWin32Status_Hex =~ "0", "The operation completed successfully.",
scWin32Status_Hex =~ "1", "Incorrect function.",
scWin32Status_Hex =~ "2", "The system cannot find the file specified.",
scWin32Status_Hex =~ "3", "The system cannot find the path specified.",
scWin32Status_Hex =~ "4", "The system cannot open the file.",
scWin32Status_Hex =~ "5", "Access is denied.",
scWin32Status_Hex =~ "8009030e", "SEC_E_NO_CREDENTIALS",
scWin32Status_Hex =~ "8009030C", "SEC_E_LOGON_DENIED",
"See - https://msdn.microsoft.com/library/cc231199.aspx")
| extend decodedUriQuery = url_decode(csUriQuery)

Stage 5: summarize

| summarize makeset(decodedUriQuery), makeset(csUserName), makeset(sSiteName), makeset(sPort), makeset(csUserAgent), makeset(csMethod), makeset(csUriQuery), makeset(scStatusFull), makeset(scStatusFull_Friendly), makeset(scWin32Status_Hex), makeset(scWin32Status_Friendly), FailedConnectionsCount = count() by bin(TimeGenerated, timeBin), cIP, Computer, sIP
Threshold
ge 20

Stage 6: where

| where FailedConnectionsCount >= failedThreshold

Stage 7: project

| project TimeGenerated, cIP, set_csUserName, set_decodedUriQuery, Computer, set_sSiteName, sIP, set_sPort, set_csUserAgent, set_csMethod, set_scStatusFull, set_scStatusFull_Friendly, set_scWin32Status_Hex, set_scWin32Status_Friendly, FailedConnectionsCount

Stage 8: sort

| order by FailedConnectionsCount

Stage 9: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 10: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
HostNameDomain =
ifDomainIndex != -1substring(Computer, (DomainIndex + 1))
elseComputer

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
FailedConnectionsCountge
  • 20 transforms: cased
csUserNamene
  • - transforms: cased
scStatusin
  • 401 transforms: cased
  • 403 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
Computerproject
FailedConnectionsCountproject
TimeGeneratedproject
cIPproject
sIPproject
set_csMethodproject
set_csUserAgentproject
set_csUserNameproject
set_decodedUriQueryproject
set_sPortproject
set_sSiteNameproject
set_scStatusFullproject
set_scStatusFull_Friendlyproject
set_scWin32Status_Friendlyproject
set_scWin32Status_Hexproject
DomainIndexextend
HostNameextend
HostNameDomainextend