1

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."
    }
}
Craig Silver
  • 587
  • 4
  • 25
  • You can't really share state across process boundaries, no. Why not just run the whole thing elevated? What are you trying to guard against? – Mathias R. Jessen Feb 24 '23 at 17:23
  • I must be able to at least pass values between the process boundaries, but I'm struggling with the syntax. I don't need to guard against anything, but some code must run elevated, like renaming the computer, joining a domain, etc., while other code is meant to affect the "current" user, and when running elevated, which user is that? – Craig Silver Feb 24 '23 at 17:30
  • "Running elevated" doesn't mean "under a different identity" - the elevated process is still running as the user, just with a "full" security token rather than a "filtered" security token. Anything you can do "unelevated" you can do elevated – Mathias R. Jessen Feb 24 '23 at 17:32
  • 1
    @Mathias, it's only the same user identity if the current user is an administrator - otherwise, an administrator's credentials must be provided for elevation. Also, one thing you notably can _not_ do with elevation even when running with the same identity is to establish persistent drive mappings, at least by default (a system can be configured to share mappings between elevated and non-elevated sessions). – mklement0 Feb 24 '23 at 17:39
  • @mklement0 Please read the code - there's no second identity involved in this case. I don't know why you bring up shared drive mappings, as running the entire script elevated would alleviate that problem completely :) – Mathias R. Jessen Feb 24 '23 at 17:40
  • @MathiasR.Jessen, it was a general observation, and the drive-mapping problem applies either way. While you may be right that _in practice_ this code will be run with a current user who is an administrator, note that the code itself doesn't tell you that. A second identity is _automatically_ involved should the current user not be an administrator - via the UAC prompt then asking for an _administrator_'s credentials. – mklement0 Feb 24 '23 at 17:46
  • 1
    Fortunately, the current user _is_ a local administrator. I must have been initially testing on a computer and user that doesn't have local admin because I noticed that HKCU was not setting values for the current user, but for the user I supplied when prompted to elevate. @MathiasR.Jessen's idea of running the whole script elevated should work for me. Thanks both. – Craig Silver Feb 24 '23 at 17:46
  • If, for some reason, I did need to use the `RunElevated` function sometime, I'd love to see the syntax for passing values to the new, elevated process. – Craig Silver Feb 24 '23 at 17:47
  • 1
    @MathiasR.Jessen, as for why I bring things up: I do so as a matter of routine practice: If a comment appears to make claims of a general nature that I perceive to be incorrect or incomplete in a way that can be misleading, I point that out - whether or not the inaccuracy is directly relevant to the question. Not all users pay attention to comments, but for the sake of those who do (such as I), this practice strikes me as important. – mklement0 Feb 24 '23 at 18:05
  • 2
    Re passing values to another PowerShell process. You can do so without having to worry about quoting (and other) intricacies by using the `-EncodedCommand` parameter. – zett42 Feb 24 '23 at 18:23
  • 1
    @CraigSilver It's a good habit to modularize scripts once they have to run in separate contexts. Consider adjusting to call a script from `RunElevated` instead, like: `$DoStuff = "C:\scripts\Do-Stuff.ps1 -Param1 $myValue -Param2 '$myString'"`, then `RunElevated -ScriptBlock $DoStuff`. There are still caveats, but it will help if you have to run sections as a non-admin user in the future – Cpt.Whale Feb 24 '23 at 19:25

1 Answers1

1

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:

  • not only eliminates quoting headaches,
  • but also enables rich data-type support for the arguments (within the limits of the type fidelity that PowerShell's XML-based cross-process serialization infrastructure, as also used in remoting, can provide)[1]

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.

    • In general, due to a long-standing bug that won't be fixed for the sake of backward-compatibility, it is ultimately simpler to pass all arguments inside a single string, using embedded double-quoting as needed - see this answer.
  • 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.

      • A different user context is automatically and invariably involved when elevation is requested if the current user isn't an administrator: the UAC dialog then automatically asks for an administrator's credentials.
    • 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:

      • Attempts to establish persistent drive mappings quietly result in non-persistent mappings (and the user's elevated incarnation doesn't see preexisting persistent mappings of its non-elevated incarnation), unless your system is explicitly configured to share persistent mappings between elevated and non-elevated sessions - see this answer for details.

[1] See this answer for details.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Great answer, thank you! For now, I'll keep things simple and run as Administrator as @mathias suggested, but this is great info. – Craig Silver Feb 27 '23 at 21:39