Detection rules › Kusto

AD account with Don't Expire Password

Severity
low
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Identifies whenever a user account has the setting "Password Never Expires" in the user account properties selected. This is indicated in Security event 4738 in the EventData item labeled UserAccountControl with an included value of %%2089. %%2089 resolves to "Don't Expire Password - Enabled".

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1098 Account Manipulation

Event coverage

ProviderEventTitle
Security-AuditingEvent ID 4738A user account was changed.

Rule body kusto

id: 6c360107-f3ee-4b91-9f43-f4cfd90441cf
name: AD account with Don't Expire Password
description: |
  'Identifies whenever a user account has the setting "Password Never Expires" in the user account properties selected.
  This is indicated in Security event 4738 in the EventData item labeled UserAccountControl with an included value of %%2089.
  %%2089 resolves to "Don't Expire Password - Enabled".'
severity: Low
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
 union isfuzzy=true
 (
  SecurityEvent
  | where EventID == 4738
  // 2089 value indicates the Don't Expire Password value has been set
  | where UserAccountControl has "%%2089"
  | extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")
  // 2050 indicates that the Password Not Required value is NOT set, this often shows up at the same time as a 2089 and is the recommended value.  This value may not be in the event.
  | extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")
  // If value %%2082 is present in the 4738 event, this indicates the account has been configured to logon WITHOUT a password. Generally you should only see this value when an account is created and only in Event 4720: Account Creation Event.
  | extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")
  | project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName, TargetDomainName, TargetSid, 
  AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectUserName, SubjectDomainName, SubjectUserSid
  ),
  (
  WindowsEvent
  | where EventID == 4738 and EventData has '2089'
  // 2089 value indicates the Don't Expire Password value has been set
  | extend UserAccountControl = tostring(EventData.UserAccountControl)
  | where UserAccountControl has "%%2089"
  | extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")
  // 2050 indicates that the Password Not Required value is NOT set, this often shows up at the same time as a 2089 and is the recommended value.  This value may not be in the event.
  | extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")
  // If value %%2082 is present in the 4738 event, this indicates the account has been configured to logon WITHOUT a password. Generally you should only see this value when an account is created and only in Event 4720: Account Creation Event.
  | extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")
  | extend Activity="4738 - A user account was changed."
  | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | extend TargetSid = tostring(EventData.TargetSid)
  | extend SubjectAccount = strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
  | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
  | extend AccountType=case(SubjectAccount endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
  | project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName), TargetSid, 
  AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName), SubjectUserSid = tostring(EventData.SubjectUserSid)
  )
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetAccount
      - identifier: Name
        columnName: TargetUserName
      - identifier: NTDomain
        columnName: TargetDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: TargetSid
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SubjectAccount
      - identifier: Name
        columnName: SubjectUserName
      - identifier: NTDomain
        columnName: SubjectDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: SubjectUserSid
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.2.2
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Stage 1: union

union isfuzzy=true

Stage 2: source time_window=86400s

SecurityEvent

Stage 3: where

| where EventID == 4738

Stage 4: where

| where UserAccountControl has "%%2089"

Stage 5: extend

| extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")

Stage 6: extend

| extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")

Stage 7: extend

| extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")

Stage 8: project

| project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName, TargetDomainName, TargetSid, 
 AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectUserName, SubjectDomainName, SubjectUserSid

Stage 9: source

WindowsEvent

Stage 10: where

| where EventID == 4738 and EventData has '2089'

Stage 11: extend

| extend UserAccountControl = tostring(EventData.UserAccountControl)

Stage 12: where

| where UserAccountControl has "%%2089"

Stage 13: extend

| extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")

Stage 14: extend

| extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")

Stage 15: extend

| extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")

Stage 16: extend

| extend Activity="4738 - A user account was changed."

Stage 17: extend

| extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)

Stage 18: extend

| extend TargetSid = tostring(EventData.TargetSid)

Stage 19: extend

| extend SubjectAccount = strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)

Stage 20: extend

| extend SubjectUserSid = tostring(EventData.SubjectUserSid)

Stage 21: extend

| extend AccountType=case(SubjectAccount endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")

Stage 22: project

| project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName), TargetSid, 
 AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName), SubjectUserSid = tostring(EventData.SubjectUserSid)

Stage 23: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 24: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)

Stage 25: project-away

| project-away DomainIndex

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
EventDatamatch
  • 2089
EventIDeq
  • 4738 transforms: cased corpus 5 (splunk 4, kusto 1)
UserAccountControlmatch
  • %%2089

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
AccountTypeproject
Activityproject
Computerproject
EventIDproject
StartTimeproject
SubjectAccountproject
SubjectDomainNameproject
SubjectUserNameproject
SubjectUserSidproject
TargetAccountproject
TargetDomainNameproject
TargetSidproject
TargetUserNameproject
UserAccountControlproject
Value_2050project
Value_2082project
Value_2089project
HostNameextend
HostNameDomainextend