/
SigninPasswordSpray.yaml
71 lines (70 loc) · 4.71 KB
/
SigninPasswordSpray.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
id: 48607a29-a26a-4abf-8078-a06dbdd174a4
name: Password spray attack against Azure AD application
description: |
'Identifies evidence of password spray activity against Azure AD applications by looking for failures from multiple accounts from the same
IP address within a time window. If the number of accounts breaches the threshold just once, all failures from the IP address within the time range
are bought into the result. Details on whether there were successful authentications by the IP address within the time window are also included.
This can be an indicator that an attack was successful.
The default failure acccount threshold is 5, Default time window for failures is 20m and default look back window is 3 days
Note: Due to the number of possible accounts involved in a password spray it is not possible to map identities to a custom entity.
References: https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let timeRange = 3d;
let lookBack = 7d;
let authenticationWindow = 20m;
let authenticationThreshold = 5;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
// Lookup up resolved identities from last 7 days
let identityLookup = SigninLogs
| where TimeGenerated >= ago(lookBack)
| where not(Identity matches regex isGUID)
| where isnotempty(UserId)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName;
// collect window threshold breaches
SigninLogs
| where TimeGenerated > ago(timeRange)
| where ResultType in (50126, 50053) // invalid password, account is locked - too many sign ins
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), make_set(ClientAppUsed), count() by bin(TimeGenerated, authenticationWindow), IPAddress, AppDisplayName, UserPrincipalName
| summarize FailedPrincipalCount = dcount(UserPrincipalName) by bin(TimeGenerated, authenticationWindow), IPAddress, AppDisplayName
| where FailedPrincipalCount >= authenticationThreshold
| summarize WindowThresholdBreaches = count() by IPAddress
| join kind= inner (
// where we breached a threshold, join the details back on all failure data
SigninLogs
| where TimeGenerated > ago(timeRange)
| where ResultType in (50126, 50053)
| extend FullLocation = strcat(Location,'|', LocationDetails.state, '|', LocationDetails.city)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), make_set(ClientAppUsed), make_set(FullLocation), FailureCount = count() by IPAddress, AppDisplayName, UserPrincipalName, UserDisplayName, Identity, UserId
// lookup any unresolved identities
| extend UnresolvedUserId = iff(Identity matches regex isGUID, UserId, "")
| join kind= leftouter (
identityLookup
) on $left.UnresolvedUserId==$right.UserId
| extend UserDisplayName=iff(isempty(lu_UserDisplayName), UserDisplayName, lu_UserDisplayName)
| extend UserPrincipalName=iff(isempty(lu_UserPrincipalName), UserPrincipalName, lu_UserPrincipalName)
| summarize StartTime = min(StartTime), EndTime = max(EndTime), make_set(UserPrincipalName), make_set(UserDisplayName), make_set(set_ClientAppUsed), make_set(set_FullLocation), make_list(FailureCount) by IPAddress, AppDisplayName
| extend FailedPrincipalCount = arraylength(set_UserPrincipalName)
) on IPAddress
| project IPAddress, StartTime, EndTime, TargetedApplication=AppDisplayName, FailedPrincipalCount, UserPrincipalNames=set_UserPrincipalName, UserDisplayNames=set_UserDisplayName, ClientAppsUsed=set_set_ClientAppUsed, Locations=set_set_FullLocation, FailureCountByPrincipal=list_FailureCount, WindowThresholdBreaches
| join kind= inner (
SigninLogs // get data on success vs. failure history for each IP
| where TimeGenerated > ago(timeRange)
| where ResultType in (0, 50076, 50158, 50074, 50126, 50053) // success or failure types
| summarize GlobalSuccessPrincipalCount = dcountif(UserPrincipalName, (ResultType in (0, 50076, 50158, 50074))), GlobalFailPrincipalCount = dcountif(UserPrincipalName, (ResultType in (50126, 50053))) by IPAddress
| where GlobalFailPrincipalCount > GlobalSuccessPrincipalCount // where the number of failed principals is greater than success - eliminates FPs from IPs who authenticate successfully alot and as a side effect have alot of failures
) on IPAddress
| project-away IPAddress1
| extend timestamp=StartTime, IPCustomEntity = IPAddress