← Back to SOC feed Coverage →

Potential IIS brute force

kql MEDIUM Azure-Sentinel
T1110
W3CIISLog
huntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-06-04T23:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may be using brute force techniques to gain unauthorized access to an IIS server by overwhelming it with failed login attempts before succeeding. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify and mitigate potential credential compromise and lateral movement attempts.

KQL Query


W3CIISLog
| where scStatus in ("401","403")
| where cIP !startswith "192.168." and cIP != sIP and cIP != "::1" //and csUserName != "-" 
// Handling Exchange specific items in IIS logs to remove the unique log identifier in the URI
| extend csUriQuery = iff(csUriQuery startswith "MailboxId=", tostring(split(csUriQuery, "&")[0]) , csUriQuery )
| extend csUriQuery = iff(csUriQuery startswith "X-ARR-CACHE-HIT=", strcat(tostring(split(csUriQuery, "&")[0]),tostring(split(csUriQuery, "&")[1])) , csUriQuery )
| summarize FailStartTime = min(TimeGenerated), FailEndTime = max(TimeGenerated), makeset(sPort), makeset(csUserAgent), makeset(csUserName), csUserNameCount = dcount(csUserName), ConnectionCount = count() by Computer, sSiteName, sIP, cIP, csUriQuery, csMethod, scStatus, scSubStatus, scWin32Status
| extend csUserAgentPerIPCount = arraylength(set_csUserAgent)
| extend sPortCount = arraylength(set_sPort)
| 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 =~ "52e", "Logon failure: Unknown user name or bad password.", 
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)
| where (ConnectionCount >= 1200 and csUserAgentPerIPCount > 1) or (ConnectionCount >= 1200 and sPortCount > 1)
// now join back to see if there is a successful logon after so many failures
| join (
W3CIISLog
| where scStatus startswith "20"
| where cIP !startswith "192.168." and cIP != sIP and cIP != "::1"
| extend LogonSuccessTime = TimeGenerated, Success_scStatus = scStatus
| distinct LogonSuccessTime, Computer, sSiteName, sIP, cIP, Success_scStatus
) on Computer, sSiteName, sIP, cIP
| where FailEndTime < LogonSuccessTime and not(LogonSuccessTime between (FailStartTime .. FailEndTime))
| summarize makeset(LogonSuccessTime) by FailStartTime, FailEndTime, Computer, sSiteName, sIP, cIP, tostring(set_csUserName), csUserNameCount, csUriQuery, csMethod, scStatus, scSubStatus, scWin32Status, tostring(set_sPort), tostring(set_csUserAgent), ConnectionCount, csUserAgentPerIPCount, sPortCount, scStatusFull, scStatusFull_Friendly, scWin32Status_Hex, scWin32Status_Friendly
| project FailStartTime, FailEndTime, set_LogonSuccessTime, Computer, sSiteName, sIP, cIP, set_csUserName, csUserNameCount, csUriQuery, csMethod, scStatus, scSubStatus, scWin32Status, set_sPort, set_csUserAgent, ConnectionCount, csUserAgentPerIPCount, sPortCount, scStatusFull, scStatusFull_Friendly, scWin32Status_Hex, scWin32Status_Friendly
| extend timestamp = FailStartTime, IPCustomEntity = cIP, HostCustomEntity = Computer

Analytic Rule Definition

id: 934011da-1fe6-4507-aadb-d3914c877bcd
name: Potential IIS brute force
description: |
  'Query shows 1200+ failed attempts by cIP per hour on server, then successful logon. Only includes > 1 user agent string or port. Could indicate successful probing and brute force success on IIS servers.'
description_detailed: |
  'This query shows when 1200 (20 per minute) or more failed attempts by cIP per hour occur on a given server and then a successful logon by cIP. 
  This only includes when more than 1 user agent strings is used or more than 1 port is used.
  This could be indicative of successful probing and password brute force success on your IIS servers. 
  Feel free to adjust the threshold as needed - ConnectionCount >= 1200 
  References: Status code mappings for your convenience, also inline if the mapping is not available
  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'
requiredDataConnectors:
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |

  W3CIISLog
  | where scStatus in ("401","403")
  | where cIP !startswith "192.168." and cIP != sIP and cIP != "::1" //and csUserName != "-" 
  // Handling Exchange specific items in IIS logs to remove the unique log identifier in the URI
  | extend csUriQuery = iff(csUriQuery startswith "MailboxId=", tostring(split(csUriQuery, "&")[0]) , csUriQuery )
  | extend csUriQuery = iff(csUriQuery startswith "X-ARR-CACHE-HIT=", strcat(tostring(split(csUriQuery, "&")[0]),tostring(split(csUriQuery, "&")[1])) , csUriQuery )
  | summarize FailStartTime = min(TimeGenerated), FailEndTime = max(TimeGenerated), makeset(sPort), makeset(csUserAgent), makeset(csUserName), csUserNameCount = dcount(csUserName), ConnectionCount = count() by Computer, sSiteName, sIP, cIP, csUriQuery, csMethod, scStatus, scSubStatus, scWin32Status
  | extend csUserAgentPerIPCount = arraylength(set_csUserAgent)
  | extend sPortCount = arraylength(set_sPort)
  | 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_

Required Data Sources

Sentinel TableNotes
W3CIISLogEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/W3CIISLog/Potential_IIS_BF.yaml