Search code examples
powershellelevated-privileges

PowerShell: Elevated code from non-elevated script, accessing variables and functions


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."
    }
}

Solution

  • 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.