0

We have an application that validate user credentials with our internal ActiveDirectory domain. To do so, it uses the PrincipalContext::ValidateCredentials method from the .NET Framework.

While investigating another issue, we discovered that this method return true even when the password is expired. This result in users being able to access one of our internet systems despite having an expired password for months, even years in some cases. This seems strange, and a severe security flaw that we need to fix now that we're aware of it.

I tried looking up online about this behavior, but so far I found nothing. As far as I could tell, this method is really supposed to reject credentials if there is anything wrong with the account. For example, it does return false when the account is locked.

I doubt that this a bug in the ValidateCredential method itself. Its been around too long for that. It's fairly simple to use, so I don't think we screwed up here. Here's our code :

using (PrincipalContext context = new PrincipalContext(ContextType.Domain, domainName))
{
    bool valide = context.ValidateCredentials(userName, passWord);
    // Remaining code omitted
}

So, what could be happening here? What could cause ValidateCredentials to accept expired password?

Dantre
  • 43
  • 3
  • Here is similar issue from many years ago: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/f3150686-c15f-48ea-9c16-fb09304aa3fb/explanation-of-principalcontextvalidatecredentials-behaviour-over-ldaps?forum=netfxbcl. So it might be broken after all. – Evk Nov 17 '22 at 17:11
  • `ValidateCredentials` [uses `LdapConnection` in behind](https://github.com/dotnet/runtime/blob/main/src/libraries/System.DirectoryServices.AccountManagement/src/System/DirectoryServices/AccountManagement/Context.cs#L222). Try using `LdapConnection` directly and see if you get the same results. There's example code here: https://stackoverflow.com/a/11033489/1202807 – Gabriel Luci Nov 17 '22 at 18:48
  • The link Evk gave suggests that it may only happen when using SSL, so that's something else you can check directly with `LdapConnection`. (specify port 636 to use SSL) – Gabriel Luci Nov 17 '22 at 18:50
  • Using LdapConnection directly, like in the example posted by @GabrielLuci, works : the credentials are rejected if the password is expired. I was actually testing with an account where the password is not only expired, but required to change, but I still see different behaviors between the two methods. – Dantre Nov 17 '22 at 19:23
  • Have you tried using SSL with `LdapConnection` and see if it's any different? – Gabriel Luci Nov 17 '22 at 19:30
  • And note that an expired password and password required to change are two different scenarios that cannot happen at the same time. If the password is expired, then the [`pwdLastSet`](https://learn.microsoft.com/en-us/windows/win32/adschema/a-pwdlastset) attribute has a value and it exceeds the time the domain allows. But if the user is forced to change their password on next login, then `pwdLastSet` is `0`. – Gabriel Luci Nov 17 '22 at 19:32
  • I tested using LdapConnection while specifing the port, with port 636 to use SSL, and then with port 389 to ignore SSL. Both times, the credentials were rejected, as it should be. So, SSL doesn't seems to be a factor here. – Dantre Nov 17 '22 at 19:42
  • @GabrielLuci They might be different scenarios, but both behave the same in this case. I personaly have only tested with a password set to change, though, and in this case ValidateCredentials accepts the credentials while LdapConnection rejects them. We do have confirmed that users can access our website with an expired password so I know that ValidateCredentials haven't rejected them like it should have. – Dantre Nov 17 '22 at 19:50

2 Answers2

0

The source code shows that it tries both simple bind over SSL and Negotiate. It is likely one of the authentication methods that's allowing it for some reason. Why it would allow it, I have no idea.

You could play around with specifying different ContextOptions in the constructor to PrincipalContext. For example (since you said that SSL performs the way you expect):

using (PrincipalContext context = new PrincipalContext(ContextType.Domain, domainNam 
       , ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind))
{
    bool valide = context.ValidateCredentials(userName, passWord);
    // Remaining code omitted
}

But if you have to change your code anyway, you may think about using LdapConnection directly, since the exception will show you the reason why it fails, as described in that example I linked to.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Thanks for the input. I'll experiment with these options and see how it goes. I'll also check the source code and do some more tests based on what it does. Since using LdapConnection directly works as expected and ValidateCredentials is using it, there mus be something in there that's causing this. I'll report on my findings in a few days. – Dantre Nov 18 '22 at 14:55
0

As it turns out, and it took digging into the .NET framework source code to find out why, the reason that ValidateCredentials allows expired password is because it tries simple bind over SSL and Negotiate, but when it does a simple bind it activate support for fast concurrent binds, which apparently only validate that the user has a valid enabled account and password.

Here an excerpt from the Active Directory Cookbook from O'Reilly about fast concurrent binds :

Concurrent binding, unlike simple binding, does not generate a security token or determine a user’s group memberships during the authentication process. It only determines if the authenticating user has a valid enabled account and password, which makes it much faster than a typical bind. Concurrent binding is implemented as a session option that is set after you establish a connection to a domain controller, but before any bind attempts are made. After the option has been set, any bind attempt made with the connection will be a concurrent bind.

Yeah, I know this book is very old, but I presume this part is still pretty much valid. The tests I did seems to confirm that, as ValidateCredentials would accept expired passwords, but not locked accounts for example.

So, how to prevent this? Pretty simple, actually :

using (PrincipalContext context = new PrincipalContext(ContextType.Domain, domainName))
{
    bool valide = context.ValidateCredentials(userName, passWord, ContextOptions.Negotiate | ContextOptions.Signing | ContextOptions.Sealing);
    // Remaining code omitted
}

The trick is to specify connection options, but in the ValidateCredentials method and NOT the PrincipalContext constructor, otherwise the options are ignored by ValidateCredentials and it will attempt the fast bind with the problematic behavior.

Thanks to Gabriel Luci for pointing me in the right direction.

Dantre
  • 43
  • 3