4

I can't seem to start a process as another user when using impersonation under .Net Core. I'm running this script in Linqpad running as User1 and trying to launch a program as User2. At first, the impersonation seems to work (the Console.Writeline()s on the current user change correctly from User1 to User2 in the RunImpersonated() Method). However, the process always runs as User1.

This is one of many tests I'm doing to validate that RunImpersonated() works (this originally stems from issues in an ASP.Net Core App trying to impersonate the current user). This is the simplest reproducible example I could find.

[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
    int dwLogonType, int dwLogonProvider, out SafeAccessTokenHandle phToken);

void Main()
{
    string domainName = "myDomain";
    string userName = "User2";
    string passWord = "User2Password";

    const int LOGON32_PROVIDER_DEFAULT = 0;
    //This parameter causes LogonUser to create a primary token. 
    const int LOGON32_LOGON_INTERACTIVE = 2;

    // Call LogonUser to obtain a handle to an access token. 
    SafeAccessTokenHandle safeAccessTokenHandle;
    bool returnValue = LogonUser(userName, domainName, passWord,
        LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
        out safeAccessTokenHandle);

    if (false == returnValue)
    {
        int ret = Marshal.GetLastWin32Error();
        Console.WriteLine("LogonUser failed with error code : {0}", ret);
        throw new System.ComponentModel.Win32Exception(ret);
    }

    Console.WriteLine("Did LogonUser Succeed? " + (returnValue ? "Yes" : "No"));
    // Check the identity.
    Console.WriteLine("Before impersonation: " + WindowsIdentity.GetCurrent().Name);

    // Note: if you want to run as unimpersonated, pass
    //       'SafeAccessTokenHandle.InvalidHandle' instead of variable 'safeAccessTokenHandle'
    WindowsIdentity.RunImpersonated(
        safeAccessTokenHandle,
        // User action
        () =>
        {
            // Check the identity.
            Console.WriteLine("During impersonation: " + WindowsIdentity.GetCurrent().Name);
            Directory.GetFiles(@"C:\TMP\").Dump();
            var pi = new ProcessStartInfo
            {
                WorkingDirectory = @"C:\TMP\",
                FileName = @"C:\TMP\TestUser.exe"
            };
            var proc = Process.Start(pi);
            proc.WaitForExit();
        }
        );

    // Check the identity again.
    Console.WriteLine("After impersonation: " + WindowsIdentity.GetCurrent().Name);
}
Joao Jorge
  • 103
  • 1
  • 2
  • 16
Laurent Gabiot
  • 1,251
  • 9
  • 15
  • Does it work when you set the `UserName` and `Password` properties of the `ProcessStartInfo` object? – Progman Dec 22 '19 at 12:42
  • I used `Process.Start` and run an application under another credentials without any problem. – Reza Aghaei Dec 22 '19 at 15:40
  • @Progman The goal is not to run another process, it's the minimum reproductible project we could make where code ran within the impersionation context doesn't behave as suspected, in the actual scenario we are not trying to run another program, and we have access to a token to impersonate but not to the login/password. The goal is to understand why this doesn't work and how to make this work to avoid making a very long question here as this seems to be simplest case but we need to know why it doesn't work as is and how to make it work through impersonation at a thread level, not process start – Ronan Thibaudau Dec 22 '19 at 21:21
  • @RezaAghaei While passing the credentials to the process it is easy, but here we're trying to do it without passing anything within the impersonation context, expectation would be that it runs as the impersonated user, instead it runs as the original user – Ronan Thibaudau Dec 22 '19 at 21:22
  • run direct Process.Start(@"C:\TMP\TestUser.exe"); – Jin Thakur Dec 27 '19 at 17:10
  • Processinfo creates new process. Try process.start, Or you can convert exe to util dll and run inside code like utli.testuser code. Use dll call method from main program and not exe. – Jin Thakur Dec 27 '19 at 17:12

4 Answers4

2

If you don't specify username and password, Process.Start will uses the token for the calling process, not the impersonation token.

Looking into source code of Process.Start:

If the calling process is impersonating another user, the new process uses the token for the calling process, not the impersonation token. To run the new process in the security context of the user represented by the impersonation token, use the CreateProcessAsUser or CreateProcessWithLogonW function.

Without passing username and password the process always runs on security context of the original process owner. If you want to run the process under the context of another user:

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • I know it's possible to run another process "as" without changing the current process runner, however this is not the goal in this question (just the simplest reproductible example), the goal is to impersonate for everything (be it creating processes, making remote call to other servers or COM APIs etc). This sample shows one scenario where it fails but it seems to fail on all the one's i've tested. In the end scenario we don't have the password for the user, just the impersionation token, so passing it to ProcessInfo is not an option. – Ronan Thibaudau Dec 22 '19 at 21:12
  • So we need a solution where process A running as user A can spawn process B running as user B without ever knowing user B's credentials (which is what i understood WindowsIdentity.Impersonate should do, let us run code as user B as long as we have their token), it works for displaying the current user name, but everything seems to be done as user A, not B, when calling anything outside the current process – Ronan Thibaudau Dec 22 '19 at 21:14
  • @RonanThibaudau It's probably disappointing, but it's already been mentioned in the documentations of [`CreateProcess`](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw), *If the calling process is impersonating another user, the new process uses the token for the calling process, not the impersonation token. To run the new process in the security context of the user represented by the impersonation token, use the `CreateProcessAsUser` or `CreateProcessWithLogonW` function.* It confirms your observation. – Reza Aghaei Dec 28 '19 at 11:11
  • I can pass username and password to process start without even nugetting the impersonating library?!? Mentioning your answer in a [similar issue there](https://github.com/mattjohnsonpint/SimpleImpersonation/issues/31#issuecomment-840792125) and also reading about the [secure psw](https://stackoverflow.com/a/17909193/11323942) –  May 13 '21 at 19:51
  • Yes, `Process.Start` works great with a `SecureString` psw + username and domain and `UseShellExecute` must be false. The simpler the better! Thank you –  May 13 '21 at 20:30
1

There are multiple impersonation levels. Documentation states that you cannot use thread impersonation token to start the process - primary token will always be used:

The system uses the primary token of the process rather than the impersonation token of the calling thread in the following situations:

If an impersonating thread calls the CreateProcess function, the new process always inherits the primary token of the process.

Given you don't have user's password and you want to use impersonation token instead to start a process, unfortunately, the answer is - you can't do that.

You can impersonate caller for other operations though (RPC, COM, FS...).

fenixil
  • 2,106
  • 7
  • 13
  • This is actually good news as we are trying to impersonate for COM, can you confirm that creating a com object and calling it within RunImpersonated should flow the impersonation to the COM API? (we are trying to track wether this is a bug in our code, or a production issue). – Ronan Thibaudau Dec 28 '19 at 12:26
  • Yes, impersonation works with COM. Please note, that with a default impersonation level, you can work only with machine level resources (local COM). If you want to cross machine boundaries (use DCOM), you need to use Delegation level (requirs AD changes). Read more about [COM impersonation and cloacking here](https://learn.microsoft.com/en-us/windows/win32/com/delegation-and-impersonation). – fenixil Dec 28 '19 at 15:43
  • @RonanThibaudau, I hit impersonation issue when debugged application on the same machine and it works like a charm, but once application is on the server it didn't impersonate as I expected. Issue was that local machine has highest trust, so when you run server and make a call to localhost, it could use Delegation and impersonate your calls outside. When you have machine A makes a call to machine B and B tries to impersonate A to call C, then C will authenticate caller as B (it is all about machine boundaries) – fenixil Dec 28 '19 at 15:43
0

A while back I used the following code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;
using System.Text;
using System.Windows;
using Microsoft.Win32.SafeHandles;

namespace ZZZ
{

    partial class User : IDisposable
    {

        private string m_domain;
        private string m_user;
        private string m_pass;
        private WindowsIdentity user;
        private SafeTokenHandle safeTokenHandle;
        private WindowsImpersonationContext impContext;

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool LogonUser(
            string lpszUsername,
            string lpszDomain,
            string lpszPassword,
            int dwLogonType,
            int dwLogonProvider,
            out SafeTokenHandle phToken);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private extern static bool CloseHandle(IntPtr handle);

        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool RevertToSelf();

        //For the current user
        public User()
        {
            user = WindowsIdentity.GetCurrent();
        }

        // For custom user
        public User(string domain, string user, string password, bool doImepsonate = false)
        {
            m_domain = domain;
            m_user = user;
            m_pass = password;
            if (doImepsonate) this.Impersonate();
        }


        // If it's intended to incorporate this code into a DLL, then demand FullTrust.
        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        public void Impersonate()
        {
            if (impContext != null) throw new ImpersonationException();

            try
            {
                // Get the user token for the specified user, domain, and password using the unmanaged LogonUser method. 
                // The local machine name can be used for the domain name to impersonate a user on this machine.
                const int LOGON32_PROVIDER_DEFAULT = 0;
                //This parameter causes LogonUser to create a primary token. 
                const int LOGON32_LOGON_INTERACTIVE = 2;

                // Call LogonUser to obtain a handle to an access token. 
                bool returnValue = LogonUser(
                    m_user,
                    m_domain,
                    m_pass,
                    LOGON32_LOGON_INTERACTIVE,
                    LOGON32_PROVIDER_DEFAULT,
                    out safeTokenHandle);

                if (returnValue == false)
                {
                    int ret = Marshal.GetLastWin32Error();
                    throw new Win32Exception(ret);
                }

                using (safeTokenHandle)
                {
                    // Use the token handle returned by LogonUser. 
                    user = new WindowsIdentity(safeTokenHandle.DangerousGetHandle());
                    impContext = user.Impersonate();
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show("Exception occurred:\n" + ex.Message);
            }
        }

        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        private void Quit()
        {
            if (impContext == null) return;
            impContext.Undo();
            safeTokenHandle.Dispose();
        }
        #endregion

        internal IEnumerable<string> Groups
        {
            get
            {
                return user.Groups.Select(p =>
                {
                    IdentityReference ir = null;
                    try { ir = p.Translate(typeof(NTAccount)); }
                    catch { }
                    return ir == null ? null : ir.Value;
                });
            }
        }

    }

    // Win32 API part
    internal sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        private SafeTokenHandle() : base(true) { }

        [DllImport("kernel32.dll")]
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [SuppressUnmanagedCodeSecurity]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr handle);

        protected override bool ReleaseHandle()
        {
            return CloseHandle(handle);
        }
    }

    internal sealed class ImpersonationException : Exception
    {
        public ImpersonationException() : base("The user is already impersonated.") { }
    }

}
JohnyL
  • 6,894
  • 3
  • 22
  • 41
0

This works great for me. Check the user who is running the process. Sometime the user is not administrator or cannot impersonate.

Processinfo creates new process. Try process.start, Or you can convert exe to util dll and run inside code like utli.testuser code. Use dll call method from main program and not exe.

   const int LOGON32_PROVIDER_DEFAULT = 0;  
    //This parameter causes LogonUser to create a primary token.     
    const int LOGON32_LOGON_INTERACTIVE = 2;  
    // Call LogonUser to obtain a handle to an access token.     
    SafeAccessTokenHandle safeAccessTokenHandle;  
    bool returnValue = LogonUser(userName, domainName, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out safeAccessTokenHandle);  
    WindowsIdentity.RunImpersonated(safeAccessTokenHandle, () => {  
        Var impersonatedUser = WindowsIdentity.GetCurrent().Name;  
        //--- Call your Method here…….  
    });
Jin Thakur
  • 2,711
  • 18
  • 15
  • As mentioned this is not where we have an issue, this already displays the impersonated user name for us (which does us no good, we’re looking at impersonating them not just getting their name), it’s everything else that doesn’t work where you said « // call your method » : creating a new process, doing a remote call to a COM API etc... – Ronan Thibaudau Dec 27 '19 at 02:01
  • you don't have to call new process like this. var pi = new ProcessStartInfo. run direct Process.Start(@"C:\TMP\TestUser.exe"); – Jin Thakur Dec 27 '19 at 17:08
  • this doesn’t work any better, the pi is a relicate of checking that it does work when manually passing credentials (used to pass it login info which worked but is bit an option as we need the impersonate method to work for everything as in our actual project we only get the token, not the login/password). – Ronan Thibaudau Dec 28 '19 at 04:18