Detection rules › Kusto

Sign-ins from IPs that attempt sign-ins to disabled accounts

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

'Identifies IPs with failed attempts to sign in to one or more disabled accounts using the IP through which successful signins from other accounts have happened. This could indicate an attacker who obtained credentials for a list of accounts and is attempting to login with those accounts, some of which may have already been disabled. References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes 50057 - User account is disabled. The account has been disabled by an administrator. This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results.'

MITRE ATT&CK coverage

Rule body kusto

id: 500c103a-0319-4d56-8e99-3cec8d860757
name: Sign-ins from IPs that attempt sign-ins to disabled accounts
description: |
  'Identifies IPs with failed attempts to sign in to one or more disabled accounts using the IP through which successful signins from other accounts have happened.
  This could indicate an attacker who obtained credentials for a list of accounts and is attempting to login with those accounts, some of which may have already been disabled.
  References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes
  50057 - User account is disabled. The account has been disabled by an administrator.
  This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: BehaviorAnalytics
    dataTypes:
      - BehaviorAnalytics
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1078
  - T1098
query: |
  let aadFunc = (tableName: string) {
  let failed_signins = table(tableName)
  | where ResultType == "50057"
  | where ResultDescription == "User account is disabled. The account has been disabled by an administrator.";
  let disabled_users = failed_signins | summarize by UserPrincipalName;
  table(tableName)
    | where ResultType == 0
    | where isnotempty(UserPrincipalName)
    | where UserPrincipalName !in (disabled_users)
  | summarize
          successfulAccountsTargettedCount = dcount(UserPrincipalName),
          successfulAccountSigninSet = make_set(UserPrincipalName, 100),
          successfulApplicationSet = make_set(AppDisplayName, 100)
      by IPAddress, Type
      // Assume IPs associated with sign-ins from 100+ distinct user accounts are safe
      | where successfulAccountsTargettedCount < 50
      | where isnotempty(successfulAccountsTargettedCount)
    | join kind=inner (failed_signins
  | summarize
      StartTime = min(TimeGenerated),
      EndTime = max(TimeGenerated),
      totalDisabledAccountLoginAttempts = count(),
      disabledAccountsTargettedCount = dcount(UserPrincipalName),
      applicationsTargeted = dcount(AppDisplayName),
      disabledAccountSet = make_set(UserPrincipalName, 100),
      disabledApplicationSet = make_set(AppDisplayName, 100)
  by IPAddress, Type
  | order by totalDisabledAccountLoginAttempts desc) on IPAddress
  | project StartTime, EndTime, IPAddress, totalDisabledAccountLoginAttempts, disabledAccountsTargettedCount, disabledAccountSet, disabledApplicationSet, successfulApplicationSet, successfulAccountsTargettedCount, successfulAccountSigninSet, Type
  | order by totalDisabledAccountLoginAttempts};
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
  | join kind=leftouter (
      BehaviorAnalytics
      | where ActivityType in ("FailedLogOn", "LogOn")
      | where EventSource =~ "Azure AD"
      | project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserPrincipalName
      | project-rename IPAddress = SourceIPAddress
      | summarize
          Users = make_set(UserPrincipalName, 100),
          UsersInsights = make_set(UsersInsights, 100),
          DevicesInsights = make_set(DevicesInsights, 100),
          IPInvestigationPriority = sum(InvestigationPriority)
      by IPAddress
  ) on IPAddress
  | extend SFRatio = toreal(toreal(disabledAccountsTargettedCount)/toreal(successfulAccountsTargettedCount))
  | where SFRatio >= 0.5
  | sort by IPInvestigationPriority desc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 2.1.3
kind: Scheduled

Stages and Predicates

Parameters

let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let binding: aadFunc

let aadFunc = (tableName: string) {
let failed_signins = table(tableName)
| where ResultType == "50057"
| where ResultDescription == "User account is disabled. The account has been disabled by an administrator.";

Let binding: disabled_users

let disabled_users = failed_signins | summarize by UserPrincipalName;

Stage 1: union

union of 2 branches

Stage 2: source

aadFunc

Stage 3: source

aadFunc

Stage 4: join

join kind=leftouter (...)

Stage 5: extend

extend SFRatio

Stage 6: where

where SFRatio >= 0.5

Stage 7: sort

sort by IPInvestigationPriority

Stage 8: summarize

summarize by IPAddress

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
ActivityTypein
  • FailedLogOn transforms: cased
  • LogOn transforms: cased
EventSourceeq
  • Azure AD
SFRatioge
  • 0.5 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
IPAddresssummarize