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:
is the following monstrosity (I certainly wish this were easier):
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.