Brute force attack against Azure Portal
Id | 28b42356-45af-40a6-a0b4-a554cdfd5d8a |
Rulename | 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. 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. |
Severity | Medium |
Tactics | CredentialAccess |
Techniques | T1110 |
Required data connectors | AzureActiveDirectory |
Kind | Scheduled |
Query frequency | 1d |
Query period | 1d |
Trigger threshold | 0 |
Trigger operator | gt |
Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure Active Directory/Analytic Rules/SigninBruteForce-AzurePortal.yaml |
Version | 2.1.1 |
Arm template | 28b42356-45af-40a6-a0b4-a554cdfd5d8a.json |
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"
}
}
]
}