Detection rules › Kusto

AWSCloudTrail - Login to AWS Management Console without MFA

Status
available
Severity
low
Time window
1d
Group by
AWSRegion, AccountName, AccountUPNSuffix, EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, SessionMfaAuthenticated, SourceIpAddress, UserAgent, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityUserName, indexId
Source
github.com/Azure/Azure-Sentinel

Identifies successful AWS Management Console sign-ins where CloudTrail records a ConsoleLogin event without multi-factor authentication. The rule looks for logins where MFAUsed is not Yes and the console response is not Failure, which can indicate credential misuse or weak account protection.

MITRE ATT&CK coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: d25b1998-a592-4bc5-8a3a-92b39eedb1bc
name: AWSCloudTrail - Login to AWS Management Console without MFA
description: |
  Identifies successful AWS Management Console sign-ins where CloudTrail records a ConsoleLogin event without
  multi-factor authentication. The rule looks for logins where MFAUsed is not Yes and the console response is not
  Failure, which can indicate credential misuse or weak account protection.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AWSS3
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
  - Persistence
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  AWSCloudTrail
  | where EventName =~ "ConsoleLogin"
  | extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin), indexId = indexof(tostring(UserIdentityPrincipalid),":")
  | where MFAUsed !~ "Yes" and LoginResult !~ "Failure"
  | where SessionIssuerUserName !contains "AWSReservedSSO"
  | 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 StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId,  UserIdentityPrincipalid, UserAgent,
  UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, indexId
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  EventName: EventName
  LoginResult: LoginResult
  MFAUsed: MFAUsed
  AWSRegion: AWSRegion
  UserAgent: UserAgent
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS Management Console sign-in without MFA: {{AccountName}} from {{SourceIpAddress}}'
  alertDescriptionFormat: 'Successful ConsoleLogin without MFA for {{AccountName}} from {{SourceIpAddress}} in account {{RecipientAccountId}}.'
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName =~ "ConsoleLogin"

Stage 3: extend

| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin), indexId = indexof(tostring(UserIdentityPrincipalid),":")

Stage 4: where

| where MFAUsed !~ "Yes" and LoginResult !~ "Failure"

Stage 5: where

| where SessionIssuerUserName !contains "AWSReservedSSO"

Stage 6: extend (4 consecutive steps)

| 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]), "")

Stage 7: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId,  UserIdentityPrincipalid, UserAgent,
UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, indexId

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SessionIssuerUserNamecontainsAWSReservedSSOexcludes:SessionIssuerUserName field:"SessionIssuerUserName" value:"AWSReservedSSO"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionsummarize
AccountNamesummarize
AccountUPNSuffixsummarize
EndTimeUtcsummarize
EventNamesummarize
EventTypeNamesummarize
LoginResultsummarize
MFAUsedsummarize
RecipientAccountIdsummarize
SessionMfaAuthenticatedsummarize
SourceIpAddresssummarize
StartTimeUtcsummarize
UserAgentsummarize
UserIdentityAccountIdsummarize
UserIdentityPrincipalidsummarize
UserIdentityUserNamesummarize
indexIdsummarize