Detection rules › Kusto

Expired access credentials being used in Azure

Status
available
Severity
medium
Time window
1d
Group by
Day, FailedIp, IPAddress, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

This query searches for logins with an expired access credential (for example an expired cookie). It then matches the IP address from which the expired credential access occurred with the IP addresses of successful logins. If there are logins with expired credentials, but no successful logins from an IP, this might indicate an attacker has copied the authentication cookie and is re-using it on another machine.

MITRE ATT&CK coverage

TacticTechniques
Credential Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 433c3b0a-7278-4d74-b137-963ac6f9a7e7
name: Expired access credentials being used in Azure
description: |
  This query searches for logins with an expired access credential (for example an expired cookie). It then matches the IP address from which the expired credential access occurred with the IP addresses of successful logins.
  If there are logins with expired credentials, but no successful logins from an IP, this might indicate an attacker has copied the authentication cookie and is re-using it on another machine.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1528
query: |
  // Timeframe to search for failed logins.
  let timeframe=1d;
  // Timeframe to look back for successful logins from the same user by IP.
  let lookback=7d;
  let SuspiciousSignings=(
      SigninLogs
      | where TimeGenerated >= ago(timeframe)
      | where ResourceDisplayName contains "Windows Azure Active Directory"
      // 50132 = SsoArtifactInvalidOrExpired - The session is not valid due to password expiration or recent password change.
      // 50173 = FreshTokenNeeded - The provided grant has expired due to it being revoked, and a fresh auth token is needed. 
      // 70008 = ExpiredOrRevokedGrant - The refresh token has expired due to inactivity. The token was issued on XXX and was inactive for a certain period of time.
      // 81010 = DesktopSsoAuthTokenInvalid - Seamless SSO failed because the user's Kerberos ticket has expired or is invalid.
      | where ResultType in (50173, 50132, 70008, 81010)
      | summarize FailedCountPerDay=count(),FailedUserAgents=make_set(UserAgent), FailedCountries=make_set(LocationDetails.countryOrRegion),FailedIps=make_set(IPAddress) by UserPrincipalName, Day=bin(TimeGenerated, 1d)
      | where FailedCountPerDay >= 1
  );
  let SuccessLogins=(
      SigninLogs
      | where TimeGenerated >= ago(lookback)
      | where UserPrincipalName in ((SuspiciousSignings | project UserPrincipalName))
      | where ResultType == 0
      | summarize count() by UserPrincipalName, IPAddress
  );
  SuspiciousSignings
  | mv-expand FailedIp=FailedIps
  | extend FailedIp=tostring(FailedIp)
  | join kind=leftanti SuccessLogins on $left.FailedIp==$right.IPAddress, UserPrincipalName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: FailedIp
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1d;
let lookback = 7d;

let SuspiciousSignings is inlined into the numbered stages below.

Let binding: SuccessLogins used in Stage 9

let SuccessLogins = (
    SigninLogs
    | where TimeGenerated >= ago(lookback)
    | where UserPrincipalName in ((SuspiciousSignings | project UserPrincipalName))
    | where ResultType == 0
    | summarize count() by UserPrincipalName, IPAddress
);

Stages 1 to 6 define let SuspiciousSignings (the rule's main pipeline source); stages 7 to 9 run on it.

Stage 1: source

SigninLogs

Stage 2: where

| where TimeGenerated >= ago(timeframe)

Stage 3: where

| where ResourceDisplayName contains "Windows Azure Active Directory"

Stage 4: where

| where ResultType in (50173, 50132, 70008, 81010)

Stage 5: summarize

| summarize FailedCountPerDay=count(),FailedUserAgents=make_set(UserAgent), FailedCountries=make_set(LocationDetails.countryOrRegion),FailedIps=make_set(IPAddress) by UserPrincipalName, Day=bin(TimeGenerated, 1d)

Stage 6: where

| where FailedCountPerDay >= 1

Stage 7: mv-expand

SuspiciousSignings
| mv-expand FailedIp=FailedIps

Stage 8: extend

| extend FailedIp=tostring(FailedIp)

Stage 9: join (negated)

| join kind=leftanti SuccessLogins on $left.FailedIp==$right.IPAddress, UserPrincipalName

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ResultTypeeq0excludes:ResultType field:"ResultType" value:"0"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Daysummarize
FailedCountPerDaysummarize
FailedCountriessummarize
FailedIpssummarize
FailedUserAgentssummarize
UserPrincipalNamesummarize
FailedIpextend