Detection rules › Kusto

Successful AWS Console Login from IP Address Observed Conducting Password Spray

Severity
medium
Time window
1m
Group by
AccountObjectId, AlertName, AlertSeverity, AlertTime, AlertTimeGenerated, EntityIp, ProductName, ProviderName, SourceIpAddress, Tactics, Techniques
Source
github.com/Azure/Azure-Sentinel

'This query aims to detect instances of successful AWS console login events followed by multiple failed app logons alerts generated by Microsoft Cloud App Security or password spray alerts generated by Defender Products. Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. The login is considered relevant if it originates from an IP address associated with potential attackers.'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
Credential AccessT1110 Brute Force

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: 188db479-d50a-4a9c-a041-644bae347d1f
name: Successful AWS Console Login from IP Address Observed Conducting Password Spray
description: |
  'This query aims to detect instances of successful AWS console login events followed by multiple failed app logons alerts generated by Microsoft Cloud App Security or password spray alerts generated by Defender Products.
   Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. 
   The login is considered relevant if it originates from an IP address associated with potential attackers.'
severity: Medium
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: MicrosoftDefenderAdvancedThreatProtection
    dataTypes:
      - SecurityAlert
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1110
  - T1078
query: |
  SecurityAlert 
  // Filtering alerts based on Microsoft product names and Relevent alert names
    | where ProductName in ( "Microsoft Cloud App Security","Azure Active Directory Identity Protection")
    |where AlertName in ("Multiple failed user log on attempts to an app","Password Spray")
  // Parsing and extending the 'Entities' column as JSON objects
    | extend Entities = parse_json(Entities) 
  // Exploring IP entities within the alert entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'ip' 
        | extend EntityIp = tostring(Entity.Address) 
        ) 
  // Exploring account entities within the alert entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'account' 
        | extend AccountObjectId = tostring(Entity.AadUserId)
        )
  // Filtering out alerts with missing IP or account information
    | where isnotempty(EntityIp) and isnotempty(AccountObjectId)
  // Summarizing relevant fields for further analysis
    | summarize 
        by 
        AlertName,
        ProductName,
        ProviderName,
        AlertSeverity,
        EntityIp,
        Tactics,
        Techniques,
        AlertTime= bin(TimeGenerated, 1min),
        AccountObjectId,
        AlertTimeGenerated=TimeGenerated
  // Joining with IdentityInfo to obtain additional account details
    | join kind=inner (
        IdentityInfo
        | where TimeGenerated >= ago(1d)
        | distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
        )
        on AccountObjectId 
        |extend Name = tostring(split(AccountUPN,'@')[0]), UPNSuffix =tostring(split(AccountUPN,'@')[1])
  // Joining with AWSCloudTrail data to correlate AWS console logins
    | join kind=inner (
        AWSCloudTrail
        | where EventName == "ConsoleLogin"
        | extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
        | extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)  
        | where ActionType == "Success"
        | extend AWSTime= bin(TimeGenerated, 1min)
        | project
            EventName,
            EventSource,
            EventTypeName,
            RecipientAccountId,
            ResponseElements,
            SessionMfaAuthenticated,
            SourceIpAddress,
            TimeGenerated,
            UserAgent,
            UserIdentityArn,
            UserIdentityType,
            CTUPN,
            AWSTime,
            UserIdentityUserName
        )
        on $left.EntityIp == $right.SourceIpAddress 
  // Filtering login event after the Alert generation time
    | where AlertTimeGenerated between ((AWSTime - 1h)..(AWSTime + 1h))
  // Calculating the time difference between alert generation and AWS login
    | extend timediff = datetime_diff('minute', AlertTimeGenerated, TimeGenerated) 
  // Filtering alerts with a time difference of up to 60 minutes
    | where timediff <= 60
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUPN
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
   AWSUser: UserIdentityArn
   UserAgent: UserAgent
   AWSUserUPN: CTUPN
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where ProductName in ( "Microsoft Cloud App Security","Azure Active Directory Identity Protection")

Stage 3: where

| where AlertName in ("Multiple failed user log on attempts to an app","Password Spray")

Stage 4: extend

| extend Entities = parse_json(Entities)

Stage 5: kusto:mv-apply

| mv-apply Entity = Entities on 
      ( 
      where Entity.Type == 'ip' 
      | extend EntityIp = tostring(Entity.Address) 
      )

Stage 6: kusto:mv-apply

| mv-apply Entity = Entities on 
      ( 
      where Entity.Type == 'account' 
      | extend AccountObjectId = tostring(Entity.AadUserId)
      )

Stage 7: where

| where isnotempty(EntityIp) and isnotempty(AccountObjectId)

Stage 8: summarize

| summarize 
      by 
      AlertName,
      ProductName,
      ProviderName,
      AlertSeverity,
      EntityIp,
      Tactics,
      Techniques,
      AlertTime= bin(TimeGenerated, 1min),
      AccountObjectId,
      AlertTimeGenerated=TimeGenerated

Stage 9: join

| join kind=inner (
      IdentityInfo
      | where TimeGenerated >= ago(1d)
      | distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
      )
      on AccountObjectId

Stage 10: extend

| extend Name = tostring(split(AccountUPN,'@')[0]), UPNSuffix =tostring(split(AccountUPN,'@')[1])

Stage 11: join

| join kind=inner (
      AWSCloudTrail
      | where EventName == "ConsoleLogin"
      | extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
      | extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)  
      | where ActionType == "Success"
      | extend AWSTime= bin(TimeGenerated, 1min)
      | project
          EventName,
          EventSource,
          EventTypeName,
          RecipientAccountId,
          ResponseElements,
          SessionMfaAuthenticated,
          SourceIpAddress,
          TimeGenerated,
          UserAgent,
          UserIdentityArn,
          UserIdentityType,
          CTUPN,
          AWSTime,
          UserIdentityUserName
      )
      on $left.EntityIp == $right.SourceIpAddress

Stage 12: where

| where AlertTimeGenerated between ((AWSTime - 1h)..(AWSTime + 1h))

Stage 13: extend

| extend timediff = datetime_diff('minute', AlertTimeGenerated, TimeGenerated)

Stage 14: where

| where timediff <= 60

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
AccountObjectIdis_not_null
  • (no value, null check)
ActionTypeeq
  • Success transforms: cased
AlertNamein
  • Multiple failed user log on attempts to an app transforms: cased
  • Password Spray transforms: cased
EntityIpis_not_null
  • (no value, null check)
EventNameeq
  • ConsoleLogin transforms: cased
ProductNamein
  • Azure Active Directory Identity Protection transforms: cased
  • Microsoft Cloud App Security transforms: cased
Typeeq
  • account transforms: cased
  • ip transforms: cased
timediffle
  • 60 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
AccountObjectIdsummarize
AlertNamesummarize
AlertSeveritysummarize
AlertTimesummarize
AlertTimeGeneratedsummarize
EntityIpsummarize
ProductNamesummarize
ProviderNamesummarize
Tacticssummarize
Techniquessummarize
Nameextend
UPNSuffixextend
timediffextend