简体   繁体   中英

How can I elevate Powershell while keeping the current working directory AND maintain all parameters passed to the script?

function Test-IsAdministrator
{
    $Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $Principal = New-Object System.Security.Principal.WindowsPrincipal($Identity)
    $Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Test-IsUacEnabled
{
    (Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System).EnableLua -ne 0
}

if (!(Test-IsAdministrator))
{
    if (Test-IsUacEnabled)
    {
        [string[]]$argList = @('-NoProfile', '-NoExit', '-File', $MyInvocation.MyCommand.Path)
        $argList += $MyInvocation.BoundParameters.GetEnumerator() | ForEach-Object {"-$($_.Key)", "$($_.Value)"}
        $argList += $MyInvocation.UnboundArguments
        Start-Process PowerShell.exe -Verb Runas -WorkingDirectory $pwd -ArgumentList $argList 
        return
    }
    else
    {
        throw "You must be an administrator to run this script."
    }
}

If I run the script above, it successfully spawns another PowerShell instance with elevated privileges but the current working directory is lost and automatically set to C:\Windows\System32 . Bound Parameters are also lost or incorrectly parsed.

After reading similar questions I learned that when using Start-Process with -Verb RunAs, the -WorkingDirectory argument is only honored if the target executable is a .NET executable. For some reason PowerShell 5 doesn't honor it:

The problem exists at the level of the .NET API that PowerShell uses behind the scenes (see System.Diagnostics.ProcessStartInfo), as of this writing (.NET 6.0.0-preview.4.21253.7).

Quote from this related question :

In practice - and the docs do not mention that - the -WorkingDirectory parameter is not respected if you start a process elevated (with administrative privileges, which is what -Verb RunAs - somewhat obscurely - does): the location defaults to $env:SYSTEMROOT\system32 (typically, C:\Windows\System32).

So the most common solution I've seen involves using -Command instead of -File. IE:

Start-Process -FilePath powershell.exe -Verb Runas -ArgumentList '-Command', 'cd C:\ws; & .\script.ps1'

This looks really hack-ish but works. The only problem is I can't manage to get an implementation that can pass both bound and unbound parameters to the script being called via -Command.

I am trying my hardest to find the most robust implementation of self-elevation possible so that I can nicely wrap it into a function (and eventually into a module I'm working on) such as Request-AdminRights which can then be cleanly called immediately in new scripts that require admin privileges and/or escalation. Pasting the same auto-elevation code at the beginning of every script that needs admin rights feels really sloppy.

I'm also concerned I might be overthinking things, and to just leave elevation to the script level instead of wrapping it into a function.

Any input at all is greatly appreciated.

The closest you can get to a robust, cross-platform self-elevating script solution that supports:

  • both positional (unnamed) and named arguments
    • while preserving type fidelity within the constraints of PowerShell's serialization (see this answer )
  • preserving the caller's working directory.

is the following monstrosity (I certainly wish this were easier):

  • Note:
    • For (relative) brevity, I've omitted your Test-IsUacEnabled test, and simplified the test for whether the current session is already elevated to [bool] (net session 2>$null)

    • You can drop everything between # --- BEGIN: Conditional SELF-ELEVATION CODE and # --- END: Conditional SELF-ELEVATION CODE into any script to make it self-elevating.

# Sample parameters.
param(
  [object] $First,
  [int] $Second,
  [array] $Third
)

# --- BEGIN: Conditional SELF-ELEVATION CODE
$isWin = $env:OS -eq 'Windows_NT'
# Trick on Windows: `net session` only succeeds (has stdout output) in elevated sessions.
$mustElevate = ($isWin -and -not (net session 2>$null)) -or (-not $isWin -and 0 -ne (id -u))
if ($mustElevate) {
  # Note: 
  #   * On Windows, the script invariably runs in a *new window*, and by design we let it run asynchronously, in a stay-open session.
  #   * On Unix, sudo runs in the *same window, synchronously*, and we return to the calling shell when the script exits.
  #   * -inputFormat xml -outputFormat xml are NOT used:
  #      * The use of -encodedArguments *implies* CLIXML serialization of the arguments; -inputFormat xml presumably only relates to *stdin* input.
  #      * On Unix, the CLIXML output created by -ouputFormat xml is not recognized by the calling PowerShell instance and passed through as text.
  #   * On Windows, the elevated session's working dir. is set to the same as the caller's (happens by default on Unix, and also in PS Core on Windows - but not in *WinPS*)
  Write-Verbose ("This script, `"$PSCommandPath`", requires " + ("superuser privileges, ", "admin privileges, ")[$isWin] + ("re-invoking with sudo...", "re-invoking in a new window with elevation...")[$isWin])
  $psExe = (Get-Process -Id $PID).Path
  if (0 -ne ($PSBoundParameters.Count + $args.Count)) {
    # Arguments were passed, so the CLI must be called with -encodedCommand and -encodedArguments, for robustness.
    # !! To work around a bug in the deserialization of [switch] instances, replace them with Boolean values.
    foreach ($key in @($PSBoundParameters.Keys)) {
      if (($val = $PSBoundParameters[$key]) -is [switch]) { $null = $PSBoundParameters.Remove($key); $null = $PSBoundParameters.Add($key, $val.IsPresent) }
    }
    # Note: If the enclosings script is non-advanced, *both* $PSBoundParameters and $args can be populated, so we pass *both* through.
    $serializedArgs = [System.Management.Automation.PSSerializer]::Serialize(($PSBoundParameters, $args), 1) # Use the same depth as the remoting infrastructure.
    # The command that receives the (deserialized) arguments.
    $cmd = 'param($bound, $positional) Set-Location "{0}"; & "{1}" @bound @positional' -f (Get-Location -PSProvider FileSystem).ProviderPath, $PSCommandPath
    if ($isWin) {
      Start-Process -Verb RunAs $psExe ('-noexit -encodedCommand {0} -encodedArguments {1}' -f [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd)), [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($serializedArgs)))
    } else {
      # Note: No need to load the profile if the session isn't kept open.
      sudo $psExe -noprofile -encodedCommand ([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))) -encodedArguments ([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($serializedArgs)))
    }
  }
  else {
    # NO arguments were passed - simple reinvocation of the script with -c is sufficient.
    # Note: While -f (-File) should be sufficient, it leaves $args undefined. Also, on WinPS we must set the working dir.
    if ($isWin) {
      Start-Process -Verb RunAs $psExe ('-noexit -c Set-Location "{0}"; & "{1}"' -f (Get-Location -PSProvider FileSystem).ProviderPath, $PSCommandPath)
    }
    else {
      # Note: No need to load the profile if the session isn't kept open.
      sudo $psExe -noprofile -c "& `"$PSCommandPath`""
    }
  }
  # EXIT after reinvocation.
  # Note: On Windows, since Start-Process was invoked asynchronously, all we can report is whether *it* failed on invocation.
  exit ($LASTEXITCODE, (1, 0)[$?])[$isWin]
} else {
  Write-Verbose "Being run as $(("superuser", "admin")[$IsWin])."
}
# --- END: Conditional SELF-ELEVATION CODE

# GETTING HERE MEANS THAT ELEVATION HAS BEEN PERFORMED.
# Print the arguments passed in diagnostic form.
[PSCustomObject] @{
  PSBoundParameters = $PSBoundParameters.GetEnumerator() | Select-Object Key, Value, @{ n='Type'; e={ $_.Value.GetType().Name } } | Out-String
  # Only applies to non-advanced scripts
  Args =  $args | ForEach-Object { [pscustomobject] @{ Value = $_; Type = $_.GetType().Name } } | Out-String
} | Format-List

I figured out a really short solution:

if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
    if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
        $CommandLine = "-NoExit -c cd '$pwd'; & `"" + $MyInvocation.MyCommand.Path + "`""
        Start-Process powershell -Verb runas -ArgumentList $CommandLine
        Exit
    }
}

#Elevated script content after that

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM