Azure-Sentinel/Hunting Queries/MultipleDataSources/PermutationsOnLogonNames.yaml

125 строки
8.2 KiB
YAML

id: 472e83d6-ccec-47b8-b1cd-75500f936981
name: Permutations on logon attempts by UserPrincipalNames indicating potential brute force
description: |
'This identifies failed logon attempts using permutations based on known first and last names within 10m time windows. Iteration through separators or order changes in the logon name may indicate potential Brute Force logon attempts.'
description_detailed: |
'Attackers sometimes try variations on account logon names, this will identify failed attempts on logging in using permutations
based on known first and last name within 10m time windows, for UserPrincipalNames that separated by hyphen(-), underscore(_) and dot(.).
If there is iteration through these separators or order changes in the logon name it may indicate potential Brute Force logon attempts.
For example, attempts with first.last@example.com, last.first@example.com, first_last@example.com and so on.'
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: Office365
dataTypes:
- OfficeActivity
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let fl_Min = 3;
let un_MatchMin = 2;
let upnFunc = (tableName:string){
table(tableName)
| extend Operation = columnifexists("Operation", "Sign-in activity")
| where Operation == "UserLoginFailed" or Operation == "Sign-in activity"
| extend Result = columnifexists("ResultType", "tempValue")
| extend Result = iff(Result == "tempValue", columnifexists("ResultStatus", Result), Result)
| extend ResultValue = case(Result == "0", "Success", Result == "Success" or Result == "Succeeded", "Success", Result)
| where ResultValue != "Success"
| extend UserPrincipalName = columnifexists("UserPrincipalName", "tempValue")
| extend UserPrincipalName = iff(tableName == "OfficeActivity", tolower(UserId), tolower(UserPrincipalName))
| extend UPN = split(UserPrincipalName, "@")
| extend UserNameOnly = tostring(UPN[0]), DomainOnly = tostring(UPN[1])
| where UserNameOnly contains "." or UserPrincipalName contains "-" or UserPrincipalName contains "_"
// Verify we only get accounts without other separators, it would be difficult to identify multi-level separators
// Count of any that are not alphanumeric
| extend charcount = countof(UserNameOnly, '[^0-9A-Za-z]', "regex")
// Drop any that have non-alphanumeric characters still included
| where charcount < 2
// Creating array of name pairs that include the separators we are interested in, this can be added to if needed.
| extend unoArray = case(
UserNameOnly contains ".", split(UserNameOnly, "."),
UserNameOnly contains "-", split(UserNameOnly, "-"),
UserNameOnly contains "_", split(UserNameOnly, "_"),
UserNameOnly)
| extend First = iff(isnotempty(tostring(parse_json(unoArray)[0])), tostring(parse_json(unoArray)[0]),tostring(unoArray))
| extend Last = tostring(parse_json(unoArray)[1])
| extend First4char = iff(countof(substring(First, 0,4), '[0-9A-Za-z]', "regex") >= 4, substring(First, 0,4), "LessThan4"),
First6char = iff(countof(substring(First, 0,6), '[0-9A-Za-z]', "regex") >= 6, substring(First, 0,6), "LessThan6"),
First8char = iff(countof(substring(First, 0,8), '[0-9A-Za-z]', "regex") >= 8, substring(First, 0,8), "LessThan8"),
Last4char = iff(countof(substring(Last, 0,4), '[0-9A-Za-z]', "regex") >= 4, substring(Last, 0,4), "LessThan4"),
Last6char = iff(countof(substring(Last, 0,6), '[0-9A-Za-z]', "regex") >= 6, substring(Last, 0,6), "LessThan6"),
Last8char = iff(countof(substring(Last, 0,8), '[0-9A-Za-z]', "regex") >= 8, substring(Last, 0,8), "LessThan8")
| where First != Last
| summarize UserNames = makeset(UserNameOnly),
fl_Count = count() by bin(TimeGenerated, 10m), First4char, First6char, First8char, Last4char, Last6char, Last8char, Type
};
let SigninList = upnFunc("SigninLogs");
let OffActList = upnFunc("OfficeActivity");
let UserNameList = (union isfuzzy=true SigninList, OffActList);
let Char4List = UserNameList
| project TimeGenerated, First4char, Last4char, UserNames, fl_Count, Type
| where First4char != "LessThan4" and Last4char != "LessThan4";
// Break out first and last so we can then join and see where a first and last match.
let First4charList = Char4List | where isnotempty(First4char)
| summarize un_MatchOnFirst = makeset(UserNames),
fl_CountForFirst = sum(fl_Count) by TimeGenerated, CharSet = First4char, Type
| project TimeGenerated, CharSet, un_MatchOnFirst, un_MatchOnFirstCount = array_length(un_MatchOnFirst), fl_CountForFirst, Type;
let Last4charList = Char4List | where isnotempty(Last4char)
| summarize un_MatchOnLast = makeset(UserNames), fl_CountForLast = sum(fl_Count) by TimeGenerated, CharSet = Last4char, Type
| project TimeGenerated, CharSet, un_MatchOnLast, un_MatchOnLastCount = array_length(un_MatchOnLast), fl_CountForLast, Type;
let char4 = First4charList | join Last4charList on CharSet and TimeGenerated
| project-away TimeGenerated1, CharSet1
// Make sure that we get more than a single match for First or Last
| where un_MatchOnFirstCount >= un_MatchMin or un_MatchOnLastCount >= un_MatchMin
| where fl_CountForFirst >= fl_Min or fl_CountForLast >= fl_Min;
let Char6List = UserNameList
| project TimeGenerated, First6char, Last6char, UserNames, fl_Count, Type
| where First6char != "LessThan6" and Last6char != "LessThan6";
// Break out first and last so we can then join and see where a first and last match.
let First6charList = Char6List | where isnotempty(First6char)
| summarize un_MatchOnFirst = makeset(UserNames), fl_CountForFirst = sum(fl_Count) by TimeGenerated, CharSet = First6char, Type
| project TimeGenerated, CharSet, un_MatchOnFirst, un_MatchOnFirstCount = array_length(un_MatchOnFirst), fl_CountForFirst, Type;
let Last6charList = Char6List | where isnotempty(Last6char)
| summarize un_MatchOnLast = makeset(UserNames), fl_CountForLast = sum(fl_Count) by TimeGenerated, CharSet = Last6char, Type
| project TimeGenerated, CharSet, un_MatchOnLast, un_MatchOnLastCount = array_length(un_MatchOnLast), fl_CountForLast, Type;
let char6 = First6charList | join Last6charList on CharSet and TimeGenerated
| project-away TimeGenerated1, CharSet1
// Make sure that we get more than a single match for First or Last
| where un_MatchOnFirstCount >= un_MatchMin or un_MatchOnLastCount >= un_MatchMin
| where fl_CountForFirst >= fl_Min or fl_CountForLast >= fl_Min;
let Char8List = UserNameList
| project TimeGenerated, First8char, Last8char, UserNames, fl_Count, Type
| where First8char != "LessThan8" and Last8char != "LessThan8";
// Break out first and last so we can then join and see where a first and last match.
let First8charList = Char8List | where isnotempty(First8char)
| summarize un_MatchOnFirst = makeset(UserNames), fl_CountForFirst = sum(fl_Count) by TimeGenerated, CharSet = First8char, Type
| project TimeGenerated, CharSet, un_MatchOnFirst, un_MatchOnFirstCount = array_length(un_MatchOnFirst), fl_CountForFirst, Type;
let Last8charList = Char8List | where isnotempty(Last8char)
| summarize un_MatchOnLast = makeset(UserNames), fl_CountForLast = sum(fl_Count) by TimeGenerated, CharSet = Last8char, Type
| project TimeGenerated, CharSet, un_MatchOnLast, un_MatchOnLastCount = array_length(un_MatchOnLast), fl_CountForLast, Type;
let char8 = First8charList | join Last8charList on CharSet and TimeGenerated
| project-away TimeGenerated1, CharSet1
// Make sure that we get more than a single match for First or Last
| where un_MatchOnFirstCount >= un_MatchMin or un_MatchOnLastCount >= un_MatchMin
| where fl_CountForFirst >= fl_Min or fl_CountForLast >= fl_Min;
(union isfuzzy=true char4, char6, char8)
| project Type, TimeGenerated, CharSet, UserNameMatchOnFirst = un_MatchOnFirst, UserNameMatchOnFirstCount = un_MatchOnFirstCount,
FailedLogonCountForFirst = fl_CountForFirst, UserNameMatchOnLast = un_MatchOnLast, UserNameMatchOnLastCount = un_MatchOnLastCount,
FailedLogonCountForLast = fl_CountForLast
| sort by UserNameMatchOnFirstCount desc, UserNameMatchOnLastCount desc
| extend timestamp = TimeGenerated
version: 1.0.1
metadata:
source:
kind: Community
author:
name: Shain
support:
tier: Community
categories:
domains: [ "Security - Other", "Identity" ]