Resetting passwords honoring password history (or what's happening under the hood when changing / resetting passwords)

Todays topic:

Resetting passwords honoring password history

(or what's happening under the hood when changing / resetting passwords)

You may have already came across the task to programmatically change or reset passwords on user accounts in Active Directory. Thanks to the the ChangePassword() and SetPassword() macros of the Active Directory Service Interface (ADSI) implementation this is an easy and straight forward coding and in most cases you need not take care about what's happening on the Domain Controller performing the password handling for you.
Anyhow it still may come in handy knowing how this is processed from the Active Directory service (NTDS) on a DC – especially when we want to accomplish what's mentioned in the headline (Resetting passwords honoring password history).

First of all – and for the sake of completeness – let's list the usage of the two ADSI macros ChangePassword() and SetPassword() in VBS and .Net (note –.Net System.DirectoryService namespace is wrapping ADSI):

VBS:

Set IADsUser = GetObject("LDAP://CN=TheCN,OU=TheOU,DC=contoso,DC=com")

IADsUser.ChangePassword "0ldPa55W0rd", "N3wPa55W0rd"

        IADsUser.SetPassword "N3wPa55W0rd"

.Net (System.DirectoryServices):

DirectoryEntry IADsUser = new DirectoryEntry("LDAP://CN=TheCN,OU=TheOU,DC=contoso,DC=com");

IADsUser.Invoke("ChangePassword", new object[] { "0ldPa55W0rd", "N3wPa55W0rd" });

IADsUser.Invoke("SetPassword", new object[] { "N3wPa55W0rd" });

Now let's have a look at what's actually happening when calling the ADSI macros:

The password change or reset call is actually an attribute modification request against the unicodePwd attribute of the user account which requests the NTDS service to handle the incoming modification request appropriately (note – the password is NOT stored in this attribute).

ChangePassword():

When changing the password we send a modification request to our directory connection, that was established against a domain controller when 'connecting' the user object, that contains:

  • the distinguishedName (internal directory path - in our sample "CN=TheCN,OU=TheOU,DC=contoso,DC=com")
  • the name of the attribute to modify (unicodePwd)
  • a delete attribute value modification containing the old password
  • an add attribute value modification containing the new password

When the modification request arrives at the domain controller the NTDS service does the following:

  • check whether the hash of the old password to be deleted is in the list of remembered password hashes (this is the verification part of the old password) -> if so proceed, if not return an error "password incorrect"
  • check whether the new password meets password policy rules (like comlpexity, password history, password length) -> if so proceed, if not return error "passwort does not meet pwd complexity rules"

Thus we see - the password rule checks are done when performing an add operation to the unicodPwd attribute.

SetPassword():

Resetting the password sends a modification request to our directory connection containing:

  • the distinguishedName (internal directory path - in our sample "CN=TheCN,OU=TheOU,DC=contoso,DC=com")
  • the name of the attribute to modify (unicodePwd)
  • a replace attribute value modification containing the new password

The only password rule checks that are done while proceeding a replace operation are password complexity and password length – password history is not checked here. Why? Because we only check the existance of a value while modifying an attribute when deleting or adding values.

Reset password honoring password history:

Knowing the above described functionalities this sounds easy – just send an add operation with the new password -  unfortunately you cannot send an add operation to the unicodePwd attribute without a preceding delete operation (means you have to know the old password). But we have an Identity Management solution in place that should be able to reset passwords and not reuse previously set passwords + the helpdesk will not and should not know the old password – so how can this be achieved?

We have to say good bye to the handy ADSI implementation and code closer to the LDAP APIs (no worries – we will still use managed code!). Since .Net 2.0 we have the namespace System.DirectoryServices.Protocols in place, wrapping the LDAP APIs directly.

See following illustration how the various implementations are talking to LDAP:

Here we can perform our modification request ourselves and control what has to be send and how this has to be handled.

When sending requests to a directory connection we can additionally send Extended Controls with the request (find a list of controls here: https://msdn.microsoft.com/en-us/library/cc223320.aspx) – you may know one from LDAP queries when using paged queries. In this case ADSI is sending a search request with the extended control for paged search to the DC.
If you check the list in the above link you will find an Extended Control called LDAP_SERVER_POLICY_HINTS_OID (1.2.840.113556.1.4.2066) with the following description: "Used with an LDAP operation to enforce password history policies during password set. ".

Cool – we have all there what we need – unfortunately not necessarily.
If you check the answer of an UDP call against rootDSE in your domain (ex: ldp.exe -> Connect) you will see a list of OIDs in the attribute supportedControl.
Depending on  the OS version of your DCs the POLICY_HINTS OID may be missing.
On DCs with OS Windows Server 2008 (R2) it's not there by default. To enable the usage of this Extended Control on Windows Server 2008 (R2) DCs  you must first introduce the OID and it's usage to the DCs by applying the following hotfix: https://support.microsoft.com/?id=2386717 .

Since Windows Server 2012 we do have a new OID for the Extended Control LDAP_SERVER_POLICY_HINTS_OID (1.2.840.113556.1.4.2239). The OID 1.2.840.113556.1.4.2066 is still valid on Windows Server 2012 (R2) ADs but it's now called LDAP_SERVER_POLICY_HINTS_DEPRECATED_OID.

Suggest you check the supportedControl attribute of a rootDSE call and check, whether you find LDAP_SERVER_POLICY_HINTS_OID = 1.2.840.113556.1.4.2239. If so you should use the new OID.

If one of the above OIDs is present we are now able to send our modification request containing the the Extended Control LDAP_SERVER_POLICY_HINTS_OID with the value 0x1 to honor password history when resetting passwords.

!Note: There are several things to keep in mind when establishing an LdapConnection in code:

  • The connection must be encrypted, either by Kerberos or SecureSocketLayer.
    If you chose SSL as encryption method make sure to use port 636 for establishing the LdapConnection.
  • Sending the extended control for honoring the password history is only functional if we ensure that LDAP protocol version 3 is used.
  • Setting authentication Type of the LdapConnection to Basic Authentication will cause an internal fallback to LDAP protocol version 2
    -> thus the extended control for honoring the password history will just be dropped and the password history will not be honored unless we enforce protocol version 3.

Sample Code:

 using System.ComponentModel;
using System.DirectoryServices.Protocols;
using System.Net;
using System.Text;

namespace CodingFromTheField.PwdChanger
{
    class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                string dn = "CN=TheCN,OU=TheOU,DC=contoso,DC=com";

                bool usebasicauth = false;

                int port = 389;

                if (usebasicauth)
                { port = 636; }

                /* initialize LdapConnection which inherites from DirectoryConnection  -
                 * DirectoryConnection cannot be initialized passing a directory to connect to */
                using (LdapConnection ldapCon = new LdapConnection("contoso.com:" + port.ToString()))
                {
                    if (!usebasicauth)
                    {
                        // enable Kerberos encryption
                        ldapCon.SessionOptions.Sealing = true;
                    }

                    else
                    {
                        // enable SSL encryption
                        ldapCon.SessionOptions.SecureSocketLayer = true;

                        // set authentication type to Basic Authentication
                        ldapCon.AuthType = AuthType.Basic;

                        // pass credentials
                        ldapCon.Credential = new NetworkCredential("theadmin", "thepassword", "contoso");
                    }

                    // enforce LDAP protocol version 3 usage before binding LdapConnection
                    ldapCon.SessionOptions.ProtocolVersion = 3;

                    //bind
                    ldapCon.Bind();

                    // change pwd
                    PasswordChanger(ldapCon,
                                    dn,
                                    pwdDepricate: @"0ldPa55W0rd",
                                    pwdSet: @"N3wPa55W0rdH15t0ryT3st");

                    // reset pwd without utilizing pwd history
                    PasswordChanger(ldapCon,
                                    dn,
                                    pwdSet: @"N3wP@55W0rdH15t0ryT3st");

                    /* ensure protocol version 3 usage - 
                    if protocol version 2 -> do not try to reset the password honoring password history - 
                    the password will be set anyways */
                    if (ldapCon.SessionOptions.ProtocolVersion == 3)
                    {
                        // reset pwd utilizing pwd history
                        PasswordChanger(ldapCon,
                                        dn,
                                        pwdSet: @"N3wPa55W0rdH15t0ryT3st",
                                        enforceHistory: true);
                    }
                }
            }

            catch (Exception ex)
            { Console.WriteLine(ex.ToString()); }

            Console.WriteLine("Press any key");

            Console.ReadKey();
        }

        /// <summary>
        /// Change or reset pwds on given object
        /// </summary>
        /// <param name="dcCon">established DirectoryConnection</param>
        /// <param name="distinguishedName">path to the object</param>
        /// <param name="pwdDepricate">when changing pwds - pass the current pwd in here</param>
        /// <param name="pwdSet">new pwd to be set</param>
        /// <param name="enforceHistory">when resetting pwd -> should we utilize exetended control
        /// for pwd history usage</param>
        /// <param name="useOldOID">use depricated OID or new OID</param>
        private static void PasswordChanger(LdapConnection ldapCon,
                                            string distinguishedName,
                                            string pwdDepricate = null,
                                            string pwdSet = null,
                                            bool enforceHistory = false,
                                            bool useOldOID = false)
        {
            bool letsgo = false;


            // the 'unicodePWD' attribute is used to handle pwd handling requests
            string attribute = "unicodePwd";

            // our modification control
            DirectoryAttributeModification[] damList = null;

            // the modifiy request
            ModifyRequest mrCall = null;

            //do we have an old and a new pwd -> change pwd
            if (!String.IsNullOrEmpty(pwdDepricate) && !String.IsNullOrEmpty(pwdSet))
            {
                // modification control for the delete operation
                DirectoryAttributeModification damDelete = new DirectoryAttributeModification();

                // attribute to handle
                damDelete.Name = attribute;

                // value to be send with the request
                damDelete.Add(BuildBytePWD(pwdDepricate));

                // this is a delete operation
                damDelete.Operation = DirectoryAttributeOperation.Delete;

                // modification control for the add operation
                DirectoryAttributeModification damAdd = new DirectoryAttributeModification();

                // attribute to handle
                damAdd.Name = attribute;

                // value to be send with the request
                damAdd.Add(BuildBytePWD(pwdSet));

                // this is an add operation
                damAdd.Operation = DirectoryAttributeOperation.Add;

                // combine modification controls
                damList = new DirectoryAttributeModification[] { damDelete, damAdd };

                // init modify request
                mrCall = new ModifyRequest(distinguishedName, damList);

                // we do have something to handle
                letsgo = true;
            }

            //do we have a pwd to set -> set pwd
            else if (!String.IsNullOrEmpty(pwdSet))
            {
                // modification control for the replace operation
                DirectoryAttributeModification damReplace = new DirectoryAttributeModification();

                // attribute to handle
                damReplace.Name = attribute;

                // value to be send with the request
                damReplace.Add(BuildBytePWD(pwdSet));

                // this is a replace operation
                damReplace.Operation = DirectoryAttributeOperation.Replace;

                // combine modification controls
                damList = new DirectoryAttributeModification[] { damReplace };

                // init modify request
                mrCall = new ModifyRequest(distinguishedName, damList);

                // should we utilize pwd history on the pwd reset?
                if (enforceHistory)
                {
                    // the actual extended control OID                     
                    string LDAP_SERVER_POLICY_HINTS_OID = useOldOID ? "1.2.840.113556.1.4.2066" : 
                                                                      "1.2.840.113556.1.4.2239";

                    // build value utilizing berconverter
                    byte[] value = BerConverter.Encode("{i}", new object[] { 0x1 });

                    // init extended control
                    DirectoryControl pwdHistory = new DirectoryControl(LDAP_SERVER_POLICY_HINTS_OID, 
                                                                       value, false, true);

                    // add extended control to modify request
                    mrCall.Controls.Add(pwdHistory);
                }

                // we do have something to handle
                letsgo = true;
            }

            // something to be handled?
            if (letsgo)
            {
                DirectoryResponse drResult = null;

                string msg = "";

                try
                {
                    /* send the request into the DirectoryConnection
                     * and receive the response */
                    drResult = ldapCon.SendRequest(mrCall);

                    // display result code
                    msg = TranslateEx(drResult, null, distinguishedName);
                }

                catch (DirectoryOperationException doex)
                { msg = TranslateEx(drResult, doex, distinguishedName); }

                catch (Exception ex)
                { msg = TranslateEx(drResult, ex, distinguishedName); }

                Console.WriteLine(msg);
            }
        }

        /// <summary>
        /// build byte array from string pwd
        /// </summary>
        /// <param name="pwd">pwd string</param>
        /// <returns>byte array</returns>
        private static byte[] BuildBytePWD(string pwd)
        {
            return (Encoding.Unicode.GetBytes(String.Format("\"{0}\"", pwd)));
        }

        /// <summary>
        /// decode exception thrown
        /// </summary>
        /// <param name="dr">Directoryresponse from the SendRequest call</param>
        /// <param name="ex">the exception to decode</param>
        /// <param name="dn">the distinguishedName of the object we touched</param>
        /// <returns></returns>
        private static string TranslateEx(DirectoryResponse dr, Exception ex, string dn)
        {
            string ret = "";

            bool success = false;

            if (dr != null)
            { success = (dr.ResultCode == ResultCode.Success) ? true : false; }

            if (success)
            { ret = String.Format("Update pwd result: {0} \n\tfor {1}\n", 
                                  dr.ResultCode.ToString(), dn); }

            else if (!success && (ex != null))
            {
                if (ex is DirectoryOperationException)
                {

                    DirectoryOperationException doex = (DirectoryOperationException)ex;

                    ret = String.Format("Update pwd result: {0} \n\tfor {1}\n", 
                                        doex.Response.ResultCode.ToString(), dn);

                    string hex = doex.Response.ErrorMessage.Split(new char[] { ':' })[0];

                    int lex = 0;

                    if (int.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out lex))
                    {
                        try
                        {
                            Win32Exception wex = new Win32Exception(lex);

                            ret = ret + String.Format("{0} ({1}) [{2}]\n", 
                                                      wex.Message, doex.Response.ErrorMessage, doex.Message);
                        }

                        catch
                        { ret = ret + String.Format("{0} [{1}]\n", 
                                                    doex.Response.ErrorMessage, doex.Message); }
                    }

                    else
                    { ret = ret + String.Format("{0} [{1}]\n", 
                                                doex.Response.ErrorMessage, doex.Message); }
                }

                else
                {
                    ret = String.Format("Update pwd result: Error \n\tfor {0}\n", dn);

                    ret = ret + String.Format("{0}\n", ex.Message);
                }
            }

            return ret;
        }
    }
}

 

Hope you had some fun reading and wish fun with testing.

9/7/2015

There were several queries regarding AD LDS and the above code.
Unfortunately the code path handling this in NTDS is not implemented in AD LDS -> no chance to implement this for AD LDS with built in mechanisms.

4/30/2016

Added !Note section above sample code.
Updated sample code to honor !Note.

 

All the best

Michael

PFE | Have keyboard. Will travel.