Detection rules › Kusto
Failed host logons but success logon to AzureAD
Identifies a list of IP addresses with a minimum number(default of 5) of failed logon attempts to remote hosts. Uses that list to identify any successful logons to Microsoft Entra ID from these IPs within the same timeframe.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078 Valid Accounts |
| Credential Access | T1110 Brute Force |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 4625 | An account failed to log on. |
Rule body kusto
id: 1ce5e766-26ab-4616-b7c8-3b33ae321e80
name: Failed host logons but success logon to AzureAD
description: |
'Identifies a list of IP addresses with a minimum number(default of 5) of failed logon attempts to remote hosts.
Uses that list to identify any successful logons to Microsoft Entra ID from these IPs within the same timeframe.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvents
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
- T1110
query: |
//Adjust this threshold to fit environment
let signin_threshold = 5;
//Make a list of IPs with failed Windows host logins above threshold
let win_fails =
SecurityEvent
| where EventID == 4625
| where LogonType in (10, 7, 3)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
let wef_fails =
WindowsEvent
| where EventID == 4625
| extend LogonType = tostring(EventData.LogonType)
| where LogonType in (10, 7, 3)
| extend IpAddress = tostring(EventData.IpAddress)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
//Make a list of IPs with failed *nix host logins above threshold
let nix_fails =
Syslog
| where Facility contains 'auth' and ProcessName != 'sudo' and SyslogMessage has 'from' and not(SyslogMessage has_any ('Disconnecting', 'Disconnected', 'Accepted', 'disconnect', @'[preauth]'))
| extend SourceIP = extract("(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.(([0-9]{1,3})))",1,SyslogMessage)
| where SourceIP != "" and SourceIP != "127.0.0.1"
| summarize count() by SourceIP
| where count_ > signin_threshold
| summarize make_list(SourceIP);
//See if any of the IPs with failed host logins hve had a sucessful Azure AD login
let aadFunc = (tableName:string){
table(tableName)
| where ResultType in ("0", "50125", "50140")
| where IPAddress in (win_fails) or IPAddress in (nix_fails) or IPAddress in (wef_fails)
| extend Reason= "Multiple failed host logins from IP address with successful Azure AD login"
| extend timestamp = TimeGenerated, Type = Type
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.1.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Parameters
let signin_threshold = 5;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
Let binding: win_fails
let win_fails = SecurityEvent
| where EventID == 4625
| where LogonType in (10, 7, 3)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
Derived from signin_threshold.
Let binding: wef_fails
let wef_fails = WindowsEvent
| where EventID == 4625
| extend LogonType = tostring(EventData.LogonType)
| where LogonType in (10, 7, 3)
| extend IpAddress = tostring(EventData.IpAddress)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
Derived from signin_threshold.
Let binding: nix_fails
let nix_fails = Syslog
| where Facility contains 'auth' and ProcessName != 'sudo' and SyslogMessage has 'from' and not(SyslogMessage has_any ('Disconnecting', 'Disconnected', 'Accepted', 'disconnect', @'[preauth]'))
| extend SourceIP = extract("(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.(([0-9]{1,3})))",1,SyslogMessage)
| where SourceIP != "" and SourceIP != "127.0.0.1"
| summarize count() by SourceIP
| where count_ > signin_threshold
| summarize make_list(SourceIP);
Derived from signin_threshold.
Let binding: aadFunc
let aadFunc = (tableName:string){
table(tableName)
| where ResultType in ("0", "50125", "50140")
| where IPAddress in (win_fails) or IPAddress in (nix_fails) or IPAddress in (wef_fails)
| extend Reason= "Multiple failed host logins from IP address with successful Azure AD login"
| extend timestamp = TimeGenerated, Type = Type
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
};
Derived from win_fails, wef_fails, nix_fails.
union isfuzzy=true (2 sources)
Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt