Detection rules › Kusto
Successful AWS Console Login from IP Address Observed Conducting Password Spray
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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | IdentityInfo action any: Identity information |
Rule body
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
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountObjectId | is_not_null | field:"AccountObjectId" kind:is_not_null | |
ActionType | eq |
| field:"ActionType" kind:eq value:"Success" |
AlertName | in |
| field:"AlertName" kind:in |
EntityIp | is_not_null | field:"EntityIp" kind:is_not_null | |
EventName | eq |
| field:"aws::eventName" kind:eq value:"ConsoleLogin" |
ProductName | in |
| field:"ProductName" kind:in |
Type | eq |
| field:"Type" kind:eq |
timediff | le |
| field:"timediff" kind:le value:"60" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountObjectId | summarize |
AlertName | summarize |
AlertSeverity | summarize |
AlertTime | summarize |
AlertTimeGenerated | summarize |
EntityIp | summarize |
ProductName | summarize |
ProviderName | summarize |
Tactics | summarize |
Techniques | summarize |
Name | extend |
UPNSuffix | extend |
timediff | extend |