Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

Brute force attack against Azure Portal

Back
Id28b42356-45af-40a6-a0b4-a554cdfd5d8a
RulenameBrute force attack against Azure Portal
DescriptionIdentifies evidence of brute force activity against Azure Portal by highlighting multiple authentication failures and by a successful authentication within a given time window.

Default Failure count is 10 and default Time Window is 20 minutes.

References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.
SeverityMedium
TacticsCredentialAccess
TechniquesT1110
Required data connectorsAzureActiveDirectory
KindScheduled
Query frequency1d
Query period1d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure Active Directory/Analytic Rules/SigninBruteForce-AzurePortal.yaml
Version2.1.1
Arm template28b42356-45af-40a6-a0b4-a554cdfd5d8a.json
Deploy To Azure
let timeRange = 24h;
let failureCountThreshold = 10;
let authenticationWindow = 20m;
let aadFunc = (tableName:string){
 table(tableName)
| where AppDisplayName has "Azure Portal"
| extend
     DeviceDetail = todynamic(DeviceDetail),
     //Status = todynamic(Status),
     LocationDetails = todynamic(LocationDetails)
| extend
     OS = tostring(DeviceDetail.operatingSystem),
     Browser = tostring(DeviceDetail.browser),
     //StatusCode = tostring(Status.errorCode),
     //StatusDetails = tostring(Status.additionalDetails),
     State = tostring(LocationDetails.state),
     City = tostring(LocationDetails.city),
     Region = tostring(LocationDetails.countryOrRegion)
// Split out failure versus non-failure types
| extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")  
// sort for sessionizing - by UserPrincipalName and time of the authentication outcome
| sort by UserPrincipalName asc, TimeGenerated asc
// sessionize into failure groupings until either the account changes or there is a success
| extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
// bin outcomes based on authenticationWindow
| summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName,SessionStartedUtc
// count the failures in each session
| summarize FailureCountBeforeSuccess=sumif(FailureOrSuccessCount, FailureOrSuccess == "Failure"), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress,15), make_set(Browser,15), make_set(City,15), make_set(State,15), make_set(Region,15), make_set(ResultType,15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type
// the session must not start with a success, and must end with one
| where array_index_of(list_FailureOrSuccess, "Success") != 0
| where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
| project-away SessionStartedUtc, list_FailureOrSuccess
// where the number of failures before the success is above the threshold 
| where FailureCountBeforeSuccess >= failureCountThreshold 
// expand out ip for entity assignment
| mv-expand IPAddress
| extend IPAddress = tostring(IPAddress)
| extend timestamp = StartTime 
};
 let aadSignin = aadFunc("SigninLogs");
 let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
 union isfuzzy=true aadSignin, aadNonInt
 | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
severity: Medium
queryFrequency: 1d
relevantTechniques:
- T1110
tactics:
- CredentialAccess
kind: Scheduled
query: |
  let timeRange = 24h;
  let failureCountThreshold = 10;
  let authenticationWindow = 20m;
  let aadFunc = (tableName:string){
   table(tableName)
  | where AppDisplayName has "Azure Portal"
  | extend
       DeviceDetail = todynamic(DeviceDetail),
       //Status = todynamic(Status),
       LocationDetails = todynamic(LocationDetails)
  | extend
       OS = tostring(DeviceDetail.operatingSystem),
       Browser = tostring(DeviceDetail.browser),
       //StatusCode = tostring(Status.errorCode),
       //StatusDetails = tostring(Status.additionalDetails),
       State = tostring(LocationDetails.state),
       City = tostring(LocationDetails.city),
       Region = tostring(LocationDetails.countryOrRegion)
  // Split out failure versus non-failure types
  | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")  
  // sort for sessionizing - by UserPrincipalName and time of the authentication outcome
  | sort by UserPrincipalName asc, TimeGenerated asc
  // sessionize into failure groupings until either the account changes or there is a success
  | extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
  // bin outcomes based on authenticationWindow
  | summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName,SessionStartedUtc
  // count the failures in each session
  | summarize FailureCountBeforeSuccess=sumif(FailureOrSuccessCount, FailureOrSuccess == "Failure"), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress,15), make_set(Browser,15), make_set(City,15), make_set(State,15), make_set(Region,15), make_set(ResultType,15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type
  // the session must not start with a success, and must end with one
  | where array_index_of(list_FailureOrSuccess, "Success") != 0
  | where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
  | project-away SessionStartedUtc, list_FailureOrSuccess
  // where the number of failures before the success is above the threshold 
  | where FailureCountBeforeSuccess >= failureCountThreshold 
  // expand out ip for entity assignment
  | mv-expand IPAddress
  | extend IPAddress = tostring(IPAddress)
  | extend timestamp = StartTime 
  };
   let aadSignin = aadFunc("SigninLogs");
   let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
   union isfuzzy=true aadSignin, aadNonInt
   | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])  
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure Active Directory/Analytic Rules/SigninBruteForce-AzurePortal.yaml
queryPeriod: 1d
status: Available
version: 2.1.1
name: Brute force attack against Azure Portal
requiredDataConnectors:
- dataTypes:
  - SigninLogs
  connectorId: AzureActiveDirectory
- dataTypes:
  - AADNonInteractiveUserSignInLogs
  connectorId: AzureActiveDirectory
triggerOperator: gt
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: Name
  - identifier: UPNSuffix
    columnName: UPNSuffix
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IPAddress
id: 28b42356-45af-40a6-a0b4-a554cdfd5d8a
description: |
  'Identifies evidence of brute force activity against Azure Portal by highlighting multiple authentication failures and by a successful authentication within a given time window. 
  Default Failure count is 10 and default Time Window is 20 minutes.
  References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.'  
triggerThreshold: 0
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/28b42356-45af-40a6-a0b4-a554cdfd5d8a')]",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/28b42356-45af-40a6-a0b4-a554cdfd5d8a')]",
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules",
      "kind": "Scheduled",
      "apiVersion": "2022-11-01-preview",
      "properties": {
        "displayName": "Brute force attack against Azure Portal",
        "description": "'Identifies evidence of brute force activity against Azure Portal by highlighting multiple authentication failures and by a successful authentication within a given time window. \nDefault Failure count is 10 and default Time Window is 20 minutes.\nReferences: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.'\n",
        "severity": "Medium",
        "enabled": true,
        "query": "let timeRange = 24h;\nlet failureCountThreshold = 10;\nlet authenticationWindow = 20m;\nlet aadFunc = (tableName:string){\n table(tableName)\n| where AppDisplayName has \"Azure Portal\"\n| extend\n     DeviceDetail = todynamic(DeviceDetail),\n     //Status = todynamic(Status),\n     LocationDetails = todynamic(LocationDetails)\n| extend\n     OS = tostring(DeviceDetail.operatingSystem),\n     Browser = tostring(DeviceDetail.browser),\n     //StatusCode = tostring(Status.errorCode),\n     //StatusDetails = tostring(Status.additionalDetails),\n     State = tostring(LocationDetails.state),\n     City = tostring(LocationDetails.city),\n     Region = tostring(LocationDetails.countryOrRegion)\n// Split out failure versus non-failure types\n| extend FailureOrSuccess = iff(ResultType in (\"0\", \"50125\", \"50140\", \"70043\", \"70044\"), \"Success\", \"Failure\")  \n// sort for sessionizing - by UserPrincipalName and time of the authentication outcome\n| sort by UserPrincipalName asc, TimeGenerated asc\n// sessionize into failure groupings until either the account changes or there is a success\n| extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == \"Success\")\n// bin outcomes based on authenticationWindow\n| summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName,SessionStartedUtc\n// count the failures in each session\n| summarize FailureCountBeforeSuccess=sumif(FailureOrSuccessCount, FailureOrSuccess == \"Failure\"), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress,15), make_set(Browser,15), make_set(City,15), make_set(State,15), make_set(Region,15), make_set(ResultType,15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type\n// the session must not start with a success, and must end with one\n| where array_index_of(list_FailureOrSuccess, \"Success\") != 0\n| where array_index_of(list_FailureOrSuccess, \"Success\") == array_length(list_FailureOrSuccess) - 1\n| project-away SessionStartedUtc, list_FailureOrSuccess\n// where the number of failures before the success is above the threshold \n| where FailureCountBeforeSuccess >= failureCountThreshold \n// expand out ip for entity assignment\n| mv-expand IPAddress\n| extend IPAddress = tostring(IPAddress)\n| extend timestamp = StartTime \n};\n let aadSignin = aadFunc(\"SigninLogs\");\n let aadNonInt = aadFunc(\"AADNonInteractiveUserSignInLogs\");\n union isfuzzy=true aadSignin, aadNonInt\n | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])\n",
        "queryFrequency": "P1D",
        "queryPeriod": "P1D",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0,
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CredentialAccess"
        ],
        "techniques": [
          "T1110"
        ],
        "alertRuleTemplateName": "28b42356-45af-40a6-a0b4-a554cdfd5d8a",
        "customDetails": null,
        "entityMappings": [
          {
            "fieldMappings": [
              {
                "columnName": "Name",
                "identifier": "Name"
              },
              {
                "columnName": "UPNSuffix",
                "identifier": "UPNSuffix"
              }
            ],
            "entityType": "Account"
          },
          {
            "fieldMappings": [
              {
                "columnName": "IPAddress",
                "identifier": "Address"
              }
            ],
            "entityType": "IP"
          }
        ],
        "templateVersion": "2.1.1",
        "status": "Available",
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure Active Directory/Analytic Rules/SigninBruteForce-AzurePortal.yaml"
      }
    }
  ]
}