Metadata #3 - Spot who is cheating on the password policy

Yes there are some ways to workaround the password policy... Mainly for operators and administrators though. But those should not be exempted of being monitored for compliance.

This post is a part of the Metadata series. Have a look at the intro to have more information about it:

Metadata #0 - Introduction, what are metadata and why do we care?

Here is one of the scenario we're looking at:

  • The password policy forces users to change their password once every 120 days
  • Password history is set to 10
  • The minimum password age is set 0 day (by mistake or by design see later in this article)

There is the possibility that a user changes its password 10 times in a row and finally reuse the same. You can find out if some users found the trick on the internet and identify who is doing it now :) But that's not it...

User must change password at next logon

There is another trick that make you able to keep the same password. If you check the option "User must change password at next logon", apply and then uncheck it the targeted account is good with its previous password for another 120 days without changing it.

FRAUD_0

Only accounts with some administrative right can do it, so your admins can do it. Why that? Password expiration is determined by a simple calculation between Password Expiration Time and the attribute pwdLastSet. If the delta between the current date and the pwdLastSet is larger than the threshold defined in the password policy, the password is expired and the user is prompted to change it. Using this option will set the pwdLastSet to the current date whereas the password is actually not touched. Easy to spot, if the last modification time in the metadata of the password attributes is not equal to the value of pwdLastSet, someone has been playing you. Time to check your admin or service accounts!

Password storage

The password of the user (well the hash of the password of the user) is stored in two attributes:

  • unicodePwd stores the NTHash of the user account
  • dBCSPwd stores the LMHash of the user (this is being updated at the same time at the previous one, even if you disabled the LMHash storage, then it is just updated with a random value)

You also have the two extra attributes which contain the history of the password (ntPwdHistory and lmPwdHistory) but we don't really care about them in this article.

Note that you don't have any way to read this value of those attributes (well there isn't any API exposing those information... In Windows). Anyhow, because those information have to be replicated across all domain controllers of the domain they do have replication metadata associated to it. Metadata means that we have a version number, so ultimately we will know how many time the value of the attribute has changed since the creation of the account.

User creation

What is the default value of the password when you create a user account? I just created a user called Carlos with a blank password (you can create a user with a blank password if you create it disabled). Let's have a look at the metadata of the password:

C:>repadmin /showobjmeta . "CN=Carlos,OU=People,DC=contoso,DC=com" | findstr "Ver unicodePwd"Loc.USN                           Originating DSA  Org.USN  Org.Time/Date        Ver Attribute  24784              Default-First-Site-NameDC01     24784 2014-08-12 06:17:25    2 unicodePwd

So basically the password version is 2 when you create it with "net user ... /add" or the other admin graphical tools. But in fact it depends how you create the account. Let's create a user with ADSIEDIT.MSC:

C:>repadmin /showobjmeta . "CN=Carlos-ADSIEDIT,OU=People,DC=contoso,DC=com" | findstr "Ver unicodePwd"Loc.USN                           Originating DSA  Org.USN  Org.Time/Date        Ver Attribute  24789              Default-First-Site-NameDC01     24789 2014-08-12 06:22:01    1 unicodePwd

This time the password version is 1. But anyways, it is a small number. Then it is incremented by one every time you change your password. And because you are likely using the option User must change password at next logon, the first "useable" password will be version 3.
You can also use the information to know if a password ever changed for a given account. If the value for the whenCreated attribute is is equal to the Org.Time/Date for the password attribute (more or less one second), it means the password never changed. Well you could have just use the pwdLastTime as well... But this article is talking about metadata :)

Math

Let's do the math. If we know when the user has been created, and how long a password can be used, a simple division will tell us how many time a given user should have changed its password since its creation. It is only theory. Nothing prevent a user to change its password before the end of the expiration, or maybe the user forgot its password and had it reset by the helpdesk. So if the result of the division we just discuss varies by 3 or 4 from the expected result, no need to worry. However, if it varies from 20 or more, maybe we spotted a weird behavior :)

  • Threshold = 120 days, a user must change its password every 120 days at least
    Well we can collect this info from the domain policy:

     (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
    
  • whenCreated of the object is 2012-01-01 09:00:00 (it has been created January the 1st at 9AM... well it wasn't a fun NYE party for the IT guy in charge of creating this account).

  • Now it is now :) which in PowerShell is translated by: Get-Date

  • the version of the unicodePwd is 12 (it started at 2, plus one for the first set so the password changed 9 times, 12-3=9)

  • How many days between Now and whenCreated?

     ((Get-Date) - (Get-Date "2012-01-01 09:00:00")).Days 
    #Output: 954
    

    So 954 days elapsed since the creation

  • So the password should have changed... 954 / 120 = ?

     ((Get-Date) - (Get-Date "2012-01-01 09:00:00")).Days / 120 
    #Output: 7.95
    

    Well we do not care about unfinished day so let's floor it:

     [Math]::Floor(7.95)
    #Ouptut: 7
    

    I know you don't need PowerShell to floor the number, I put it here because we will use PowerShell in the script :)

  • And the password changed 9 times, a delta of 2. So it looks like this account is ok. The user probably forgot its password twice and called the helpdesk.

Let's do that for all the users

It could be long and painful depending on the number of users you have, so you can limit the check for a subscope like an OU or use other criteria. Please refer to the article Metadata #1 - When did the delegation change? How to track security descriptor modifications which has an section called I have millions of objects which deal with large environment.

What about Fine Grained Password Policy?

If a user is applying a FGPP, it might have a different password expiration policy. Fortunately, we have a PowerShell cmdLet that tells us what FGGP (if any) applies to the user: Get-ADUserResultantPasswordPolicy. So we can go this way:

 
#Set a dc for the LDAP and metadata queries
$_dc_to_use = "DC01"
#Set the default password age threshold , this might be different for users applying FGPPs
$_domain_threshold = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days

#Get all the enabled users in the domain
#WARNING Set a smaller scope dependig on the number of users
$_myQuery = Get-ADUser `
   -SearchBase "DC=contoso,DC=com" `
   -SearchScope Subtree `
   -LDAPFilter "(!userAccountControl:1.2.840.113556.1.4.803:=2)" ` 
   -Properties distinguishedName,whenCreated `
   -Server $_dc_to_use

#process all users returned by the previous query
$_myQuery | ForEach-Object `
{
    $_user_distinguishedName = $_.distinguishedName
    $_user_whenCreated = $_.whenCreated.ToUniversalTime()
    #Check if the user is applying a FGPP
    $_user_check_fgpp = Get-ADUserResultantPasswordPolicy -Identity $_.distinguishedName
    if ($_user_check_fgpp -ne $null)
    {
        $_user_threshold = $_user_check_fgpp.MaxPasswordAge.days
        $_user_fgpp = $true
    } else {
        #The user applies a FGPP, changing the threshold
        $_user_threshold = $_domain_threshold
        $_user_fgpp = $false
    }
    #Getting and storing metadata
    $_user_metadata = Get-ADReplicationAttributeMetadata -Object $_user_distinguishedName -Properties pwdLastset,unicodePwd -Server $_dc_to_use
    $_user_pwdLastset_version =  ($_user_metadata | Where-Object { $_.AttributeName -eq "pwdLastset" }).Version
    $_user_pwdLastset_date =  ($_user_metadata | Where-Object { $_.AttributeName -eq "pwdLastset" }).LastOriginatingChangeTime
    $_user_unicodePwd_version =  ($_user_metadata | Where-Object { $_.AttributeName -eq "unicodePwd" }).Version
    $_user_unicodePwd_date =  ($_user_metadata | Where-Object { $_.AttributeName -eq "unicodePwd" }).LastOriginatingChangeTime

    #Calculate the fraudulant user threshold
    $_fraud_threshold = [int] [Math]::Floor( ((Get-Date) - $_user_whenCreated).Days / $_user_threshold ) + 2

    #Create an object for a nice looking output
    New-Object -TypeName psobject -Property @{
        User = $_user_distinguishedName
        Created = $_user_whenCreated
        LastPasswordChange = $_user_pwdLastset_date
        NbPaswordChange = $_user_unicodePwd_version
        ApplyFGPP = $_user_fgpp
        UserThreshold = $_user_threshold
        FraudThreshold = $_fraud_threshold

    }
} | Out-GridView -Title Results #Displaying the result in a Grid-View, this requires PowerShell_ISE installed

Here is the output:

FRAUD_2

So if the NbPasswordChange is higher than the FraudThreshold, there is something fishy about the account. In my example, if you take a look at the second row, you can see that Alice changed her password 31 times whereas based on how old the account is used, a normal usage should be 6. It seems that she is using this password rotation technique... Or that she forgot her password many times and open tickets with the helpdesk to reset it...

Also look at the last row. The user Pierre changed its password 7 times whereas it exists for less than a month (this is why the fraudulent threshold is 2, which means that the password should have not changed since the user has never reach its password age threshold 30 days in this case since the user apparently apply a FGPP).

Fine & Final report

So now, let's make this script a bit better by adding the following improvements:

  • Detect if the user has kept the same password using the technique mentioned earlier consisting on checking the user must change its password at next logon and uncheck it just after...
  • Detect if the user is using the password rotation trick
  • Generate a nice HTML report, because it looks better and also removed the dependency on PowerShell_ISE for the Out-GridView portion

Here is it (again, provided AS-IS, no error management and minimal output customization):

 #Set a dc for the LDAP and metadata queries
$_dc_to_use = "DC01"
#Set the default password age threshold , this might be different for users applying FGPPs
$_domain_threshold = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
$_domain_threshold_rotation = (Get-ADDefaultDomainPasswordPolicy).PasswordHistoryCount
#Set value for reporting
$_today = Get-Date
$_timestamp = $_today.toString(‘yyyyMMddhhmm’)
$_output_name = "$($_timestamp)-CheckPasswordFraud.html"

#Get all the enabled users in the domain
#WARNING Set a smaller scope dependig on the number of users
$_myQuery = Get-ADUser `
   -SearchBase "DC=contoso,DC=com" `
   -SearchScope Subtree `
   -LDAPFilter "(!userAccountControl:1.2.840.113556.1.4.803:=2)" `
   -Properties distinguishedName,whenCreated,pwdLastset `
   -Server $_dc_to_use

#Create an array to store the results
$_results = @()
#Process all users returned by the previous query
$_myQuery | ForEach-Object `
{
    $_user_distinguishedName = $_.distinguishedName
    $_user_whenCreated = $_.whenCreated.ToUniversalTime()
    $_user_pwdLastset = $_.pwdLastset
    #Check if the user is applying a FGPP
    $_user_check_fgpp = Get-ADUserResultantPasswordPolicy -Identity $_.distinguishedName
    if ($_user_check_fgpp -ne $null)
    {
        $_user_threshold = $_user_check_fgpp.MaxPasswordAge.days
        $_user_rotation_threshold = $_user_check_fgpp.PasswordHistoryCount
        $_user_fgpp = $true
    } else {
        #The user applies a FGPP, changing the threshold
        $_user_threshold = $_domain_threshold
        $_user_rotation_threshold = $_domain_threshold_rotation
        $_user_fgpp = $false
    }
    #Getting and storing metadata
    $_user_metadata = Get-ADReplicationAttributeMetadata -Object $_user_distinguishedName -Properties pwdLastset,unicodePwd -Server $_dc_to_use
    $_user_pwdLastset_version =  ($_user_metadata | Where-Object { $_.AttributeName -eq "pwdLastset" }).Version
    $_user_pwdLastset_date =  ($_user_metadata | Where-Object { $_.AttributeName -eq "pwdLastset" }).LastOriginatingChangeTime
    $_user_unicodePwd_version =  ($_user_metadata | Where-Object { $_.AttributeName -eq "unicodePwd" }).Version
    $_user_unicodePwd_date =  ($_user_metadata | Where-Object { $_.AttributeName -eq "unicodePwd" }).LastOriginatingChangeTime

    #Calculate the fraudulant user threshold
    $_fraud_threshold = [int] [Math]::Floor( ((Get-Date) - $_user_whenCreated).Days / $_user_threshold ) + 2
    
    #Determine if the user is fraudulent
    #Check if the user is rotate its password way too many time
    $_rotation_delta = $_user_unicodePwd_version - $_fraud_threshold 

    if ( $_rotation_delta -gt $_user_rotation_threshold )
    {
        $_user_fraud_rotation = $true
    } else {
        $_user_fraud_rotation = $false
    }

    #Check if the user currently has the checkbox User must change its password at next logon
    if ( $_user_pwdLastset -eq 0 )
    {
        #the user currently have checkbox checked we can skip irregualrity check
        $_user_fraud_trick = $false
    } else {
        if ( $_user_pwdLastset_date -gt $_user_unicodePwd_date )
        {
            $_user_fraud_trick = $true
        } else {
            $_user_fraud_trick = $false
        }
    }


    #Create an object for a nice looking output
    $_results += New-Object -TypeName psobject -Property @{
        User = $_user_distinguishedName
        Created = $_user_whenCreated
        LastPasswordChange = $_user_pwdLastset_date
        NbPaswordChange = $_user_unicodePwd_version
        ApplyFGPP = $_user_fgpp
        UserThreshold = $_user_threshold
        FraudThreshold = $_fraud_threshold
        FraudTrick = $_user_fraud_trick
        FraudRotation = $_user_fraud_rotation

    }
}


# Prepare an embeded PNG image for the report
$_ico_rep = "<img src=""data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAZCAYAAAA14t7uAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADrSURBVEhL7ZVBCsIwEEV7FS/gUhAP6c6FegPBpRs3giBeQEGliHoAabVWaszoD1QnNE0txpV9MGTSTl5DGRJPCEG/CE9KSS6ARxPjYa29+jo4+IhTMc8r8Su3ips9n8b+iRIh1Yi5qQ7BPcAqnu4iVZQyP1yMdQhOYVdEyV0VpmDnpjoEp1Bcdsc8t4pbfZ8m27MqxIi5qQ4BeG4VlwnuAUZxvbOhwSKg8Kr/42MsaLgMqNHN7hzw3CgerUP1Mo/ZPsqsATw3iuOb/cR7NkdmDSe3Kz7BtqY63bT8z8UpzrtCu6VhdwE8b7GgB+EAjr6jfR4GAAAAAElFTkSuQmCC"" />"
$style = @"

<style type="text/css">
    body
        {font-family: Helvetica, Arial, sans-serif; font-weight: normal;font-style: normal;color: black;}
    table
        {font-size:12px;color:black;width:100%;border-width: 1px;border-color: #729EA5;border-collapse: collapse;}
    th
        {font-size:12px;background-color:#66CBEA;border-width: 1px;padding: 8px;border-style: solid;border-color: #729EA5;text-align:left;}
    tr
        {background-color:#FFFFFF;}
    td
        {font-size:12px;border-width: 1px;padding: 8px;border-style: solid;border-color: #66CBEA;}
    tr:hover
        {background-color:#CAEDF8;}
    h1
        {border-bottom: 2px solid #66CBEA }
    h2
        {border-bottom: 2px solid #66CBEA }


</style>

"@
$_output_obj_html = ConvertTo-Html -Head $style -PreContent "
<h1>$_ico_rep Password fraud check</h1>

<b>Domain controller:</b> $_dc_to_use
<b>Collection:</b> $_today

"
$_output_obj_html += $_results | Select-Object User, Created, FraudTrick, FraudRotation | ConvertTo-Html -Fragment
    
# Generate the full HTML report
$_output_obj_html = $_output_obj_html | Out-String
ConvertTo-Html -Body $_output_obj_html | Out-File $_output_name
Write-Host "Output file:`t$_output_name"
# Lauch the browser and open the report
Invoke-Item $_output_name

Here is the output (note that the built-in admin could be reported if its password has never been changed this its creation).

FRAUD_3

But please: try, comment, improve and post your comments!