As Evan said, this is purposefully made to be difficult to do, because it's so easy to exploit and use maliciously.
But you can do it, if you have enough dedication and you're willing to get your hands dirty.
I created a very robust, reliable Windows Service that runs on machines in a production environment, that executes GUI applications in the security context of logged on users under specific conditions, so that the application just appears right before the user's eyes... so my point is it is possible to do it in a robust and reliable way.
The key is the CreateProcessAsUser
API function from Advapi32.dll.
And in order to use that function, you must be able to "steal" the primary access token of a logged on user. And to do that, you can use the WTSQueryUserToken
API function from Wtsapi32.dll.
One of the restrictions of using the aforementioned API is that it may only be called from within the security context of Local System -- which is why PsExec.exe -si 2 \\Test-CLI-01 calc.exe
works for you -- because the -s
switch in there tells PsExec to run as Local System.
This is naturally one of those times when your Windows service would execute as Local System, since you require it if you want to use WTSQueryUserToken
. Let it be stated that running a program or service as Local System is otherwise generally a bad idea, because Local System has unlimited access to the machine on which it's executing, so if a hacker exploits a flaw in your program, he or she can use that exploit to cause your code to take some action with all the security privileges that the Local System security context confers. (i.e., all of them.)
This is also where extreme caution must be exercised by the programmer, because it's your responsibility to dispose of these security tokens and not leak them. You (or a bad actor) could obviously use the security tokens for nefarious purposes if they are not handled properly.
Some helpful examples, taken from some C# that I wrote a while back:
Getting a user's primary access token:
WTSQueryUserToken((uint)session.SessionId, out userPrimaryAccessToken)
Using that primary access token to start a process in their session, as that user:
CreateProcessAsUser(userPrimaryAccessToken, null, cmdLine, ref saProcessAttributes, ref saThreadAttributes, false, 0, IntPtr.Zero, null, ref si, out pi)
These are just Windows API calls... you could replicate it in any language that you wish.