1

My app needs to execute PowerShell scripts to create and manage AD accounts. My current setup uses HangFire to launch background jobs, PowerShell execution being one of them. I have an ImpersonationService setup that Uses RunImpersonated and RunImpersonatedAsync methods of WindowsIdentity.

Impersonation works for API calls, but fails when I run the PowerShell scripts as the scripts are being executed under IIS APPPOOL\APP NAME. My goal is to use a service account which has the required permissions to perform the actions in the scripts.

After a lot of research, I found out that the PowerShell Execution Thread does not inherit the impersonation and have tried multiple ways around it without success.

I understand that I may have to alter my approach if running scripts as an impersonated user from the app is not possible so I'm open to any alternate solutions.

Below is the flow of my current setup.

HangFire queues a background job:

 BackgroundJob.Enqueue(() => RunScripts(mac.Username, mac.Id, username));

RunScripts creates an action to be passed through to the ImpersonationService:

public async Task RunScripts(string username, int macId, string responsibleUser)
{
    var parameters = new Dictionary<string, object>()
    {
        { "MACID", macId }
    };

    var auditId = await _macAuditTrailService.InitiateScriptProgressTracking(macId, username,
        "PowerShell Script Execution", responsibleUser);

    var response = new PowerShellRunResponseDto
    {
        Response = new PSDataCollection<PSObject>()
    };

    try
    {
        // Prepare the action to be run under impersonation
        Action runPowerShellScript = () =>
        {
            var runningAs = WindowsIdentity.GetCurrent()?.Name;
            parameters.Add("RunningAs", runningAs);

            // Directly invoke the script as a synchronous method within the impersonated context
            response = _powerShellRunspaceService.RunScript(parameters, auditId, macId).Result; // Ensure RunScript can be called synchronously
        };

        // Run the action under impersonated context
        _impersonationService.RunImpersonated(runPowerShellScript, false);
    }
    catch (Exception e)
    {
        // Any exceptions thrown by PowerShell Automation will be caught and logged here
        response.Errors = e.Message;
    }

    // Audit the script execution
    await _scriptProgressService.AuditPowerShellScripts(response, username,
        action: "PowerShell Script Execution", macId: macId, responsibleUser: responsibleUser,
        auditId: auditId);

    Console.WriteLine("Script execution completed.");
}

I have tested 3 different methods for Impersonation: RunImpersonated:

 public void RunImpersonated(Action action, bool isAderantCall)
 {
     SafeAccessTokenHandle safeAccessTokenHandle;
     bool returnValue = isAderantCall
         ? LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle)
         : LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle);

     if (!returnValue)
     {
         int ret = Marshal.GetLastWin32Error();
         safeAccessTokenHandle.Dispose();
         throw new System.ComponentModel.Win32Exception(ret);
     }

     try
     {
         WindowsIdentity.RunImpersonated(safeAccessTokenHandle, action);
     }
     finally
     {
         safeAccessTokenHandle.Dispose();
     }
 }

RunImpersonatedAsync - Similar setup as above that uses the Async version of RunImpersonated RunImpersonatedThread:

public void RunImpersonatedThread(Action action, bool isAderantCall)
{
    Thread newThread = new Thread(() =>
    {
        SafeAccessTokenHandle safeAccessTokenHandle;
     bool returnValue = isAderantCall
         ? LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle)
         : LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle);

        if (!returnValue)
        {
            int ret = Marshal.GetLastWin32Error();
            safeAccessTokenHandle.Dispose();
            throw new System.ComponentModel.Win32Exception(ret);
        }

        using (safeAccessTokenHandle)
        {
            var identity = new WindowsIdentity(safeAccessTokenHandle.DangerousGetHandle());
            // Using the synchronous version of RunImpersonated
            WindowsIdentity.RunImpersonated(identity.AccessToken, () =>
            {
                action();  // Execute the passed in action synchronously
            });
        }

        
    });

    newThread.Start();
    newThread.Join();  // Optionally wait for the thread to complete
}

This is the RunScript function which creates a Runspace and executes the scripts using PowerShell.Automation:

public async Task<PowerShellRunResponseDto> RunScript(Dictionary<string, object> scriptParameters, int auditId, int macId, string runningAs = "")
{
    var dto = new PowerShellRunResponseDto();

    InitialSessionState state = InitialSessionState.CreateDefault();
    state.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;

    using (Runspace runspace = RunspaceFactory.CreateRunspace(state))
    {
        runspace.Open();

        using (PowerShell ps = PowerShell.Create(runspace))
        {

            ps.AddScript(
                "Write-Output 'Current User:'; Write-Output $([Security.Principal.WindowsIdentity]::GetCurrent().Name)");
            ps.Invoke();

            var scriptPath = _scriptExecutionSettings.BaseUrl + _scriptExecutionSettings.ScriptName;

            // Add the script to the PowerShell instance
            ps.AddCommand(scriptPath);

            // Append each parameter to the command
            foreach (var kvp in scriptParameters)
            {
                ps.AddParameter(kvp.Key, kvp.Value);
            }

            ps.Streams.Progress.DataAdded += async (sender, e) =>
            {
                if (sender is PSDataCollection<ProgressRecord> progressRecords)
                {
                    var progress = progressRecords[e.Index].Activity;
                    Console.WriteLine("Progress: " + progress);
                    // Update the database with the progress asynchronously
                    try
                    {
                        await _scriptProgressService.UpdateScriptProgress(auditId, macId, progress,
                            _ScriptName);
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(
                            $"An error occurred while updating powershell script progress: {ex.Message}");
                    }
                }
            };

            // Execute the script and await the result.
            var pipelineObjects = await ps.InvokeAsync().ConfigureAwait(false);

            // Check for errors
            var errors = "";
            if (ps.Streams.Error.Count > 0)
            {
                foreach (var error in ps.Streams.Error)
                {
                    Console.WriteLine(error.ToString());
                    errors += error.ToString() + '\n';
                }
            }

            // Print the resulting pipeline objects to the console.
            foreach (var item in pipelineObjects)
            {
                Console.WriteLine(item.BaseObject.ToString());
            }

            // Assign output to PowerShellRunResponseDto for Auditing logs
            dto.Errors = errors;
            dto.Data = ps.Streams;
            dto.Response = pipelineObjects;
        }

        runspace.Close();
    }
    
    return dto;
}

My setup does what its supposed to do, it provides live progress feedback from the scripts. It just needs to correctly run the impersonation.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.