Home > front end >  Capturing output/error in PowerShell of process running as other user
Capturing output/error in PowerShell of process running as other user

Time:03-21

This is a variation of a question that has been asked and answered before.

The variation lies in using UserName and Password to set up the System.Diagnostics.ProcessStartInfo object. In this case, we're not able to read the output and error streams of the process – which makes sense because the process does not belong to us!

But even so, we've spawned the process so it should be possible to capture the output.

I suspect that this is a duplicate but it seems to have been misunderstood in the answer section.

CodePudding user response:

You can capture the output streams from an (invariably non-elevated) process you've launched with a different user identity, as the following self-contained example code shows:

Note:

  • Does not work if you execute the command via PowerShell remoting, such as via Invoke-Command -ComputerName, including JEA.

    • Apparently, impersonating another user is then not allowed, resulting in an Access denied error.
  • The code prompts for the target user's credentials.

  • The working directory must be set to a directory path that the target user is permitted to access, and defaults to their local profile folder below - assuming it exists; adjust as needed.

  • Stdout and stderr output are captured separately, in full, as multi-line strings.

    • If you want to merge the two streams, call your target program via a shell and use its redirection features (2>&1).

    • The example call below performs a call to a shell, namely cmd.exe via its /c parameter, outputting one line to stdout, the other to stderr (>&2). If you modify the Arguments = ... line as follows, the stderr stream would be merged into the stdout stream:

      Arguments = '/c "(echo success & echo failure >&2) 2>&1"'
      
  • The code works in both Windows PowerShell and PowerShell (Core) 7 , and guards against potential deadlocks by reading the streams asynchronously.[1]

    • In the install-on-demand, cross-platform PowerShell (Core) 7 edition, the implementation is more efficient, as it uses dedicated threads to wait for the asynchronous tasks to complete, via ForEach-Object -Parallel.

    • In the legacy, ships-with-Windows Windows PowerShell edition, periodic polling, interspersed with Start-Sleep calls, must be used to see if the asynchronous tasks have completed.

    • If you only need to capture one stream, e.g., only stdout, if you've merged stderr into it (as described above), the implementation can be simplified to a synchronous $stdout = $ps.StandardOutput.ReadToEnd() call, as shown in this answer.

# Prompt for the target user's credentials.
$cred = Get-Credential

# The working directory for the new process.
# IMPORTANT: This must be a directory that the target user is permitted to access.
#            Here, the target user's local profile folder is used.
#            Adjust as needed.
$workingDir = Join-Path (Split-Path -Parent $env:USERPROFILE) $cred.UserName

# Start the process.
$ps = [System.Diagnostics.Process]::Start(
  [System.Diagnostics.ProcessStartInfo] @{
    FileName = 'cmd'
    Arguments = '/c "echo success & echo failure >&2"'
    UseShellExecute = $false
    WorkingDirectory = $workingDir
    UserName = $cred.UserName
    Password = $cred.Password
    RedirectStandardOutput = $true
    RedirectStandardError = $true
  }
)

# Read the output streams asynchronously, to avoid a deadlock.
$tasks = $ps.StandardOutput.ReadToEndAsync(), $ps.StandardError.ReadToEndAsync()

if ($PSVersionTable.PSVersion.Major -ge 7) {
  # PowerShell (Core) 7 : Wait for task completion in background threads.
  $tasks | ForEach-Object -Parallel { $_.Wait() }
} else {  
  # Windows PowerShell: Poll periodically to see when both tasks have completed.
  while ($tasks.IsComplete -contains $false) {
    Start-Sleep -MilliSeconds 100
  }
}

# Wait for the process to exit.
$ps.WaitForExit()

# Sample output: exit code and captured stream contents.
[pscustomobject] @{
  ExitCode = $ps.ExitCode
  StdOut = $tasks[0].Result.Trim()
  StdErr = $tasks[1].Result.Trim()
} | Format-List

Output:

ExitCode : 0
StdOut   : success
StdErr   : failure

If running as a given user WITH ELEVATION (as admin) is required:

  • By design, you cannot both request elevation with -Verb RunAs and run with a different user identity (-Credential) in a single operation - neither with Start-Process nor with the underlying .NET API, System.Diagnostics.Process.

    • If you request elevation and you're an administrator yourself, the elevated process will run as you - assuming you've confirmed the Yes / No form of the UAC dialog presented.
    • Otherwise, UAC will present a credentials dialog, requiring you to provide an administrator's credentials - and there's no way to preset these credentials, not even the username.
  • By design, you cannot directly capture output from an elevated process you've launched - even if the elevated process runs with your own identity.

    • However, if you launch a non-elevated process as a different user, you can capture the output, as shown in the top section.

To get what you're looking for requires the following approach:

  • You need two Start-Process calls:

    • The first one to launch an - of necessity - non-elevated process as the target user (-Credential)

    • A second one launched from that process to request elevation, which then elevates in the context of the target user, assuming they're an administrator.

  • Because you can only capture output from inside the elevated process itself, you'll need to launch your target program via a shell and use its redirection (>) features to capture output in files.

Unfortunately, this makes for a nontrivial solution, with many subtleties to consider.

Here's a self-contained example:

  • It executes the commands whoami and net session (which only succeeds in an elevated session) and captures their combined stdout and stderr output in file out.txt in the specified working directory.

  • It executes synchronously, i.e. it waits for the elevated target process to exit before continuing; if that isn't a requirement Remove -PassThru and the enclosing (...).WaitForExit(), as well as -Wait from the nested Start-Process call.

    • Note: The reason that -Wait cannot also be used in the outer Start-Process call is a bug, still present as of PowerShell 7.2.2. - see GitHub issue #17033.
  • As instructed in the source-code comments:

    • when you're prompted for the target user's credentials, be sure to specify an administrator's credentials, to ensure that elevation with that user's identity succeeds.

    • in $workingDir, specify a working directory that the target user is permitted to access, even from a non-elevated session. The target user's local profile is used by default - assuming it exists.

# Prompt for the target user's credentials.
# IMPORTANT: Must be an *administrator*
$cred = Get-Credential

# The working directory for both the intermediate non-elevated
# and the ultimate elevated process.
# IMPORTANT: This must be a directory that the target user is permitted to access,
#            even when non-elevated.
#            Here, the target user's local profile folder is used.
#            Adjust as needed.
$workingDir = Join-Path (Split-Path $env:USERPROFILE) $cred.UserName

(Start-Process -PassThru -WorkingDirectory $workingDir -Credential $cred -WindowStyle Hidden powershell.exe @'
-noprofile -command Start-Process -Wait -Verb RunAs powershell \"
    -noexit -command `"Set-Location -LiteralPath \`\"$($PWD.ProviderPath)\`\"; & { whoami; net session } 2>&1 > out.txt`"
  \"
'@).WaitForExit()

[1] The output from a process' redirected standard streams is buffered, and the process is blocked from writing more data when a buffer fills up, in which case it has to wait for the reader of the stream to consume the buffer. Thus, if you try to synchronously read to the end of one stream, you may get stuck if the other stream's buffer fills up in the meantime, and therefore blocks the process from finishing writing to the first stream.

  • Related