I was reading about delegates on MSDN and saw a line that says
"Note:
Delegates run under the caller's security permissions, not the declarer's permissions"
What does that mean?
Assuming the question is about Windows permissions, not .Net Code Access Security (CAS).
Irrespective what account code was run under when you created delegate (i.e. box admin) Windows permissions will be computed at the moment of actual invocation - which may be different from the one where at creation time.
Imagine that you run code that performs impersonation of an account (Windows user) to access some files:
// run under "account1" - has access to c:\myFile.txt
// current Environment.UserName = "account2"
Func<string,string> readAllFile = fileName => File.ReadAllText(fileName);
// start impersonation of account2 - has access to c:\otherFile.txt,
// but not c:\myFile.txt
ImpersonateAccount("account2", readAllFile);
....
...ImpersonateAccout(string name, Func<string,string> readAllFile)
{
// .... impersonation code omitted
// current Environment.UserName = "account2"
var text1 = readAllFile(#"c:\otherFile.txt"); // success
var text2 = readAllFile(#"c:\myFile.txt"); // failure
....
In above sample readAllFile created when code was running under account1, but it does not "capture" permission of that account and hence later delegate can't read c:\myFile.txt which "account2" has no permissions to.
Note that delegates "capture" C# level context like local variables, which may bring assumption that other kinds of contexts are captured too. It is not the case for Window security context as well as for .Net execution context (current thread's culture for example).
Related
I've developed a small script in c# that is querying SQL Server and add computer objects to some Active Directory groups based on certain criteria. The script is working fine when I run it using the account which has the necessary rights to add/remove objects from Active Directory Group.
When I try to schedule the job, so it runs automatically from server using the "SYSTEM" account it does not work, I get "Access denied" I've updated the bind account to use the credentials from an account that works but I still have the same issue.
> Error Message:
> *2020-01-13 18:32:30,984 [1] ERROR TestAD.clsActiveDirectory - Error occured when trying to add computerobject abcdefg-a10 to group. Error
> message: Access is denied.*
The only way to make it work is using the actual account as account to run the scheduled task, however, problem is that our company policy does not allow us to store passwords, so I need to have the account logged-on to run this script.
code snippet
de.Username = "testing#test.com";
de.Password = "xxxxxxxxx";
de.AuthenticationType = AuthenticationTypes.Secure;
de.AuthenticationType = AuthenticationTypes.Sealing;
de.AuthenticationType = AuthenticationTypes.Delegation;
de.Properties.Count.ToString();
ds.SearchRoot = de;
ds.Filter = "(&(objectClass=computer)(name=" + _myComputerName.ToString() + `"))";`
ds.PropertiesToLoad.Add("memberof");
ds.PropertiesToLoad.Add("distinguishedname");
ds.SizeLimit = 10;
ds.PageSize = 0;
ds.SearchScope = System.DirectoryServices.SearchScope.Subtree;
I've tried adding some "AuthenticationTypes" to see if that made difference but still same
any help would be appreciated
Thx.
Have you tried using SQL Server Agents? My company uses them as opposed to Scheduled Tasks. They may be less elegant, but it may be a good alternative to your situation.
Create a SQL Server Agent that calls the executable with or without parameters.
If you cannot call the executable from the hosting OS, you can call an SSIS package on the network to call the executable for you.
Please let me know if you need more details.
I found the issue, and in the end pretty straight forward.
The Active Directory flow is following
- Bind to active directory with my special account and search for the computer object and
validate if it needs to be added to Active Directory Group
- if it needs to be added do 2nd bind to the Active Directory group and add computer
object. ==> This piece failed when using scheduled task or run under "SYSTEM" context
Reason for failure: When I bind 2nd time I did not specify any credentials so it was
using default credentials (SYSTEM) if I run the script my account which has enough
rights to add computer objects to groups.
I updated code for 2nd bind to include binding credentials and now it's working as
expected.
I hope this will be helpful for somebody else who has to troubleshoot similar issues.
old code
try
{
DirectoryEntry ent = new DirectoryEntry(bindString);
ent.Properties["member"].Add(newMember);
ent.CommitChanges();
}
New code
try
{
DirectoryEntry ent = new DirectoryEntry(bindString);
ent.AuthenticationType = AuthenticationTypes.Secure;
ent.AuthenticationType = AuthenticationTypes.Sealing;
ent.AuthenticationType = AuthenticationTypes.Delegation;
ent.Username = "test123#test.com";
ent.Password = "test123";
ent.Properties["member"].Add(newMember);
ent.CommitChanges();
}
Ok, first of all: my task is, to map a network drive programmatically from a C# program that runs as administrator.
I managed to map a drive as my normal admin user without elevated privileges and it was still visible after logoff/reboot (even though it didn't reconnect, but that's a different story). I did this with the WNetAddConnection2 WinApi function and also with the net use command in cmd, just to check.
Sadly, both didn't work with elevated privileges. In this case the drive is added as it schould, but after a reboot it is completely gone.
Is it even possible to achieve this with elevated privileges or is there some Windows account magical stuff I don't know about that prevents that?
Here is the Code I used, setting all flags that should usually make the drive be remembered and also reconnected:
uint flags = (uint)(Flags.CONNECT_CMD_SAVECRED |
Flags.CONNECT_INTERACTIVE |
Flags.CONNECT_COMMANDLINE |
Flags.CONNECT_UPDATE_PROFILE);
NETRESOURCE NetworkResource = new NETRESOURCE();
oNetworkResource.dwType = ResourceType.RESOURCETYPE_DISK;
oNetworkResource.lpLocalName = Console.ReadLine() + ":";
oNetworkResource.lpRemoteName = #"\\[Server]\foo";
oNetworkResource.lpProvider = null;
Console.WriteLine(WNetAddConnection2(NetworkResource, "[Password]", #"[Domain]\[Username]", flags));
it was still visible after logoff/reboot
this is because, when CONNECT_UPDATE_PROFILE flag used - called exported, but undocumented function I_MprSaveConn (from mpr.dll) which save in registry, under HKEY_CURRENT_USER\Network\<lpLocalName> information which you pass to WNetAddConnection2. but I_MprSaveConn at very begin call function bool IsElevatedCaller(PLUID ) and if function return true - it just exit, without saving in registry. so you absolute correct - when you call WNetAddConnection2 from elevated process (without impersonation) - this connection not persist (info not saved in registry)
solution: you need got not elevated token (say from explorer) - open/duplicate (for TokenImpersonation type) and call SetThreadToken. in this case IsElevatedCaller (can) return false (it first try open thread token (only if it not exist - process token) ) and query opened token for TokenElevationType (and return true if TokenElevationTypeFull )
so this of course not documented, but current (i test) if you impersonate self thread with not elevated token (how you got it separate question) flag CONNECT_UPDATE_PROFILE will be worked well
I need to find a way to guarantee that people using my library have set the requestedExecutionLevel to a minimum of highestAvailable in the application manifest, as follows:
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
</requestedPrivileges>
When they haven't set up the manifest correctly, I want to throw an exception so the developer is aware about this need, without having to read the documentation.
The main question: Is it possible to access this information, if so how?
Instead of checking this setting I already thought about checking the result of this setting. When set to highestAvailable I would expect any user part of the administrators group, to be running as administrator.
This would be possible using:
WindowsPrinciple.IsInRole() to check the currently used role.
Seemingly more difficult to find, a method to check whether a user is in a given group, or more particularly the administrator group. The IsUserInAdminGroup() method listed on the UAC code samples might do the trick.
Your approach is flawed, the dev that uses your library is going to dislike it greatly. The issue is that the manifest isn't the only way to get a process to run elevated. And in fact is almost never the way it is done when you debug code, the Visual Studio Hosting Process is the EXE you run and has the active manifest, without "highestAvailable". The dev will instead elevate Visual Studio itself, the program he starts inherits the security token and will run elevated as well.
What you really want to know is whether the process is elevated when it uses your library and that the user account belongs to the Administrators group. That's rather hard to come by, if the process does not run elevated then the security token is one of a normal user.
You need the code available here. Throw the exception if IsUserInAdminGroup is true and IsProcessElevated is false.
As Hans Passant points out in his answer, the real underlying question - why in this scenario you would want to check the manifest - is:
How to check whether the current user can run the process with elevated privileges?
As suggested in the question, the following would work:
var myPrincipal = new WindowsPrincipal( WindowsIdentity.GetCurrent() );
if ( !myPrincipal.IsInRole( WindowsBuiltInRole.Administrator ) &&
IsUserInAdminGroup() )
{
throw new NotSupportedException( "Some useful comments ..." );
}
The main question is thus, how do you write IsUserInAdminGroup()? The code listed in the UAC self-elevation sample albeit useful, doesn't explain what is going on, and why it is needed.
Hans Passant replied in a comment "Windows emulates a non-elevated process too well, no way to tell from .NET that the user account is in fact an admin account. Falling back to interrogating the token with pinvoke is the workaround used in the linked code.".
In short, you'll need to rely on P/Invoke in order to implement IsUserInAdminGroup(), of which the code can be found in the UAC sample.
More interesting perhaps, is why?
In order to find out I refactored the sample code and incorporated the function into my library. The result in my opinion is a bit more clear. Below you can find the outline, the comments are probably more relevant than the code since it depends on other classes etc ...
Starting from Windows Vista, you have different token types as expressed by TOKEN_ELEVATION_TYPE. Although you can access WindowsIdentity.Token through .NET, this isn't the token we need to check whether someone is administrator. This is a limited token. It has a linked elevated token attached to it, but this isn't exposed in .NET.
Pretty much all the (semi-pseudo) code below does is look up whether there is such an elevated token attached to the original token, and use that to check IsInRole() instead.
// Default token's received aren't impersonation tokens,
// we are looking for an impersonation token.
bool isImpersonationToken = false;
// Open the access token of the current process.
SafeTokenHandle processToken;
if ( !AdvApi32.OpenProcessToken( ..., out processToken ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
// Starting from Vista linked tokens are supported which need to be checked.
if ( EnvironmentHelper.VistaOrHigher )
{
// Determine token type: limited, elevated, or default.
SafeUnmanagedMemoryHandle elevationTypeHandle = ...;
if ( !AdvApi32.GetTokenInformation( ... elevationTypeHandle ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
var tokenType = (AdvApi32.TokenElevationType)Marshal.ReadInt32(
elevationTypeHandle.DangerousGetHandle() );
// If limited, get the linked elevated token for further check.
if ( tokenType == AdvApi32.TokenElevationType.TokenElevationTypeLimited )
{
// Get the linked token.
SafeUnmanagedMemoryHandle linkedTokenHandle = ...;
if ( !AdvApi32.GetTokenInformation( ... linkedTokenHandle ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
processToken = new SafeTokenHandle(
Marshal.ReadIntPtr( linkedTokenHandle.DangerousGetHandle() ) );
// Linked tokens are already impersonation tokens.
isImpersonationToken = true;
}
}
// We need an impersonation token in order
// to check whether it contains admin SID.
if ( !isImpersonationToken )
{
SafeTokenHandle impersonatedToken;
if ( !AdvApi32.DuplicateToken( ..., out impersonatedToken ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
processToken = impersonatedToken;
}
// Check if the token to be checked contains admin SID.
var identity= new WindowsIdentity( processToken.DangerousGetHandle() );
var principal = new WindowsPrincipal( identity );
return principal.IsInRole( WindowsBuiltInRole.Administrator );
When installing a service, there is a helpful .NET class called ServiceProcessInstaller. This class has a property Account, which is a ServiceAccount enumeration with possible values LocalService, LocalSystem, NetworkService and User.
This is fine at install-time, but does anybody know how I can change this value for an existing service?
I assuming that I need to move away from the actual install-type classes, and have been researching hooking into the advapi32 ChangeServiceConfig method, WMI and ManagementObjects etc.
Indeed I have found code which will actually change the account under which the service runs,
ManagementObject mo = new ManagementObject("Win32_Service.Name='" + myService + "'");
object[] configParams = new object[11];
configParams[6] = userName;
configParams[7] = password;
object result = mo.InvokeMethod("Change", configParams);
(which on its own looks a bit like black magic but makes sense when viewed with the ChangeServiceConfig signature)
However when I apply this code to a service which happens to be installed as LocalSystem, it has no effect (although when I interpret result the call is reporting success). This doesn't really surprise me since I am only setting a username and password, I am not saying "rather than running as a local service, this service needs to run under a specific user account".
Now, my gut feel is that I am heading along the right lines here. The problem is that none of the parameters in ChangeServiceConfig appear to offer the opportunity to do this.
Any ideas? TIA, Pete
Error code 16 means "Service marked for deletion". Sometimes when you change service parameter, in particular when you delete / re-create a service you need to reboot your PC for operation to complete. While it's still pending, you can't manipulate service and you get error code 16.
Also, it might not be the case, that you problem has something to do with the fact that the call is inside a dll. If you put you code in a test rig dll and call it from a test rig exe (the same way you tested it in a test rig exe) and don't create / delete service in between I think it will work anyway.
The reason it does not working in your application on my opinion has to do with what you did with the service before (and this something most likely is not described in your question).
You need Impersonate an thread to run at context of user.
Try this class :
A small C# Class for impersonating a User
or this one :
Impersonate User
Return code is 21: "Invalid Parameter".
I ran into the same issue: Problem occurs when trying to apply a new user/password to a service which currently has "LocalSystem" with "Allow Service to interact with desktop" enabled.
To resolve, set the "DesktopInteract" flag in the "Change" query
var query = new ManagementPath(string.Format("Win32_Service.Name='{0}'", serviceName)); // string.Format("SELECT * FROM Win32_Service where Name='{0}'", serviceName);
using (ManagementObject service = new ManagementObject(query))
{
object[] wmiParams = new object[10];
//WMI update doesn't work if the service's user is currently set to LocalSystem
// with Interact with desktop on
wmiParams[5] = false;
wmiParams[6] = serviceUserName;
wmiParams[7] = password;
//update credentials for the service
var rtn = service.InvokeMethod("Change", wmiParams);
}
I have a windows service which runs under system account and executes some programs from time to time (yeah,yeah, I know that's a bad practice, but that's not my decision). I need to set the "interact with desktop" check, to see the gui of that executed programs, after the service is installed. I've tried several ways, putting the code below in AfterInstall or OnCommited event handlers of my service installer:
ConnectionOptions coOptions = new ConnectionOptions();
coOptions.Impersonation = ImpersonationLevel.Impersonate;
ManagementScope mgmtScope = new System.Management.ManagementScope(#"root\CIMV2", coOptions);
mgmtScope.Connect();
ManagementObject wmiService = new ManagementObject("Win32_Service.Name='" + ServiceMonitorInstaller.ServiceName + "'");
ManagementBaseObject InParam = wmiService.GetMethodParameters("Change");
InParam["DesktopInteract"] = true;
ManagementBaseObject OutParam = wmiService.InvokeMethod("Change", InParam, null);
or
RegistryKey ckey = Registry.LocalMachine.OpenSubKey(
#"SYSTEM\CurrentControlSet\Services\WindowsService1", true);
if(ckey != null)
{
if(ckey.GetValue("Type") != null)
{
ckey.SetValue("Type", ((int)ckey.GetValue("Type") | 256));
}
}
both of these methods "work". They set the check, but after I start the service it launches the exe - and gui isn't shown! So, if I stop the service, recheck and start it again - bingo! everything starts and is shown. The second way to achieve the result is to reboot - after it the gui is also shown.
So the question is: Is there a correct way to set "interact with desktop" check, so it'll start working without rechecks and reboots?
OS: Windows XP (haven't tried Vista and 7 yet...)
private static void SetInterActWithDeskTop()
{
var service = new System.Management.ManagementObject(
String.Format("WIN32_Service.Name='{0}'", "YourServiceName"));
try
{
var paramList = new object[11];
paramList[5] = true;
service.InvokeMethod("Change", paramList);
}
finally
{
service.Dispose();
}
}
And finally after searching the internet for a week - I've found a great working solution:
http://asprosys.blogspot.com/2009/03/allow-service-to-interact-with-desktop.html
Find the desktop to launch into. This
may seem facetious but it isn't as
simple as it seems. With Terminal
Services and Fast User Switching there
can be multiple interactive users
logged on to the computer at the same
time. If you want the user that is
currently sitting at the physical
console then you're in luck, the
Terminal Services API call
WTSGetActiveConsoleSessionId will get
you the session ID you need. If your
needs are more complex (i.e. you need
to interact with a specific user on a
TS server or you need the name of the
window station in a non-interactive
session) you'll need to enumerate the
Terminal Server sessions with
WTSEnumerateSessions and check the
session for the information you need
with WTSGetSessionInformation.
Now you know what session you need to
interact with and you have its ID.
This is the key to the whole process,
using WTSQueryUserToken and the
session ID you can now retrieve the
token of the user logged on to the
target session. This completely
mitigates the security problem of the
'interact with the desktop' setting,
the launched process will not be
running with the LOCAL SYSTEM
credentials but with the same
credentials as the user that is
already logged on to that session! No
privilege elevation.
Using CreateProcessAsUser and the
token we have retrieved we can launch
the process in the normal way and it
will run in the target session with
the target user's credentials. There
are a couple of caveats, both
lpCurrentDirectory and lpEnvironment
must point to valid values - the
normal default resolution methods for
these parameters don't work for
cross-session launching. You can use
CreateEnvironmentBlock to create a
default environment block for the
target user.
There is source code of the working project attached.
Same as Heisa but with WMI. (code is Powershell, but can be easily ported to C#)
if ($svc = gwmi win32_service|?{$_.name -eq $svcname})
{
try {
$null = $svc.change($svc.displayname,$svc.pathname,16,1,`
"Manual",$false,$svc.startname,$null,$null,$null,$null)
write-host "Change made"
catch { throw "Error: $_" }
} else
{ throw "Service $svcname not installed" }
See MSDN: Service Change() method for param description.