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.'

Query · kql

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
Raw source Potential IIS brute force · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
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_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
version: 1.0.1
metadata:
    source:
        kind: Community
    author:
        name: Shain
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.