I’ve been working on this far longer than I’m proud of, but this is it:
IT IS POSSIBLE to execute a powershell scriptblock from a non-admin session that does elevation and also makes use of user credentials ๐
Elevation (the easy part)
param( [Parameter(Mandatory = $True, ValueFromPipeline = $True)] [ScriptBlock]$scriptBlock ) $scriptBlockString = $scriptBlock.ToString() $captureElevatedScriptBlock = @" try { `$ErrorActionPreference = 'Stop' & { $scriptBlockString } } catch { Write-Output `$_ if (-Not `$lastexitcode) { `$lastexitcode = 1 } } finally { if (-Not `$lastexitcode) { `$lastexitcode = 0 } exit `$lastexitcode } "@ $myWd = $(Get-Location).Path $startElevatedProcessArgs = @{ FilePath = "PowerShell.exe" PassThru = $true WorkingDir = "$myWd" Verb = "runas" ArgumentList = @("-Command", "& { Set-Location '$myWd'; $captureElevatedScriptBlock }") } $p = Start-Process @startElevatedProcessArgs -Wait $p.WaitForExit() $res = @{ ExitCode = $p.ExitCode }
This snippet just runs arbitrary commands (in a script-block) with elevation (if the current user is eligible for elevation). But wait, there’s no ouput of the started process, also output redirect doesn’t seem to be working when coming from a non-elevated session…
Using ‘Tee-Object’ to obtain the output of an elevated process
... $myRandom = $([System.Guid]::NewGuid()) $myTmpPath = $([System.IO.Path]::GetTempPath()) $elevatedTextOutputFile = $(Join-Path $myTmpPath "$myRandom.elevated.log") $captureElevatedScriptBlock = @" try { `$ErrorActionPreference = 'Stop' & { $scriptBlockString } | Tee-Object $elevatedTextOutputFile } ... $res = @{ ExitCode = $p.ExitCode Output = Get-Content $elevatedTextOutputFile } Remove-Item $elevatedTextOutputFile
This already looks promising ๐ – we now can run any command with elevation and also get it’s exit code (partially) and console output!
So what’s missing? – In my use case I’m always running certain scripts as a non-administrative user, and I need to to use [PSCredentials] (which are known) and then also do elevation.
You can’t use both “-verb runas” and “-Credential” in one call (or at least I didn’t manage to in a couple of hours!” – so I’ve come up with this solution:
We’ll make use of two wrappers:
1. already known – get output of elevated process
2. new: start the first wrapper from a new powershell.exe process that is started with “-Credentials”
Running a scriptblock with credentials
$startWithCredentialsArgs = @{ FilePath = "PowerShell.exe" PassThru = $true WorkingDir = $myWd ArgumentList = @("-ExecutionPolicy bypass", "-noprofile", "-noninteractive", "-Command", "& { Set-Location '$myWd'; $someScriptBlock }") Credential = $Credential RedirectStandardOutput = $wrapperScriptLog } $p = Start-Process @startWithCredentialsArgs -Wait $p.WaitForExit()
Putting it all together
param( [Parameter(Mandatory = $True, ValueFromPipeline = $True)] [ScriptBlock]$scriptBlock, [Parameter(Mandatory = $False)] [pscredential]$Credential ) $myRandom = $([System.Guid]::NewGuid()) $myTmpPath = $([System.IO.Path]::GetTempPath()) $elevatedTextOutputFile = $(Join-Path $myTmpPath "$myRandom.elevated.log") $scriptBlockString = $scriptBlock.ToString() $captureElevatedScriptBlock = @" try { `$ErrorActionPreference = 'Stop' & { $scriptBlockString } | Tee-Object $elevatedTextOutputFile } catch { Write-Output `$_ if (-Not `$lastexitcode) { `$lastexitcode = 1 } } finally { if (-Not `$lastexitcode) { `$lastexitcode = 0 } exit `$lastexitcode } "@ $myWd = $(Get-Location).Path $startElevatedProcessArgs = @{ FilePath = "PowerShell.exe" PassThru = $true WorkingDir = "$myWd" Verb = "runas" ArgumentList = @("-Command", "& { Set-Location '$myWd'; $captureElevatedScriptBlock }") } if (-Not $Credential) { # easy: run with the current user context $p = Start-Process @startElevatedProcessArgs -Wait $p.WaitForExit() $res = @{ ExitCode = $p.ExitCode Output = Get-Content $elevatedTextOutputFile } } else { # tricky: need to switch to a new session with given credentials ... # this session then needs to run the above code actually calling the elevated session $wrapperScript = Join-Path $myTmpPath "$myRandom.magic.ps1" $wrapperScriptLog = "$wrapperScript.log" @" `$startElevatedProcessArgs = @{ FilePath = `"PowerShell.exe" PassThru = `$true WorkingDir = '$myWd' Verb = "runas" ArgumentList = @(`"-noexit`",`"-Command`", `"& { Set-Location '$myWd'; $($captureElevatedScriptBlock.Replace("`$","```$")) }") } `$p = Start-Process `@startElevatedProcessArgs -Wait `$p.WaitForExit() exit `$p.ExitCode "@ | Out-File $wrapperScript -Encoding default $startWithCredentialsArgs = @{ FilePath = "PowerShell.exe" PassThru = $true WorkingDir = $myWd ArgumentList = @("-ExecutionPolicy bypass", "-noprofile", "-noninteractive", "-Command", "& { Set-Location '$myWd'; $wrapperScript }") Credential = $Credential RedirectStandardOutput = $wrapperScriptLog } $p = Start-Process @startWithCredentialsArgs -Wait $p.WaitForExit() Remove-Item $wrapperScript $res = @{ ExitCode = $p.ExitCode Output = Get-Content $elevatedTextOutputFile WrapperOutput = Get-Content $wrapperScriptLog } Remove-Item $wrapperScriptLog } Remove-Item $elevatedTextOutputFile $res
Of course, this source code is also available on GitHub!