Using setInfo() in C++ results in E_ADS_INVALID_USER_OBJECT - c++

I am trying to use the setInfo() function in C++ to commit a few changes to a user's e-mail field in Windows AD. I am making a DLL that gets exported to a Windows service. But setInfo() keeps returning:
E_ADS_INVALID_USER_OBJECT
Everything works fine if I compile the EXACT same code into an EXE and run it in a command line. But when the DLL is exported, and setInfo() is called, it returns the above error. This is a portion of my code:
IADsUser *pUser = NULL;
RESULT hr = CoInitialize(0);
VARIANT var;
hr = ADsGetObject(L"LDAP://CN=Foo Bar,CN=Users,DC=mydomain,DC=com", IID_IADsUser, (void**) &pUser);
//hr = ADsOpenObject(L"LDAP://CN=Foo Bar,CN=Users,DC=mydomain,DC=com", NULL, NULL, ADS_SECURE_AUTHENTICATION, IID_IADsUser, (void**) &pUser);
VariantInit(&var);
V_BSTR(&var) = SysAllocString(L"foobar#email.com");
V_VT(&var) = VT_BSTR;
hr = pUser->Put(CComBSTR("mail"), var);
hr = pUser->SetInfo();
I am able to bind with LDAP successfully, the binding works also with this:
ADsOpenObject()
When I add the domain Administrator credentials in ADsOpenObject, setInfo() works.. but I do not want to use username/password for this. Is there a way to use setInfo() in a DLL without providing the credentials in code?
Also, the IADsUser Get function works without providing domain admin's credentials. Is it a read/write issue? Any help is appreciated!

If you don't explicitly provide a username and password when you bind to the AD, Windows uses the security context of the process to log in. For a service running as local system or as network service, this means that it logs in using the computer's AD account.
If you change the permissions within the AD to grant the necessary access to the computer account, it will work. (The username for the computer account is the computer name with a dollar sign appended.)

Related

Remote Desktop Connection with Custom Credential Provider

I have developed a custom credential provider using the SampleWrapExistingCredentialProvider from VistaCredentialProviderSamples. The credential provider has a filter implemented that filters all other credential providers and I see just my credential provider at the time of logon. The issue is that if we connect to it using remote desktop connection, the username/password are not passed from windows RDP client to the Credential provider and I have to enter it again when RDP session opens (unlike the behavior with default provider)
I am trying to explore which part of the code handles this scenario that the credential provider accepts the username/password from remote desktop client and does not ask again. Attached is the screenshot of my credential provider after providing successful credentials on the RDP client. After I click this icon of my credential provider, I am shown the credential provider tile that asking again for the username and password. Any help would be highly appreciated on how to receive credentials from RDP client.
I have returned S_OK for CREDUI. My SetUsageScenario is as follows:
HRESULT CSampleProvider::SetUsageScenario(
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
DWORD dwFlags
)
{
HRESULT hr;
// Create the password credential provider and query its interface for an
// ICredentialProvider we can use. Once it's up and running, ask it about the
// usage scenario being provided.
IUnknown *pUnknown = NULL;
hr = ::CoCreateInstance(CLSID_PasswordCredentialProvider, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pUnknown));
if (SUCCEEDED(hr))
{
hr = pUnknown->QueryInterface(IID_PPV_ARGS(&(_pWrappedProvider)));
if (SUCCEEDED(hr))
{
hr = _pWrappedProvider->SetUsageScenario(cpus, dwFlags);
switch (cpus)
{
case CPUS_LOGON:
case CPUS_UNLOCK_WORKSTATION:
case CPUS_CREDUI:
{
hr = S_OK;
break;
}
case CPUS_CHANGE_PASSWORD:
default:
hr = E_INVALIDARG;
break;
}
}
}
if (FAILED(hr))
{
if (_pWrappedProvider != NULL)
{
_pWrappedProvider->Release();
_pWrappedProvider = NULL;
}
}
return hr;
}
the username/password are not passed from windows RDP client to the
Credential provider and I have to enter it again when RDP session
opens (unlike the behavior with default provider)
windows can not by some magic know client username/password, which connecting by rdp.
at begin on client side some credential provider must create CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION and pass it to server. inside it clsidCredentialProvider say which concrete provider collect this serialization.
what server must do with this CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION ? obvious pass it to some credential provider SetSerialization method. but for which ? for all ? no. again obvivous only for provider which clsid exactly matches clsidCredentialProvider from CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION. this provider (if exist and not filtered) must remember this credentional and then when called GetCredentialCount - say that it have dafault credentional (not CREDENTIAL_PROVIDER_NO_DEFAULT) and it usually ready for auto logon attempt with this.
from client side (mstsc) the password provider create serialization. so will be __uuidof(PasswordCredentialProvider) or __uuidof(V1PasswordCredentialProvider) (if client run on win7) in clsidCredentialProvider.
but you disable this providers in self filter. as result you yourself break process.
filter must implement UpdateRemoteCredential method. and here copy and update passed pcpcsIn. most importand part of this - we must replace clsidCredentialProvider to self CLSID. as result our SetSerialization method will be called. here we need restore original CLSID before pass it to wrapped credentional.
also important place - inside GetCredentialCount - first pass it to wrapped credentional and then do *pbAutoLogonWithDefault = FALSE; - disable autologon - you can not do this (autologon) if you require additional (OTP ?) information from client.
inside UpdateRemoteCredential method we can not modify pcpcsIn - if declared as const. so we need write our update credentional to pcpcsOut. because system can not know which size is required for rgbSerialization - we need allocate it yourself. and system then free it. obvivous need use CoTaskMemAlloc for allocate rgbSerialization.
so - all this can be understanded and without any documentation. however if all this was documented - will be not bad too.
so code for UpdateRemoteCredential :
HRESULT STDMETHODCALLTYPE CSampleProvider::UpdateRemoteCredential(
/* [annotation][in] */
_In_ const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcsIn,
/* [annotation][out] */
_Out_ CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcsOut)
{
if (pcpcsIn->clsidCredentialProvider != __uuidof(PasswordCredentialProvider) &&
pcpcsIn->clsidCredentialProvider != __uuidof(V1PasswordCredentialProvider))
{
// we dont know format of serialization
return E_UNEXPECTED;
}
ULONG cbSerialization = pcpcsIn->cbSerialization;
if (pcpcsOut->rgbSerialization = (PBYTE)CoTaskMemAlloc(cbSerialization + sizeof(GUID)))
{
memcpy(pcpcsOut->rgbSerialization, pcpcsIn->rgbSerialization, cbSerialization);
memcpy(pcpcsOut->rgbSerialization + cbSerialization, &pcpcsIn->clsidCredentialProvider, sizeof(GUID));
pcpcsOut->cbSerialization = cbSerialization + sizeof(GUID);
pcpcsOut->ulAuthenticationPackage = pcpcsIn->ulAuthenticationPackage;
pcpcsOut->clsidCredentialProvider = __uuidof(CSampleProvider);
return S_OK;
}
return E_OUTOFMEMORY;
}
if we dont know the clsidCredentialProvider - simply return E_UNEXPECTED
oterwise allocate more (on sizeof(CLSID)) memory and save original clsidCredentialProvider in the end
now SetSerialization:
HRESULT STDMETHODCALLTYPE CSampleProvider::SetSerialization(
__in const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs
)
{
if (pcpcs->clsidCredentialProvider == __uuidof(CSampleProvider))
{
// can not query WTSIsRemoteSession, small optimization
_IsRemoteSession = true;
// we got this via ICredentialProviderFilter::UpdateRemoteCredential
ULONG cbSerialization = pcpcs->cbSerialization;
if (cbSerialization >= sizeof(GUID))
{
// restore original clsidCredentialProvider
cbSerialization -= sizeof(GUID);
memcpy(const_cast<GUID*>(&pcpcs->clsidCredentialProvider), pcpcs->rgbSerialization + cbSerialization, sizeof(GUID));
const_cast<CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION*>(pcpcs)->cbSerialization = cbSerialization;
}
}
return _pWrappedProvider->SetSerialization(pcpcs);
}
restore original clsidCredentialProvider and fix cbSerialization. also because pcpcs->clsidCredentialProvider == __uuidof(CSampleProvider) can be set only inside UpdateRemoteCredential in my case (i not do CPUS_CREDUI on client side for RDP, only for "run as admin") - i just know that this is remote connection and save this information (_IsRemoteSession = true;) for not call WTSIsRemoteSession
finally GetCredentialCount:
HRESULT STDMETHODCALLTYPE CSampleProvider::GetCredentialCount(
__out DWORD* pdwCount,
__out_range(<,*pdwCount) DWORD* pdwDefault,
__out BOOL* pbAutoLogonWithDefault
)
{
HRESULT hr = _pWrappedProvider->GetCredentialCount(pdwCount, pdwDefault, pbAutoLogonWithDefault);
*pbAutoLogonWithDefault = FALSE;//!!!
return hr;
}
note very important *pbAutoLogonWithDefault = FALSE;//!!! line
According to official documentationhttps: RDC and Custom Credential Providers
If the user connected with a non-Microsoft credential provider, then
you will be prompted on the terminal server to enter credentials again
(twice). If NLA is not enabled, then despite entering using an
unsupported credential provider on the client prior to the connection,
the user will still be connected. You will be left at the logon
screen, where you can use any credential provider that is supported
for local authentication. There’s no way to avoid the two
authentications when using unsupported credential providers.
Having said that, if you have your own credential providers and you
try to do a remote desktop connection to a Vista box (having this
Credential provider) then you would need to log-in twice. This is an
expected behavior and it is by design and there is no legitimate way
to avoid it.

Calling QueryInterface with custom identity

Issue:
I successfully call CoSetProxyBlanket on a proxy (if that's the right term for it) and then I call QueryInterface on that same proxy, but I receive a result of 0x80070005 ("Access Denied"). However, if I first call CoInitializeSecurity (which I am trying to avoid) with that same credentials then the call succeeds.
Question:
How can I successfully get the interface I need without having to call CoInitializeSecurity? From what I understand, a process can only call this method once so its not compatible with making a dll and can usually be substituted with calls to CoSetProxyBlanket.
Details:
I am experimenting with building my own OPC client that can communicate to computers running on different domains without matching user accounts.
First, I create an identity structure with a domain, username, and password that are valid on the server:
COAUTHINFO authInfo;
COAUTHIDENTITY authIdentity;
authIdentity.Domain = (unsigned short *) w_domain;
authIdentity.DomainLength = wcslen( w_domain);
authIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
authIdentity.Password = (unsigned short *) w_password;
authIdentity.PasswordLength = wcslen(w_password);
authIdentity.User = (unsigned short *) w_username;
authIdentity.UserLength = wcslen(w_username);
authInfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CALL;
authInfo.dwAuthnSvc = RPC_C_AUTHN_WINNT;
authInfo.dwAuthzSvc = RPC_C_AUTHZ_NONE;
authInfo.dwCapabilities = EOAC_NONE;
authInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE;
authInfo.pAuthIdentityData = &authIdentity;
authInfo.pwszServerPrincName = NULL;
ServerInfo.pAuthInfo = &authInfo;
Then I am able to call CoCreateInstanceEx with this server info an obtain a handle (m_IOPCServer) to my OPC server (IID_IOPCServer).
After I obtain the handle, I've found that it is necessary to once again set more permissions (see How does impersonation in DCOM work?) with this call:
hr = CoSetProxyBlanket(m_IOPCServer, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE,
NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
&authIdentity, EOAC_NONE);
After this I am able to successfully obtain a handle to a OPC Item Group:
hr = m_IOPCServer->AddGroup(L"", FALSE, reqUptRate, clientHandle,
NULL, NULL, lcid, &m_hServerGroup, &revisedUptRate,
IID_IOPCItemMgt,(LPUNKNOWN*)&m_IOPCItemMgt);
However, when I try to use this code:
hr = m_IOPCItemMgt->QueryInterface(IID_IOPCSyncIO, (void**)&m_IOPCSyncIO);
The result is 0x80070005 ("Access Denied"). This is the case even if I successfully call CoSetProxyBlanket on m_IOPCItemMgt. However if I first call CoInitializeSecurity, then the call succeeds.
I believe the issue related to How does impersonation in DCOM work? in that the QueryInterface function is a form of object creation so it doesn't use the same security as the other method calls like AddGroup. However in the Microsoft reference QueryInterface, under notes to implementer, it makes it sound like QueryInterface shouldn't be checking ACLs and under return values, Access Denied is not mentioned as a possibility. I don't think that this issue is implementation specific though because I have tried my code on some well known commercial OPC servers (e.g. Matrikon Simulation Server) as well as the opensource LightOPC which doesn't implement any extra security.
I am guessing that what I need to do is to find a way to replicate this command
hr = m_IOPCItemMgt->QueryInterface(IID_IOPCSyncIO, (void**)&m_IOPCSyncIO);
but do so while also supplying authIdentity. Is this possible? Can it be done with CoCreateInstanceEx or CoGetClassObject or some other COM call?
Without going into too much detail: CoInitializeSecurity is always invoked at least once per process. This can be done implicitly or explicitly. If your code doesn't make an explicit call, the DCOM runtime does it for you with parameters populated from the registry. You can try to tweak the appropriate registry values to force DCOm using values similar to those used in your explicit call. The registry key that holds those values is "HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID{AppID_GUID}" This key is described here:https://msdn.microsoft.com/en-us/library/windows/desktop/ms693736(v=vs.85).aspx
You have to call CoSetProxyBlanket on every new COM object instance, so in your case you have to call it even for m_IOPCItemMgt.
You need to call CoSetProxyBlanket in the IUnknown interface before using QueryInterface
CComPtr<IUnknown> pUnknown;
hr = m_IOPCItemMgt->QueryInterface(IID_IUnknown, (void**)&pUnknown);
hr = CoSetProxyBlanket(pUnknown, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE,
NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
&authIdentity, EOAC_NONE);
hr = pUnknown->QueryInterface(IID_IOPCSyncIO, (void**)&m_IOPCSyncIO);
You can check this for more info.

windows credential provider remote logon

I am trying to read a credential from a smart card on one workstation (local), then send it to a different workstation(remote) and use it to logon to that workstation from within a credential provider.
I've been searching and, and researching for weeks now but have not found anyone showing or telling how to do this.
I have found several references to an excellent work at https://www.idrix.fr/Root/Samples/LsaSmartCardLogon2.cpp, which is working code for a smart card credential provider, but unfortunately requires that the credential provider have direct access to the smart card with the credential on it.
The actual code I am working with seems a little to long to include in the post so I thought I try asking the question using sudo code.
On the local workstation:
MySendCredentialsFromSmartCard(){
CryptAcquireContext(hProv, containerName, cardName, PROV_RSA_FULL, CRYPT_SILENT)
CryptGetUserKey(*hProv, AT_KEYEXCHANGE, &hCryptKey);
CryptGetKeyParam(hCryptKey, KP_CERTIFICATE, encodedCert, &size, 0);
MySendCertToRemote(encodedCert,size);
}
On Remote workstation running in custom credential provider:
HRESULT CSampleCredential::GetSerialization(
_Out_ CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE *pcpgsr,
_Out_ CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs,
_Outptr_result_maybenull_ PWSTR *ppwszOptionalStatusText,
_Out_ CREDENTIAL_PROVIDER_STATUS_ICON *pcpsiOptionalStatusIcon){
MyReadCertFromRemote(&cert,&size);
CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0);
CryptCreateHash(hCryptProv, CALG_SHA1, NULL, 0, &hHash);
CryptHashData(hHash, (BYTE *)cert, (DWORD)size, 0);
CERT_CREDENTIAL_INFO certInfo;
CryptGetHashParam(hHash, HP_HASHVAL, certInfo.rgbHashOfCert, &dwHashLen, 0);
CredMarshalCredential(CertCredential, &certInfo, marshalledCred);
/****************************for debug only*****************************/
LogonUser((*marshalledCred, NULL, NULL, LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_WINNT50, &userHandle);
//this reuturns 1 (success)
/**************************************************************************/
KERB_INTERACTIVE_UNLOCK_LOGON kiul;
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus
KerbInteractiveUnlockLogonInit(L"", (PWSTR)marshaledCred, L"", cpus, &kiul);//(note:I've tried this with the third param set to the card's pin)
KerbInteractiveUnlockLogonPack(kiul, &pcpcs->rgbSerialization, &pcpcs->cbSerialization);
RetrieveNegotiateAuthPackage(&ulAuthPackage);
pcpcs->ulAuthenticationPackage = ulAuthPackage;
pcpcs->clsidCredentialProvider = CLSID_OfProvider;
*pcpgsr = CPGSR_RETURN_CREDENTIAL_FINISHED;
}
LogonUI.exe displays "A specified logon session does not exist. It may already have been terminated." This seams to correspond to Windows error code 1312. It seems that most developers struggling with this error code are having trouble configuring IIS with an SSL cert. I have found no reference to this error in relation to a credential provider.
Various threads seem to indicate that LogonUser is mearly a wrapper around LSALogonUser and the returned values from ICredentialProviderCredential::GetSerialization will be passed to LSALogonUser. The fact that my call to LogonUser with the marshalled credential succeeds would seem to suggest that on I'm on the right track, but I haven't been able to verify that nor figure out what I'm missing.
I would like to try to avoid solutions that require storing the credential on the remote workstation as it is typically a domain admin credential.
Any ideas anybody? Any advise on how to proceed?
Thanks
I guess you can do it using a communication mechanism where you send your data from one workstation to another one and in the remote one you serialize your data in SetSerialization() instead of GetSerialization(), preparing all the fields and processing the logon.
From my understanding point of you, the Provider class (the one inherited by ICredentialProvider) has to wait for those credentials somehow and pass them to SetSerialization

Calling remote COM component from MFC client?

I have a COM component that was originally written in Visual Studio 6. It is in a windows service which I have running on one of my development machines. I have written a quick MFC test app to call it and this works fine when run from this machine, the code looks like
COSERVERINFO si;
MULTI_QI qi;
COAUTHINFO cai = { RPC_C_AUTHN_NONE, RPC_C_AUTHZ_NONE, 0,RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IMPERSONATE,0, EOAC_NONE };
si.dwReserved1 = 0;
si.pwszName =L"{machine name}";
si.pAuthInfo = &cai;
si.dwReserved2 = 0;
qi.pIID = &IID_IMyComponent;
qi.pItf = NULL;
qi.hr = 1;
HRESULT hr = CoCreateInstanceEx(CLSID_MyComponent,NULL,CLSCTX_REMOTE_SERVER ,&si,1,&qi);
However, when I move the MFC test app to my other development machine and try and call the component on the other machine it fails. The hresult returned from CoCreateInstanceEx is -2147024891
I have already created the proxy stub DLL and registered it on both machines. Am I missing something else?
UPDATE:
I now updated the COUTHINFO structure to the below and it works.
COAUTHINFO cai = { RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,0, EOAC_NONE };
The ERROR_ACCESS_DENIED is most likely due to the wrong parameters supplied with COAUTHINFO. You set Authentication, Authorization levels to NONE, which is not enough to get permissions to connect with remote machine. Try to set these values: RPC_C_AUTHN_DEFAULT (have COM negotiate the best authentication service), RPC_C_AUTHZ_NONE, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE. Also see the MSDN source - COAUTHINFO structure
An HRESULT of -2147024891 (or 0x80070005 when converted to hex) is ERROR_ACCESS_DENIED. So it's a permission error when trying to connect to the remote machine.
The error means E_ACCESS_DENIED. Make sure the current user has the rights to access the component. Run dcomcnfg (or "Component Services") on the server box, under "DCOM Config" find the right component, under "Security" change the permissions to allow both activation and access for the calling user.
Or make sure the calling user is an admin on the server box.

WinAPI NetUserGetInfo() fails with NERR_UserNotFound error code on Active Directory domain

I'm running the following piece of code from a local service application. The purpose is to obtain the path to a user's profile before calling LoadUserProfile() to load that user's profile before calling CreateProcessAsUser() to run a user-mode process on behalf of that user.
Note that this question is not about LoadUserProfile(), or CreateProcessAsUser().
What happens is this. When the code below is run on Windows XP w/SP3 that is a part of the Active Directory domain, with a single user logged in via a local console (that user's session ID is used below) the NetUserGetInfo() API fails. Also note that it works fine in any other circumstance:
//'dwSessID' = session ID of the user to retrieve a user profile path for
LPTSTR pUserName = NULL;
DWORD dwcbSzUserName = 0;
if(!WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, dwSessID, WTSUserName, &pUserName, &dwcbSzUserName))
{
//Error
return false;
}
USER_INFO_4* pUI4 = NULL;
DWORD dwNetStatus;
if((dwNetStatus = NetUserGetInfo(NULL, pUserName, 4, (BYTE**)&pUI4)) == NERR_Success)
{
PROFILEINFO pfi = {0};
pfi.dwSize = sizeof(pfi);
pfi.lpUserName = pUserName;
pfi.dwFlags = PI_NOUI;
pfi.lpProfilePath = pUI4->usri4_profile;
LoadUserProfile(hToken, &pfi);
//And so on
}
else
{
//On that specific machine I get here with 'dwNetStatus' = 2221,
//or NERR_UserNotFound, that according to MSDN is
//"The user name could not be found."
//Also note that GetLastError is not used for this API.
}
Can some suggest why can NetUserGetInfo() fail on that particular machine, and how to fix this code?
PS. I know that MSDN for NetUserGetInfo states that there might be issues with a ACL on Active Directory domain, but it doesn't specify how to set one...
If I read the documentation for NetUserGetInfo, for the information level of the data you code 4 . It's written Level 4 Return detailed information and additional attributes about the user account. This level is valid only on servers. As far as I understand it's not your case. Do you verify the value of pUserName returned by WTSQuerySessionInformation.
As JPBlanc stated NetUserGetInfo with level 4 is valid only on servers.
Another problem is that you retrieve the name of the logged on user, but not the domain the user belongs to.
Noticed you are calling NetUserGetInfo with pUserName the type of LPTSTR.
Sometimes it won't work (if you will compile your project to use ANSII strings by default).
Consider changing you string types to LPWSTR.