Accounts with the “Don’t Expire Password” setting may indicate potential long-term access or persistence by adversaries, as such accounts can be used for sustained unauthorized activity. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify compromised or malicious accounts that could be used for ongoing lateral movement or data exfiltration.
KQL 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
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 Requir
| Sentinel Table | Notes |
|---|---|
SecurityEvent | Ensure this data connector is enabled |
WindowsEvent | Ensure this data connector is enabled |
Scenario: Scheduled Job Running Password Expiry Check
Description: A system maintenance job runs nightly to check and ensure that critical accounts do not have password expiration dates.
Filter/Exclusion: process.name != "PasswordExpiryCheckJob.exe" or process.parent.name != "TaskScheduler"
Scenario: Admin Task to Reset Passwords for Non-Expiring Accounts
Description: An administrator manually resets passwords for accounts that are configured to never expire as part of a routine security review.
Filter/Exclusion: user.account_type == "admin" or event_id != 4723 (if applicable)
Scenario: User Account Created with Password Never Expire Enabled
Description: A new user account is created with the “Don’t Expire Password” option enabled by default during onboarding.
Filter/Exclusion: event_id != 4726 or user.account_status == "new"
Scenario: Automated Password Management Tool Configuration
Description: A password management tool like HashiCorp Vault or Microsoft Azure Password Vault configures accounts to have passwords never expire as part of its policy enforcement.
Filter/Exclusion: process.name != "vault.exe" or process.name != "azurepasswordvault.exe"
Scenario: Temporary Account for Service or Application Use
Description: A temporary account is created for a service like SQL Server or Exchange that requires a non-expiring password for continuous operation.
Filter/Exclusion: user.account_role == "service_account" or process.name == "sqlservr.exe" or process.name == "msexch.exe"