Starting new process as a different user from a service on Windows Server and UAC

Fixing incomplete user impersonation in .NET

I was recently developing a service that would copy binaries to and run NUnit functional tests on a remote machine. For that I created a service account on that target machine, called it PublishService. Assigned a password to it and the required permissions, including full access to certain directories. Then I created an HTTP handler that used this remote call API to process the request, write binaries received from the Team City runner to the target location on the server and execute functional tests by running NUnit via command line in a separate process. The execution needed to happen under different credentials, mentioned above. For that I used the .NET classes Process and ProcessStartInfo. All was well, it seemed, until I tested it and realised that it wasn't being executed under the desired account, despite me specifying the UserName on ProcessStartInfo. After some digging, I discovered that those .NET classes weren't working as intended in the Windows Server environment, possibly due to additional security features introduced in Windows since those .NET components were implemented. It didn't work, despite Process.Start returning true.

After doing some investigation, I discovered that:

1. Impersonation was required

2. Extra access rules needed to be dynamically added in order to make things work.

The required functionality was not available in .NET 4, but only through Windows API. Luckily, I found a third party C# library that contained the wrappers I needed. It needed some very minor modifications, but other than that was usable right out of the box. So, in the end to properly start a new process under a different account, I needed to do the following (using the classes from the library):

First, impersonate the account:

Impersonation.LogonUser(null, "PublishService", "password", LogonType.Batch);

Then do some Windows security wizardry to allow the account to perform interactive actions (functional tests require that):

NTAccount account = new NTAccount("PublishService");
SecurityIdentifier id = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
WindowStationHandle hWinSta = WindowStationHandle.GetProcessWindowStation();
WindowStationSecurity ws = new WindowStationSecurity(hWinSta,
  System.Security.AccessControl.AccessControlSections.Access);
ws.AddAccessRule(id,
	WindowStationAccessRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow);
ws.ApplyChangesTo(hWinSta);
DesktopHandle hDesk = DesktopHandle.GetFromCurrentThread();
DesktopSecurity ds = new DesktopSecurity(hDesk,
	System.Security.AccessControl.AccessControlSections.Access);
ds.AddAccessRule(id,
	DesktopAccessRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow);
ds.ApplyChangesTo(hDesk);

Then I could use Process.Start() with the ProcessStartInfo pointing to the impersonated account, and it all worked like charm. See the complete code for the service handler below. The rest of the source code can be found in the attached zip file. It's provided as is, fully functional and tested on the server for which it was designed. It can hopefully serve as a good reference on how to properly impersonate a user and dynamically add proper access rights to your impersonated service account so that it can interact with UI applications.

Full source here: ZIP file containing an example on how to impersonate a user or service account and add desktop-interactive access rights and permissions to it.

Handler source code:

<%@ WebHandler Language="C#" Class="Handler" %>

using System;
using System.Diagnostics;
using System.EnterpriseServices;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security;
using System.Security.Principal;
using System.Web;
using System.Xml.Linq;
using DevBible.Core.Net;
using PublishShared;
using Security;
using System.Threading;
using NativeWindows.WindowStations;
using NativeWindows.Desktop;

//TODO: this code was written in a hurry under pressure to deliver, hence it's not very well structured. Refactor the code.
public class Handler : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        Server.ProcessRequest<PublishServiceImplementation>(context.Request.InputStream, context.Response.OutputStream);
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }

    public class PublishServiceImplementation : PublishService
    {
        public override PublishResponse Publish(PublishRequest package)
        {
            Impersonation impersonation = null;

            var response = new PublishResponse();
            
            try
            {
                impersonation = Impersonation.LogonUser(null, "PublishService", "password", LogonType.Batch);

                if (!Unpack(package, response))
                    return response;

                string error;
                response.TestResultsXml = RunUnitTests(out error);
                response.Error = error;
            }
            catch(Exception ex)
            {
                response.Error = "Could not get elevated permissions for writing.";
                return response;
            }
            finally
            {
                // Revert impersonation
                if (impersonation != null)
                    impersonation.Dispose();
            }

            return response;
        }

        private static string RunUnitTests(out string error)
        {
            string nunitPath = FileSystem.GetAbsolutePath(HttpRuntime.AppDomainAppPath, @"..\NUnit\nunit-console-x86.exe");
            string testDllPath = FileSystem.GetAbsolutePath(HttpRuntime.AppDomainAppPath, @"..\WebChatTests\FunctionalTests.dll");
            ProcessStartInfo startInfo = new ProcessStartInfo("cmd");
            startInfo.Arguments = "/c " + nunitPath + " \"" + testDllPath + "\" /xmlconsole";
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardOutput = true;
            var securePass = new SecureString();
            foreach (char passChar in "password")
                securePass.AppendChar(passChar);
            startInfo.UserName = "PublishService";
            startInfo.Password = securePass;
            startInfo.Domain = "WR-CTISVR-VM";
            startInfo.WorkingDirectory = (new FileInfo(nunitPath)).DirectoryName;
            
            Process process = null;

            string output;

            try
            {
                NTAccount account = new NTAccount("PublishService");
                SecurityIdentifier id = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
                WindowStationHandle hWinSta = WindowStationHandle.GetProcessWindowStation();
                WindowStationSecurity ws = new WindowStationSecurity(hWinSta,
                  System.Security.AccessControl.AccessControlSections.Access);
                ws.AddAccessRule(id,
                    WindowStationAccessRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow);
                ws.ApplyChangesTo(hWinSta);
                DesktopHandle hDesk = DesktopHandle.GetFromCurrentThread();
                DesktopSecurity ds = new DesktopSecurity(hDesk,
                    System.Security.AccessControl.AccessControlSections.Access);
                ds.AddAccessRule(id,
                    DesktopAccessRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow);
                ds.ApplyChangesTo(hDesk);
                process = new Process();
                process.StartInfo = startInfo;
                if (!process.Start())
                {   
                    error = "Failed to start NUnit process.";
                    return null;
                }
                output = process.StandardOutput.ReadToEnd();
            }
            catch (Exception ex)
            {
                error = "Failed to start NUnit";
                return null;
            }
            finally
            {
                if (process != null)
                {
                    process.Dispose();
                }
            }

            return GetXml(output, out error);
        }

        private static string GetXml(string output, out string error)
        {
            if (string.IsNullOrWhiteSpace(output))
            {
                error = "No output received from NUnit.";
                return null;
            }

            int xmlStart = output.IndexOf("<?xml", StringComparison.InvariantCultureIgnoreCase);

            if (xmlStart == -1)
            {
                error = "Could not locate XML content in the output.";
                return null;
            }

            const string xmlEndStr = "</test-results>";
            int xmlEnd = output.LastIndexOf(xmlEndStr);

            if (xmlEnd == -1)
            {
                error = "Could not determine the end of XML content.";
                return null;
            }

            output = output.Substring(xmlStart, xmlEnd+xmlEndStr.Length - xmlStart);

            error = null;
            return output;
        }
        
        private static bool Unpack(PublishRequest package, PublishResponse response)
        {
            try
            {
                UnZip(package.SampleSitePackage, @"C:\inetpub\wwwroot");
                UnZip(package.PublishServicePackage, @"C:\inetpub\wwwroot");
                UnZip(package.FunctionalTests, @"C:\inetpub\wwwroot");
                
                if (!KillProcess("EiccWebServices"))
                {
                    response.Error = "Failed to stop the EiccWebServices process.";
                    return false;
                }
                    
                UnZip(package.EiccWebServicesPackage, @"C:\inetpub\wwwroot");
                
                //Restart the process
                if (
                    !StartProcess(
                    FileSystem.GetAbsolutePath(HttpRuntime.AppDomainAppPath, @"..\EiccWebServices\EiccWebServices.exe"),
                    "/port=11111")
                    )
                    )
                {
                    response.Error = "Failed to start EiccWebServices process.";
                    return false;   
                }
            }
            catch (Exception ex)
            {
                response.Error = "Failed to unpack or write contents.";
                return false;
            }
            return true;
        }

        private static bool KillProcess(string name)
        {
            var proccesses = Process.GetProcessesByName(name);

            if (proccesses != null)
            {
                foreach (var process in proccesses)
                {
                    try
                    {
                        //TODO: this is not a nice way to stop the process, a better one would be to instruct it to close
                        //change this so that a singnal can be sent to the process to close
                        process.Kill();
                    }
                    catch
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        private static bool StartProcess(string path, string arguments)
        {
            return StartProcess(path, arguments, null, null);
        }
        
        private static bool StartProcess(string path, string arguments, string accountName, string password)
        {
            ProcessStartInfo startInfo = new ProcessStartInfo(path);
            //startInfo.WindowStyle = ProcessWindowStyle.Minimized;
            startInfo.Arguments = arguments;

            try
            {
                if (accountName != null)
                {
                    var securePass = new SecureString();
                    if (password != null)
                    {
                        foreach (char passChar in password)
                            securePass.AppendChar(passChar);
                    }
                    startInfo.UserName = accountName;
                    startInfo.Password = securePass;
                    startInfo.UseShellExecute = false;
                    startInfo.Domain = "";
                }
                Process.Start(startInfo);
            }
            catch (Exception ex)
            {
                return false;
            }

            return true;
        }

        private static void UnZip(byte[] data, string path)
        {
            MemoryStream archiveStream = null;
            ZipArchive archive = null;

            try
            {
                archiveStream = new MemoryStream(data);
                archive = new ZipArchive(archiveStream, ZipArchiveMode.Read, false);

                ClearTopLevelDirectory(archive, path);

                foreach (var entry in archive.Entries)
                {
                    string fullPath = FileSystem.GetAbsolutePath(path, entry.FullName);
                    var file = new FileInfo(fullPath);                    
                    
                    int retries = 30;
                    FileStream fileStream = null;
                    Stream entryStream = null;

                    try
                    {
                        entryStream = entry.Open();
                            
                        for(;;)
                        {
                            try
                            {
                                DirectoryInfo dir = new DirectoryInfo(file.Directory.FullName);
                                if (dir != null && !dir.Exists)
                                    dir.Create();
                                fileStream = file.Open(FileMode.Create, FileAccess.Write, FileShare.Write);
                                break;
                            }
                            catch(Exception ex)
                            {
                                if(retries-- > 0)
                                    Thread.Sleep(1000);
                                else throw ex;
                            }
                        }
                            
                        entryStream.CopyTo(fileStream);
                        fileStream.Flush();
                    }
                    finally
                    {
                        if(fileStream != null)
                            fileStream.Close();
                            
                        if(entryStream != null)
                            entryStream.Close();
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to unzip.", ex);
            }
            finally
            {
                archive.Dispose();
                archiveStream.Close();
            }
        }

        private static void ClearTopLevelDirectory(ZipArchive archive, string basePath)
        {
            var entry = archive.Entries.FirstOrDefault();

            if (entry == null)
                return;

            string name = entry.FullName;
            int start = name.StartsWith("\\") || name.StartsWith("//") ? 1 : 0,
                end = name.IndexOfAny(new[] { '\\', '/' }, start);

            string dirName = entry.FullName.Substring(start, end == -1 ? name.Length - start : end - start);

            string absPath = FileSystem.GetAbsolutePath(basePath, dirName);
            DirectoryInfo dir = null;

            int retries = 5;

            for (; ; )
            {
                try
                {
                    dir = new DirectoryInfo(absPath);
                    if (dir.Exists)
                        dir.Delete(true);
                    break;
                }
                catch(Exception ex)
                {
                    Thread.Sleep(1000);
                    if (retries--==0)
                        throw ex;
                }
            }
        }
    }//end class
}





Information Error Confirmation required