Detection rules › Kusto

Failed AzureAD logons but success logon to AWS Console

Severity
medium
Time window
1d
Group by
AWSRegion, AccountName, AccountUPNSuffix, EventTypeName, LoginResult, MFAUsed, Reason, RecipientAccountId, SourceIpAddress, UserAgent, UserIdentityType
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'Identifies a list of IP addresses with a minimum number (defualt of 5) of failed logon attempts to Microsoft Entra ID. Uses that list to identify any successful AWS Console logons from these IPs within the same timeframe.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
Credential AccessT1110 Brute Force

Rule body kusto

id: 643c2025-9604-47c5-833f-7b4b9378a1f5
name: Failed AzureAD logons but success logon to AWS Console
description: |
  'Identifies a list of IP addresses with a minimum number (defualt of 5) of failed logon attempts to Microsoft Entra ID.
  Uses that list to identify any successful AWS Console logons from these IPs within the same timeframe.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
     - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1078
  - T1110
query: |
  //Adjust this threshold to fit your environment
  let signin_threshold = 5;
  //Make a list of IPs with AAD signin failures above our threshold
  let aadFunc = (tableName:string){
  let Suspicious_signins =
  table(tableName)
  | where ResultType !in ("0", "50125", "50140")
  | where IPAddress !in ("127.0.0.1", "::1")
  | summarize count() by IPAddress
  | where count_ >  signin_threshold
  | summarize make_set(IPAddress);
  Suspicious_signins
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  let Suspicious_signins =
  union isfuzzy=true aadSignin, aadNonInt
  | summarize make_set(set_IPAddress);
  //See if any of those IPs have sucessfully logged into the AWS console
  AWSCloudTrail
  | where EventName =~ "ConsoleLogin"
  | extend LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
  | where LoginResult =~ "Success"
  | where SourceIpAddress in (Suspicious_signins)
  | extend Reason = "Multiple failed AAD logins from IP address"
  | extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed)
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Reason, LoginResult, EventTypeName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, AWSRegion, SourceIpAddress, UserAgent, MFAUsed
  | extend timestamp = StartTime
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
version: 1.0.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: aadFunc

let aadFunc = (tableName:string){
let Suspicious_signins =
table(tableName)
| where ResultType !in ("0", "50125", "50140")
| where IPAddress !in ("127.0.0.1", "::1")
| summarize count() by IPAddress
| where count_ >  signin_threshold
| summarize make_set(IPAddress);

Derived from signin_threshold, Suspicious_signins.

union isfuzzy=true (2 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt

Leg 1: aadSignin

Leg 2: aadNonInt

Applied to the combined result

| summarize make_set(set_IPAddress) | where EventName =~ "ConsoleLogin" | extend LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin) | where LoginResult =~ "Success" | where SourceIpAddress in (Suspicious_signins) | extend Reason = "Multiple failed AAD logins from IP address" | extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed) | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn) | extend UserName = tostring(split(UserIdentityArn, '/')[-1]) | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName) | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "") | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Reason, LoginResult, EventTypeName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, AWSRegion, SourceIpAddress, UserAgent, MFAUsed | extend timestamp = StartTime

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
EventNameeq
  • ConsoleLogin
LoginResulteq
  • Success
SourceIpAddressin
  • Suspicious_signins transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AWSRegionsummarize
AccountNamesummarize
AccountUPNSuffixsummarize
EndTimesummarize
EventTypeNamesummarize
LoginResultsummarize
MFAUsedsummarize
Reasonsummarize
RecipientAccountIdsummarize
SourceIpAddresssummarize
StartTimesummarize
UserAgentsummarize
UserIdentityTypesummarize
timestampextend