What is the native equivalent of PrincipalContext.ValidateCredentials? - winapi

What is the native way to validate a set of user domain credentials (username, password, domain) against that domain?
In other words, i am looking for the native equivalent of:
Boolean ValidateCredentials(String username, String password, String domain)
{
// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain))
{
// validate the credentials
return pc.ValidateCredentials(username, password)
}
}
ValidateCredentials("iboyd", "Tr0ub4dor&3", "contoso");
Hasn't this been asked, and answered, to death?
No! This question is asked, a lot. Three of those times by me. But the more you dig into it, the more you realize the accepted answers are incorrect.
Microsoft managed to solve it in .NET with the PrincipalContext class added in .NET 3.5. And the PrincipalContext is nothing magical. Underneath it uses the flat C-style ldap API. But trying to reverse engineer the code from ILSpy is not working out. And referencesource notwithstanding, Microsoft still keeps large portions of the .NET Framework class library source code secret.
What have you tried?
Method 1: Just use LogonUser
I cannot use LogonUser. LogonUser works only when your machine is either on the domain you're validating (e.g. contoso) or your domain trusts the domain you're validating. In other words, if there is a domain controller out there for the contoso.test domain then:
LogonUser("iboyd", "contoso.test", "Tr0ub4dor&3",
LOGON32_LOGON_NETWORK, "Negotiate", ref token);
will fail with error:
1326 (Logon failure: unknown user name or bad password)
That's because this domain i specify is not my own domain, or a domain i trust.
C# PrincipalContext doesn't suffer from this problem.
Method 2: Just use the SSPI (Security Support Provider Interface)
The SSPI is what LogonUser uses internally. The short answer is that it fails for the same reason that LogonUser does: Windows will not trust credentials from an untrusted domain.
The code is pretty long to provide an example of. The psuedo-code jist is:
QuerySecurityPackageInfo("Negotiate");
// Prepare client message (negotiate)
AcquireCredentialsHandle(....); //for the client
InitializeSecurityContext(...); //on the returned client handle
CompleteAuthToken(...); //on the client context
// Prepare server message (challenge).
AcquireCredentialsHandle(...); //for the server
AcceptSecurityContext(...); //on the returned server handle
CompleteAuthToken(...); //on the server context
// Prepare client message (authenticate).
AcquireCredentialsHandle(....); //for the client
InitializeSecurityContext(...); //on the returned client handle
CompleteAuthToken(...); //on the client context
// Prepare server message (authentication).
AcquireCredentialsHandle(...); //for the server
AcceptSecurityContext(...); //on the returned server handle
CompleteAuthToken(...); //on the server context
This code works great if your machine is joined to the domain you're validating credentials. But as soon as you try to validate a set of domain credentails from a foreign domain: it fails.
C# PrincipalContext doesn't suffer from this problem.
Method 3: Just use LDAP's AdsGetObject
Some might suggest using AdsGetObject.
AdsGetObject("LDAP://CN=iboyd,DC=contoso,DC=test");
That's a red-herring, because AdsGetObject supports no way to pass username/password:
HRESULT ADsGetObject(
_In_ LPCWSTR lpszPathName,
_In_ REFIID riid,
_Out_ VOID **ppObject
);
Instead you will be simply asking about a user.
Perhaps you meant AdsOpenObject:
HRESULT ADsOpenObject(
_In_ LPCWSTR lpszPathName,
_In_ LPCWSTR lpszUserName,
_In_ LPCWSTR lpszPassword,
_In_ DWORD dwReserved,
_In_ REFIID riid,
_Out_ VOID **ppObject
);
where you can specify credentials to connect as.
C# PrincipalContext doesn't suffer from this problem.
Method 4: Just use AdsOpenObject
Some might suggest using AdsOpenObject:
String path = "LDAP://CN=iboyd,DC=contoso,DC=test"
AdsOpenObject(path, "iboyd", "Tr0ub4dor&3", 0, IADs, ref ads);
Setting aside the fact that the path i constructed is invalid, setting aside the fact that there is no way to construct a valid LDAP path when you only know:
{username}
{password}
{domain}
that is because
LDAP://CN={username},DC={domain}
is not a valid LDAP path for any user.
Notwithstanding the LDAP path conundrum, the fundamental issue is that trying to query LDAP. That is wrong.
We want to validate a user's AD credentials.
Any time we attempt to query an LDAP server, we will fail if the user does not have permission to query LDAP - even though their credentials are valid.
When you pass credentials to AdsOpenObject they are used to specify who you want to connect as. Once you connect, you will then perform a query against LDAP. When you don't have permission to query LDAP, the AdsOpenObject will fail.
What's even more maddening is that even if you do have permission to query for users in LDAP, you're still needlessly performing a query of LDAP - an expensive operation.
C# PrincipalContext doesn't suffer from this problem.
Method 5: Use ADO with the ADsDSOObject provider
There are many who simply use the ADsDSOObject OLEDB provider with ADO to query LDAP. This solves the issue of having to come up with the correct LDAP path - you don't have to know the LDAP path for the user
String sql =
'SELECT userAccountControl FROM "LDAP://DC=contoso,DC=test"
'WHERE objectClass="user"
'and sAMAccountName = "iboyd"';
String connectionString = "Provider=ADsDSOObject;Password=Tr0ub4dor&3;
User ID=iboyd;Encrypt Password=True;Mode=Read;
Bind Flags=0;ADSI Flag=-2147483648";
Connection conn = new ADODB.Connection();
conn.ConnectionString = connectionString;
conn.Open();
IRecordset rs = conn.Execute(sql);
That works; it solves the problem of not knowing a user's LDAP path. But it doesn't solve the issue of if you don't have permission to query AD, then it fails.
Plus there's the issue that it is querying Active Directory, when it should be validating credentials.
C# PrincipalContext doesn't suffer from this problem.
Method 6: Just use PrincipalContext.ValidateCredentials
The .NET 3.5 class PrincipalContext has that lets you validate credentials knowing only:
Username
Password
Domain Name
You don't need to know the name or IP of the AD server. You don't need to construct any LDAP paths. And most importantly, you don't need permission to query Active Directory - it just works.
I tried digging down into the source code using ILSpy, but it gets harry fast:
ValidateCredentials
CredentialValidator.Validate
BindLdap
new LdapDirectoryIdentifier
new LdapConnection
ldapConnection.SessionOptions.FastConcurrentBind();
lockedLdapBind
Bind
With a lot of presumably important code around it. There's a lot of ups, and downs, with dependency injection, and functions too little - all the normal difficulties you get with overly complicated code structure. The complexity is on par with programming the SSPI. Nobody understands SSPI code, and i already wrote code that calls it!
Note: This question doesn't ask how to validate local credentials, as opposed to local credentials. Nor does it ask how to do both. In this case i'm simply asking how to do what is already available in the .NET world but in the native world.
Unfortunately:
System.Security.DirectoryServices.AccountManagement.PrincipalContext
was not exposed though a COM-callable wrapper:
And now that i've spent two and a half hours typing in this question: it is time to go home. Lets see if i get closed between now and tomorrow morning.

What about the LogonUser functions from advapi.dll, e.g. LogonUserA:
BOOL LogonUserA(
[in] LPCSTR lpszUsername,
[in, optional] LPCSTR lpszDomain,
[in, optional] LPCSTR lpszPassword,
[in] DWORD dwLogonType,
[in] DWORD dwLogonProvider,
[out] PHANDLE phToken
);
LogonUser(L"LocalService", L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &hToken)
This logs in against the AD on the local machine.

Related

Does the credential manager have a limit for the number of credentials stored?

I'm using CredWriteW to store some credentials and persisting through the user session. As we will have to store the credentials for lots of different accounts, I'm wondering: is there some kind of limit as to how many credentials can be stored on the credential manager?
I found this doc Credential limit per app | Microsoft Learn , but it's not clear whether it applies only to RDP, or to Credential Manager in general, or to something else. I've tried searching for this limit, but everything seems to point back to that same doc.
This is the code for reference:
CREDENTIAL credential = {0};
credential.Type = CRED_TYPE_DOMAIN_PASSWORD;
credential.TargetName = account;
credential.CredentialBlobSize = credentialBlobSize;
credential.CredentialBlob = (LPBYTE)password;
credential.Persist = CRED_PERSIST_SESSION;
credential.UserName = (LPWSTR)userName;
// Write the credential in the user space
if (!CredWriteW(&credential, 0))
{
// ...
}
This API indirectly mentioned/used in your RDP/Remote Deskop link is called "Vault", it's not the same API that the one used by CredWrite.
Vault is an undocumented API. See here for example on SO: Reverse engineering the function arguments of VaultRemoveItem or here on github's mimikatz
CredWrite is not documented to have any reasonable limit, here are 100 credentials I've just created with it:

Stop Auto Logon after failure - Custom Credential Provider Windows

I've taken up Widows Samples on Credential Providers and have built one using them as a reference. I'm able to log in seamlessly, wither by giving username and password manually. I've set
CustomCredential::SetSelected (__out BOOL* pbAutoLogon) {
*pbAutoLogon = TRUE; // FALSE;
return S_OK;
}
Now, AutoLogon, when the Tile is selected, is happening seamlessly.
As a test case, I changed the password, and as expected the Login fails. After failure, an error message appears and when I click OK ( this is the only option ), the credentials are resubmitted for a retry. How do can we stop this behavior? Which method gets called after the authentication failure?
I've handled ReportResult() but that did not help.
Thanks in advance.
It's been a while - but I believe GetSerialization() is called to return serialized credentials to LogonUI. You need to implement this as well. The credential provider samples I think have working code for KERB_INTERACTIVE_UNLOCK_LOGON.
You can also change *pbAutoLogon conditionally in SetSelected() - I do this in my credential provider depending on certain results.
ReportResult() would be called after GetSerialization returns its result to LogonUI. Inside ReportResult() you could do things like clear the password box (which is done in the samples code.)
If you're not returning serialized credentials in GetSerialization then I think you might get the kind of error you listed in your original post. In the credential samples KerbInteractiveUnlockLogonPack() is called in GetSerialization() and this is what 'logs' the user in effectively.
The error seems specific - 'the user has not been granted the requested logon type' so maybe it has something to do with the rights of the user you're testing with.
If you are using remote desktop, make sure your users are members of the right groups to be able to login (https://support.jumpdesktop.com/hc/en-us/articles/216424183-General-RDP-You-must-be-granted-the-Allow-log-on-through-the-Terminal-or-Remote-Desktop-Services-Right-) or if it is a regular user that it is allowed interactive login.
Also - SetSelected() Gets called when your credential provider is clicked on - I'm not sure if it gets called after every logon attempt or not (my guess is that it doesn't.) In my credential provider I am using a custom logon dialog that I show using SetSelected().

How to validate user's cached credentials against a domain?

When you logon to Windows, your credentials are cached. This allows you to use single sign-on. If you were to then browse to another computer, e.g.:
\\hydrogen
you would not be prompted for credentials.
Windows will take your:
current username
(hashed) password
and attempt to authenticate you automatically. The interesting thing is that this even works if your workstation is not on the domain. Windows will automatically use your username and password when connecting to the server. And if your:
local username/password matches a
domain username/password
you are automatically let in.
Pretty picture:
This is called Single Sign-On. You sign-on to Windows once, and your cached credentials are used to validate you as you use other things on the network.
Browsers also do this
Chrome, Internet Explorer, and Firefox also do a variation of this. If you need to login to a web-site, and the server supports Negotiation authorization, the server will send you back an indication that you should try the user's Windows/Domain/Kerberos credentials:
HTTP/1.1 401 Unauthorized
Server: Microsoft-IIS/7.5
WWW-Authenticate: Negotiate
Date: Thu, 09 Jul 2015 14:35:58 GMT
Content-Length: 0
Chrome will then take your cached credentials, and (after some intermediate magic) forward them to the web-server:
GET http://hr.woodglue.com HTTP/1.1
Host: hr.woodglue.com
Authorization: Negotiate YIIFzwYGKwYBBQUCoIIFwzCCBb....
Microsoft talks about this mechanism in Internet Explorer in the old article:
HTTP-Based Cross-Platform Authentication by Using the Negotiate Protocol
The client calls AcquireCredentialsHandle() and InitializeSecurityContext() with the SPN to build the Security Context that requests the session ticket from the TGS/KDC.
You validate against a domain, not servers
A final point i want to mention is that servers, web-servers, workstations, file servers, don't validate credentials against a server, they validate against a domain controller. You have a nebulous forest of many domain servers, and one of them handles your request.
In other words, you don't validate credentials against:
\\uranium (a domain controller on the woodglue.com domain)
you validate credentials against:
the woodglue.com domain
We have the important concept that someone can:
validate your cached credentials
against a domain
without having to enter a username or password
How can I do this?
How can I validate someone's cached credentials? How can I:
validate the user's cached credentials
against a domain
without the user having to enter a username or password (i.e. using the Windows cached credentials)
The important point is that is don't know (or care):
if the user's machine is joined to a domain
if the user's machine is joined to a workgroup
if the user's machine is joined to the woodglue.com or some other domain (e.g. superglue.com)
the names of the servers that power the superglue.com domain
I don't know how to do it.
I don't know what API technologies are involved.
I know there is an API called the Security Support Provider Interface (SSPI). This is what powers WWW-Authenticate: Negotiate (although i don't know if it is what powers SMB from a non-domain joined PC).
Chromium's open-source might be able to start us off with a snippet from their http_auth_sspi_win.cc. They use the SSPI function AcquireCredentialsHandle:
int AcquireDefaultCredentials(CredHandle* cred)
{
TimeStamp expiry;
// Pass the username/password to get the credentials handle.
// Note: Since the 5th argument is NULL, it uses the default
// cached credentials for the logged in user, which can be used
// for a single sign-on.
SECURITY_STATUS status = library->AcquireCredentialsHandle(
NULL, // pszPrincipal
const_cast<SEC_WCHAR*>(package), // pszPackage
SECPKG_CRED_OUTBOUND, // fCredentialUse
NULL, // pvLogonID
NULL, // pAuthData
NULL, // pGetKeyFn (not used)
NULL, // pvGetKeyArgument (not used)
cred, // phCredential
&expiry); // ptsExpiry
}
Pass the username/password to get the credentials handle.
Note: Since the 5th argument is NULL, it uses the default cached credentials for the logged in user, which can be used for a single sign-on.
This "outbound" call to AcquireCredentialsHandle is followed up by a call to InitializeSecurityContext. The idea is that InitializeSecurityContext generates an opaque blob that represents the client.
You can then perform a parallel set of calls:
"inbound" call to AcquireCredentialsHandle
call AcceptSecurityContext, passing the blob returned earlier from InitializeSecurityContext
To steal rehost Daniel Doubrovkine's excellent image:
Note: In this case "client" and "server" are used to refer to context of producer and consumer. In my case the "client" and "server" are on the same machine.
But this line of "shows research effort" falls apart because i don't see anywhere in InitializeSecurityContext where i can specify woodglue.com as the domain to validate against.
I know that InitializeSecurityContext contacts a kerberos server, and obtains a "ticket" blob. That ticket blob is then passed to the "server" through AcceptSecurityContext. Sometimes the blob can be passed over a network; in my case it is passed around in memory on the same machine.
But i don't see how to specify the domain server that it should be contacting for that ticket.
Not to imply that SSPI is at all useful to solve my problem. It's just "research effort".
Older Research Effort
What TargetName to use when calling InitializeSecurityContext (Negotiate)?
How to validate domain credentials (from native code)?
Validate a user's password using the hash?
Win32: How to validate credentials against Active Directory?
How to perform Windows Authentication?
Of course, during all this, if the cached credentials are not valid on the specified domain, i would have to prompt the user for a username and password. But usernames and passwords are evil, the bane of computing, and i want to avoid them.
i'm using native code; not C#.

QueryServiceObjectSecurity fails with access denied error

QueryServiceObjectSecurity call is failing with access denied error, i am not quite able to figure out why. I create service and then try to update the permissions for it. Interestingly once the call fails service is created and if i reexecute code, it detects existing service and attaches handle and then this call works fine, then why it fails for the first time? I am new to windows services, is there like during first time execution, service is created but SCM db is not updated before i query object security?
Code snippet is below
Service creation:
managerHandle.Attach(::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS));
serviceHandle.Attach(::CreateService(managerHandle, serviceName, serviceDisplayName,
SERVICE_CHANGE_CONFIG | SERVICE_START | SERVICE_QUERY_STATUS | SERVICE_INTERROGATE,
serviceType, startCode, errorControl, path,
groupName, NULL, NULL, NULL, NULL));
::ChangeServiceConfig2(m_serviceHandle, SERVICE_CONFIG_DESCRIPTION, &serviceDesc);
service.Detach();
now after this i call function which updates the dacl for the service
ENSURE_STATE(!!m_serviceHandle)
CAutoPtr<PSECURITY_DESCRIPTOR *> pSecurityDescriptor;
DWORD bytesNeeded = 0;
if(::QueryServiceObjectSecurity(serviceHandle, DACL_SECURITY_INFORMATION, &pSecurityDescriptor, 0, &bytesNeeded) == FALSE)
{
Any help greatly appreciated
The access rights for system services are described in the MSDN article Service Security and Access Rights.
The relevant right is
READ_CONTROL Required to call the QueryServiceObjectSecurity function to query the security descriptor of the service object.
The call to CreateService returns a handle with the access rights indicated by the dwDesiredAccess parameter, which you've set to
SERVICE_CHANGE_CONFIG |
SERVICE_START |
SERVICE_QUERY_STATUS |
SERVICE_INTERROGATE
That gives the handle the right to change the service configuration, to start the service, query the service's status and interrogate the service - but not the right to query the security descriptor.
Add READ_CONTROL to dwDesiredAccess and the problem will go away. Better still, set dwDesiredAccess to SERVICE_ALL_ACCESS.
The documentation for Service Security and Access Rights explains that the access right READ_CONTROL is required to call QueryServiceObjectSecurity.
In the call to CreateService add READ_CONTROL to the list of access rights you request for the handle.

Why doesn't LogonUser(...) work for domain accounts?

I've been trying to use LogonUser(...) to get an access token for a user account, as in this MSDN sample.
// Call LogonUser to obtain a handle to an access token.
bool returnValue = LogonUser(userName, domainName, Console.ReadLine(),
LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
out safeTokenHandle);
When I run the sample (with Administrator privileges) it works fine when given a domain of . and a local user account name and password, but no matter what I do I get error code 1326 (Logon failure: unknown user name or bad password) if I try to use a domain account. I get the same result if I enter garbage for the domain, which makes me wonder if it's actually contacting the DC at all.
What could be stopping this from working?
In my case the issue, similar to the question asker, was that the account I was trying to authenticate to was in a domain that my current machine did not belong to. Unlike the original poster, my machine should not and could not be part of this other domain. I wanted the login to perform action on a resource on this domain though.
The answer was the following
bool success = LogonUser(
userName,
domain,
password,
(int)LOGON32_LOGON_NEW_CREDENTIALS, //9
(int)LOGON32_PROVIDER_DEFAULT, //0
out userToken);
with the following constants defined:
public const int LOGON32_LOGON_NEW_CREDENTIALS = 9;
public const int LOGON32_PROVIDER_DEFAULT = 0;
Hopefully this will help others who are lost in a similar situation.
Edit: As mentioned in the comments below, this logon type allows the caller to clone its current token and specify new credentials for outbound connections. The new logon session has the same local identifier but uses different credentials for other network connections. As a result of that fact, "success" will return true even if the password is bad. You will need an additional check beyond "success" to confirm that the credentials are actually good.
This was not a concern in my initial use case as we used the current network user's credential in another function to pull the plaintext password from secure storage. So it would have never been wrong unless there was an inconsistency between that system and active directory in which case we had bigger problems.
In my case it was the fact that, although I was logged in to my computer as a domain user, my computer was not itself part of the domain. Once added to the domain the sample started to work.
Use DOMAIN\LOGIN with an empty domainname for that case...

Resources