Skip to content

Commit

Permalink
Merge pull request #9176 from praveenthepro/patch-3
Browse files Browse the repository at this point in the history
Update SigninBruteForce-AzurePortal.yaml
  • Loading branch information
petebryan authored Oct 10, 2023
2 parents d527754 + 1f3a25b commit a4dad22
Showing 1 changed file with 62 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
id: 28b42356-45af-40a6-a0b4-a554cdfd5d8a
name: 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.'
description: >
Detects Azure Portal brute force attacks by monitoring for multiple authentication failures and a successful login within a 20-minute window. Default settings: 10 failures, 25 deviations.
Ref: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
Expand All @@ -13,7 +12,7 @@ requiredDataConnectors:
dataTypes:
- AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
Expand All @@ -22,48 +21,64 @@ tactics:
relevantTechniques:
- T1110
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
};
// Set threshold value for deviation
let threshold = 25;
// Set the time range for the query
let timeRange = 24h;
// Set the authentication window duration
let authenticationWindow = 20m;
// Define a reusable function 'aadFunc' that takes a table name as input
let aadFunc = (tableName: string) {
// Query the specified table
table(tableName)
// Filter data within the last 24 hours
| where TimeGenerated > ago(1d)
// Filter records related to "Azure Portal" applications
| where AppDisplayName has "Azure Portal"
// Extract and transform some fields
| extend
DeviceDetail = todynamic(DeviceDetail),
LocationDetails = todynamic(LocationDetails)
| extend
OS = tostring(DeviceDetail.operatingSystem),
Browser = tostring(DeviceDetail.browser),
State = tostring(LocationDetails.state),
City = tostring(LocationDetails.city),
Region = tostring(LocationDetails.countryOrRegion)
// Categorize records as Success or Failure based on ResultType
| extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
// Sort and identify sessions
| sort by UserPrincipalName asc, TimeGenerated asc
| extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
// Summarize data
| summarize FailureOrSuccessCount = count() by FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName, SessionStartedUtc
| 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
// Filter records where "Success" occurs in the middle of a session
| where array_index_of(list_FailureOrSuccess, "Success") != 0
| where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
// Remove unnecessary columns from the output
| project-away SessionStartedUtc, list_FailureOrSuccess
// Join with another table and calculate deviation
| join kind=inner (
table(tableName)
| where TimeGenerated > ago(7d)
| where AppDisplayName has "Azure Portal"
| extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
| summarize avgFailures = avg(todouble(FailureOrSuccess == "Failure")) by UserPrincipalName
) on UserPrincipalName
| extend Deviation = abs(FailureCountBeforeSuccess - avgFailures) / avgFailures
// Filter records based on deviation and failure count criteria
| where Deviation > threshold and FailureCountBeforeSuccess >= 10
// Expand the IPAddress array
| mv-expand IPAddress
| extend IPAddress = tostring(IPAddress)
| extend timestamp = StartTime
};
// Call 'aadFunc' with different table names and union the results
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
// Additional transformation: Split UserPrincipalName
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
Expand All @@ -72,9 +87,11 @@ entityMappings:
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- identifier: AadUserId
columnName: UserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 2.1.1
version: 2.1.2
kind: Scheduled

0 comments on commit a4dad22

Please sign in to comment.