I am preparing new computers. After applying an image, I run a PowerShell script for some post-image deployment steps. Some steps must be run as the new (current) user, like registry settings in HCCU, while others, peppered through the script, must be run elevated.
In my script, I call the RunElevated
function below for the code the requires elevation. I would like to share values and functions between elevated and non-elevated code blocks, but is that possible? I tried passing arguments when calling Start-Process powershell.exe but ran into the “Inception” problem of quotes within quotes, arguments within arguments.
function RunElevated($ScriptBlock)
{
write-host -NoNewline "`nStarting a new window with elevated privileges. Will return here after..."
$scriptBlockWithBefore = {
write-host "`nSTEPS RUNNING WITH ELEVATED PRIVILEGES...`n" @mildAlertColours
}
$scriptBlockAfter = {
Write-Host -nonewline "`nHit Enter to exit this mode. "
Read-Host
}
$scriptBlockToUse = [scriptblock]::Create($scriptBlockWithBefore.ToString() + "`n" + $ScriptBlock.ToString() + "`n" + $scriptBlockAfter)
$proc = Start-Process "powershell.exe" -Verb runas -ArgumentList "-command `"$scriptBlockToUse`"" -PassThru -WorkingDirectory $pwd.ToString()
$proc.WaitForExit()
if($proc.ExitCode -ne 0) {
write-host "ran into a problem."
}
}
As zett42 notes, you can use the powershell.exe
, the Windows PowerShell CLI's -EncodedCommand
parameter to safely pass arbitrary code to a PowerShell child process.
To also pass arguments through safely, you need the (currently undocumented) -EncodedArguments
parameter.
This Base64-encoding-based approach:
Here's self-contained sample code that demonstrates the technique:
# Sample script block to execute in the elevated child process.
$scriptBlock = {
# Parameters
param([string] $Foo, [int] $Bar, [hashtable] $Hash)
# Embedded function
function Get-Foo { "hi: " + $args }
# Show the arguments passed.
$PSBoundParameters | Out-Host
# Call the embedded function
Get-Foo $Bar
Read-Host 'Press Enter to exit.'
}
# List of sample arguments to pass to the child process.
$passThruArgs = 'foo!', 42, @{ SomeKey = 'Some Value' }
# Call via `Start-Process -Verb RunAs` to achieve elevation, and pass the
# Base64-encoded values to `-EncodedCommand` and `-EncodedArgument`
Start-Process -Wait -Verb RunAs powershell.exe -ArgumentList (
'-EncodedCommand', (
[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($scriptBlock))
),
'-EncodedArguments', (
[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(
[System.Management.Automation.PSSerializer]::Serialize($passThruArgs)
))
)
)
Note:
In this particular case, it is safe to use Start-Process
's -ArgumentList
parameter, because the arguments passed by definition contain no spaces or other metacharacters.
Cpt.Whale makes a good point: If you out-source the parts that require elevation into separate scripts (*.ps1
files), invocation via Start-Process -Verb RunAs
becomes simpler, because you then don't have to pass code via the CLI, and can use a -File
CLI call without the need for Base64 encoding. However, you are then limited to arguments that are strings or have string-literal representations.
Optional reading: Why selective elevation may be preferred / necessary:
You may prefer selective elevation for better security: It allows you to limit what runs with elevation to only the code that truly needs it.
You need selective elevation:
if the elevation happens in a different user context and parts of your code need to run in the context of the current user.
Even if elevation happens in the same user context, an operation that can notably not be performed while running with elevation - at least by default - is to establish persistent drive mappings for the current user:
[1] See this answer for details.