User provisioning and Password_Not_Required set - why?
Today's topic:
User provisioning and Password_Not_Required set - why?
In large environments, it's definitely a clever idea having some Identity Management in place that - amongst other things - takes care for user provisioning.
Many of the solutions out there - custom built or ready solutions - do utilize the Active Directory Service Interface (ADSI) or one of its wrappers (like System.DirectoryServices) to create user accounts.
As a result we very often see the provisioned user accounts having the flag ADS_UF_PASSWD_NOTREQD (0x20) in userAccountControl attribute set.
This is definitely a security risk since a domain admin can set a blank pwd on such an account which then can be used for logon.
Question: Why did this happen?
A common thinking error here is that you must first create the user and can only then set the password after the user is already in AD.
Samples in the internet suggesting to create the user and then use the ADSI method SetPassword() after having the user created – like in the following code sample:
DirectoryEntry user = null;
try
{
// create user on dedicated dc to make sure we find the user
// for post processing (DCLocator vs. replication)
DirectoryEntry ou = new DirectoryEntry("LDAP://" + dcName + "/" + ouPath,
null, null, AuthenticationTypes.Sealing);
// Add user to OU (note - we are still in the RAM - nothing happens in AD here)
user = ou.Children.Add("CN=ADSI Test", "user");
// set logon name (sAMAccountName)
user.Properties["sAMAccountName"].Value = "adsitest";
// set userPrincipalName
user.Properties["userPrincipalName"].Value = "adsitest@mfp-labs.labsetup.org";
// write from RAM to AD
user.CommitChanges();
Console.WriteLine("Success adding user");
}
catch (Exception ex)
{ Console.WriteLine("Adding user: {0} ({1})", ex.Message, ex.GetType()); }
This results in an account with
- userAccountControl = 0x222 (ADS_UF_ACCOUNTDISABLE | ADS_UF_PASSWD_NOTREQD | ADS_UF_NORMAL_ACCOUNT)
- pwdLastSet = 0 (never -> user must change password at next logon)
Now we must set the initial password by invoking the method SetPassword of the underlying IADs object (can be inspected in user.NativeObject).
SetPassword wraps the following tasks:
- Bind to the Domain Controller we used in the creation task with AuthenticationType.Sealing set.
Note – we need Kerberos enabled on the connection to be able to set the password. - Perform a replace modification request on the attribute unicodePwd against the path of the calling IADs object (NativeObject.AdsPath)
You see why we need to pre-create the user before we can use the SetPassword method?
try
{
// this is a separate call into AD - no commit required
// therefore we need the account to be present already
user.Invoke("SetPassword", "P@ssWord");
Console.WriteLine("Success setting pwd");
}
catch (Exception ex)
{ Console.WriteLine("Setting pwd: {0} ({1})", ex.Message, ex.GetType()); }
Now we have
- userAccountControl = 0x222 (ADS_UF_ACCOUNTDISABLE | ADS_UF_PASSWD_NOTREQD | ADS_UF_NORMAL_ACCOUNT)
- pwdLastSet = time of SetPassword() (user not must change password at next logon)
Saying – any domain admin may still reset the user’s password to a blank password.
Answer: Because the following task very often just vanished in oblivion we stay with ADS_UF_PASSWD_NOTREQD flag set.
We should must adjust userAccountControl and pwdLastSet to unset ADS_UF_PASSWD_NOTREQD flag and force the user to change password at next logon:
try
{
// make account enabled / without pwd_not_req flag
user.Properties["userAccountControl"].Value = 512;
// force user must change pwd at next logon
// we must pass a lon value -> cast 0 to long
user.Properties["pwdLastSet"].Value = (long)0;
// write from RAM to AD
user.CommitChanges();
Console.WriteLine("Success setting userAccountControl + pwdLastSet");
}
catch (Exception ex)
{ Console.WriteLine("Setting userAccountControl + pwdLastSet: {0} ({1})", ex.Message,
ex.GetType()); }
Finally we have a provisioned account with acceptable settings.
Question: Why is this a thinking error?
Answer: You guessed it - because we can set the password while creating a user account.
Question: But how – we have been reading above that you need to pre-create the user account before you can set the password?
Answer: Just don’t use the method SetPassword in ADSI but perform the wrapped processing in your creation task.
Saying - perform the replace modification on unicodePwd attribute while creating the user in RAM before writing into AD:
static void SDS(string dcName, string ouPath)
{
DirectoryEntry user = null;
try
{
// create user on dedicated dc to make sure we find the user
// for post processing (DCLocator vs. replication) and
// set AuthenticationTypes.Sealing to enable pwd update
DirectoryEntry ou = new DirectoryEntry("LDAP://" + dcName + "/" + ouPath,
null, null, AuthenticationTypes.Sealing);
// Add user to OU (note - we are still in the RAM - nothing happens in AD here)
user = ou.Children.Add("CN=ADSI Test", "user");
// set logon name (sAMAccountName)
user.Properties["sAMAccountName"].Value = "adsitest";
// set userPrincipalName
user.Properties["userPrincipalName"].Value = "adsitest@mfp-labs.labsetup.org";
// pass pwd with account creation call
user.Properties["unicodePwd"].Value = BuildBytePWD("P@ssW0rd");
// make account enabled / without pwd_not_req flag
user.Properties["userAccountControl"].Value = 512;
// write from RAM to AD
user.CommitChanges();
Console.WriteLine("Success adding user");
}
catch (Exception ex)
{ Console.WriteLine("Adding user: {0} ({1})", ex.Message, ex.GetType()); }
}
private static byte[] BuildBytePWD(string pwd)
{
return (Encoding.Unicode.GetBytes(String.Format("\"{0}\"", pwd)));
}
Alternatively we say good bye to ADSI and use the cool LDAP API wrapper System.DirectoryServices.Protocols:
static void SDSP(string dcName, string ouPath)
{
// open LDAP connection to DC on port 389 (-> not GC => writable AD)
LdapDirectoryIdentifier ldap_id = new LdapDirectoryIdentifier(dcName, 389, true, false);
using (LdapConnection ldap_con = new LdapConnection(ldap_id))
{
// no kerberos -> no pwd set -> (Error = WILL_NOT_PERFORM)
ldap_con.SessionOptions.Sealing = true;
// bind to the DC
ldap_con.Bind();
// list of attributes we want to pass for creation
List<DirectoryAttribute> ldap_mod = new List<DirectoryAttribute> { };
// this has to be passed - otherwise we fail
ldap_mod.Add(new DirectoryAttribute("objectClass", "user"));
// set logon name (sAMAccountName)
ldap_mod.Add(new DirectoryAttribute("sAMAccountName", "adsitest"));
// set userPrincipalName
ldap_mod.Add(new DirectoryAttribute("userPrincipalName", "adsitest@contoso.com"));
// send the password with the creation request
ldap_mod.Add(new DirectoryAttribute("unicodePwd", BuildBytePWD("P@ssW0rd")));
// set userAccountControl to Normal account
ldap_mod.Add(new DirectoryAttribute("userAccountControl", (512).ToString()));
// if we want to enforce pwd change on first logon -
// otherwise pwdLastSet is current time
ldap_mod.Add(new DirectoryAttribute("pwdLastSet", (0).ToString()));
// build add request
AddRequest areq = new AddRequest("CN=ADSI test," + ouPath, ldap_mod.ToArray());
try
{
// send reqeust to DC
AddResponse aret = (AddResponse)ldap_con.SendRequest(areq);
Console.WriteLine("Success");
}
catch (DirectoryOperationException doex)
{
Console.WriteLine("{0} ({1})", doex.Response.ErrorMessage,
doex.Response.ResultCode);
}
catch (Exception ex)
{
Console.WriteLine("{0} ({1})", ex.Message, ex.GetType());
}
}
}
private static byte[] BuildBytePWD(string pwd)
{
return (Encoding.Unicode.GetBytes(String.Format("\"{0}\"", pwd)));
}
OK - cool - now we have code avoiding provisioned user accounts with ADS_UF_PASSWD_NOTREQD flag set.
NOTE: When we try to set a password that does not meet complexity rules or minimum password length requirement of the domain, we lose a RID from the RID pool for every creation call.
NTDS has to create the user internally to be able to write the given password to unicodePwd attribute -> we need a SID.
If we fail with the password, the pre-created user object gets destroyed. The user object gets not deleted nor recycled - thus you will not be able to find info in the DB about this used RID.
To avoid RID pool exhaustion, try to build proper password creation code and limit the retry loops in your IDM code.
Additionally ask your beloved AD Admins to watch the RID pool on regularly bases.
Question: How do we get rid of this flag on already provisioned user accounts.
We do not know whether a user account has a blank password or not and performing any sort of password hash dump has a big smell of hacking.
Answer: We can rely on an Active Directory built-in security mechanism that takes care that no enabled user account with a blank password and ADS_UF_PASSWD_NOTREQD flag not set exists in AD.
When unsetting the ADS_UF_PASSWD_NOTREQD flag in userAccountControl on an enabled user account NTDS checks by comparing the user’s current password hash with the hash of a blank password. If we have a match NTDS will not perform the modification and throw an error stating that the current password does not meet complexity rules.
Same will happen when enabling a disabled user account with a blank password and ADS_UF_PASSWD_NOTREQD flag not set.
Note – NTDS does not compare plaintext passwords – it simply doesn’t know these.
Therefore, a possible mitigation would be (thanks to Kevin Bell, PFE as well, for this idea):
- find all user accounts (objectClass=user) that are of the type user and not computer (sAMAccountType=805306368) and have the ADS_UF_PASSWD_NOTREQD flag set in userAccountControl (userAccountControl:1.2.840.113556.1.4.804:=32)
- walk the retrieved list of user accounts and unset ADS_UF_PASSWD_NOTREQD flag
- if we fail with a password complexity error - report this user accounts as users with blank passwords
PoSh sample performing the above described tasks:
$fishyusers = Get-ADUser -LDAPFilter "(&(objectClass=user)(sAMAccountType=805306368)" +
"(userAccountControl:1.2.840.113556.1.4.804:=32))"
foreach ($fish in $fishyusers)
{
try
{
Set-ADAccountControl -Identity $fish -PasswordNotRequired $false
Write-Host "secured:" $fish.DistinguishedName
}
catch [Microsoft.ActiveDirectory.Management.ADPasswordComplexityException]
{
Write-Host "blank pwd for:" $fish.DistinguishedName
// report into a log file?
}
catch
{
Write-Host $_.Exception.ToString()
}
}
What are the risks to do this in a production environment (?):
- user accounts on which we could unset the flag successfully have no blank passwords and can go on working - but are more secure
- user accounts on which we failed with password complexity error did not get modified - thus they can still work - but can be contacted and requested to set a complex password.
- depending on the amount of user accounts that have been modified we may have increased replication traffic after the modifications.
-> It could be a clever idea to do this out of work hours.
Hope you had some fun reading and wish fun with testing.
History:
4/13/2016: added NOTE about possible excessive RID pool consumption.
All the best
Michael
PFE | Have keyboard. Will travel.