Reputation: 349
I've been writing a powershell script that needs to self-elevate to admin. Self-elevation is the very last function I've added to the script after debugging the rest of it, and I get a type error when passing the parameters into the script. What seems to be happening is that during the self-elevation process, the boolean [System.Management.Automation.SwitchParameter]
type of the -Debug
parameter's value is getting typecast to [string]
, and I can't figure out a way to have it re-cast as type [bool]
. I get a similar error if the script somehow captures a whitespace string for the -NewIpdb
parameter, except it throws a validation error against the [System.IO.FileInfo]
type, even when the parameter hasn't been explicitly invoked by the user. I don't know how to make a powershell script not positionally capture arguments into named parameters if they are not explicitly invoked.
I'm using a solution found here to build a string of the original user-invoked parameters to pass into a modified version of this self-elevation solution but this comment on that answer only ambiguously advises that I would have to be "clever" about how I build the ArgumentList
. I have attempted use of the -Command
parameter for powershell.exe
as this post suggests but I still get the type error even with a few different methods of formatting the string to have it interpreted as a command expression. You can also see that I have already attempted to explicitly capture the True|False
values that switch parameters take and prefix them with a dollar sign to turn them into literal $true|$false
to no avail.
EDIT 1
I also just tried this solution that I saw suggested in the sidebar after posting this question, in combination with the capture-true/false trick to send only the switch parameter name and not an assigned value. Instead of getting an error in the admin powershell instance, it just straight-up quits.
TIDE
I'm clearly not "clever" and I am at an impasse and I need help.
Invocation (in user-level powershell window):
PS C:\Users\myname\Documents> .\changeip.ps1 -Debug
C:\Users\myname\Documents\changeip.ps1 -Debug:$True
[Debug, True]
PS C:\Users\myname\Documents>
PS C:\Users\myname\Documents> $PSVersionTable
Name Value
---- -----
PSVersion 5.1.19041.1682
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.1682
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
The error in the Admin-level powershell window:
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Try the new cross-platform PowerShell https://aka.ms/pscore6
C:\Users\myname\Documents\changeip.ps1 : Cannot convert 'System.String' to the type
'System.Management.Automation.SwitchParameter' required by parameter 'Debug'.
+ CategoryInfo : InvalidArgument: (:) [changeip.ps1], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : CannotConvertArgument,changeip.ps1
PS C:\Windows\system32>
Relevant code, copy-pasted directly from the script:
# Parameters for command line usage, because some users may prefer to use this script in a command line lol
Param(
[Parameter(HelpMessage="Path to new IP database file for script to use")]
# https://4sysops.com/archives/validating-file-and-folder-paths-in-powershell-parameters/
[ValidateScript({
# If the valid-formatted path does not exist at all, throw an error
if( -Not ($_ | Test-Path) ){
throw "File does not exist"
}
# If the valid-formatted path does not point to a file, throw an error
if( -Not ($_ | Test-Path -PathType Leaf) ){
throw "Argument must point to a file"
}
# Finally, if the valid-formatted path does not point to a JSON file, specifically, throw an error
if($_ -notmatch "\.json"){
throw "Argument must point to a JSON file"
}
return $true
})] # Gotta catch 'em all! (The bracket types, that is)
# Data type that rejects invalid Windows file paths with illegal characters
[System.IO.FileInfo]$NewIpdb,
[Parameter(HelpMessage="A custom IP configuration string in the format IP,Netmask[,Gateway]")]
[ValidateScript({
# https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
# Shortest validation regex used and modified slightly to validate a CSV list of 2-3 IPs
# This regex is reused in a helper function down below, but I can't use the function here in the Param() block for ease of input validation
if($_ -notmatch "^(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4},?){2,3}$"){
throw "A comma-separated string of valid IP addresses must be provided in the order: IP,Netmask[,Gateway]"
}
return $true
})]
[string]$SetIP,
# A simple true/false flag that can reset the IP configuration
[Parameter(HelpMessage="Reset the network interface configured for this script to automatic DHCP configuration. Does not take an argument.")]
[switch]$Reset,
# A true/false flag that can restart the network interface
[Parameter(HelpMessage="Restart the network interface configured for this script. Does not take an argument.")]
[switch]$Restart,
# Used for elevation to admin privileges if script invoked without
# DO NOT INVOKE THIS FLAG YOURSELF. THIS FLAG GETS INVOKED INTERNALLY BY THIS SCRIPT.
[Parameter(HelpMessage="Used internally by script. Script MUST run with admin privileges, and attempts to self-elevate if necessary. This flag indicates success.")]
[switch]$Elevated
# Add parameters: -ListConfigs -SetConfig
)
# https://stackoverflow.com/questions/9895163/in-a-cmdlet-how-can-i-detect-if-the-debug-flag-is-set
# The -Debug common parameter doesn't set the value of a $Debug variable unlike user-defined parameters
# So this manual hack is here to fix that :/
$Debug = $PsBoundParameters.Debug.IsPresent
# https://stackoverflow.com/questions/21559724/getting-all-named-parameters-from-powershell-including-empty-and-set-ones
$parameters = ""
foreach($key in $MyInvocation.BoundParameters.keys) {
$parameters += "-" + $key + ":" + ("","$")[$MyInvocation.BoundParameters[$key] -imatch "true|false"] + $MyInvocation.BoundParameters[$key] + " "
}
#if($Debug) {
Write-Host $MyInvocation.MyCommand.Definition $parameters
Write-Host $MyInvocation.BoundParameters
#}
# Next two blocks are almost verbatim copypasta'd from:
# https://superuser.com/a/532109
# Modified slightly to add user-invoked parameters to the argument list
# Function to test if the current security context is Administrator
function Test-Admin {
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
# If the script is not running as Administrator...
if ((Test-Admin) -eq $false) {
# Check if elevation attempt has been made
if ($elevated) {
# tried to elevate, did not work, aborting
throw "Unable to elevate to Administrator privileges. This application cannot perform its designed function. Aborting."
}
else { # Try to elevate script
Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" {1} -elevated' -f ($myinvocation.MyCommand.Definition), $parameters)
}
exit
}
# 1260 more lines of code past this point, most of it building Windows Forms...
Upvotes: 0
Views: 207
Reputation: 349
After several months of distraction from this bug, I have solved it!
The solution I implemented is manually re-building the $MyInvocation.BoundParameters
dictionary post-elevation to admin:
First, a catch-all parameter is required that can be used internally on self-elevation to admin, and then immediately following the parameters block, a conditional block that rebuilds the BoundParameters
dictionary:
Param(
# Generic catch-all parameter
[Parameter()]
[string]$ElevatedParams,
# All other parameters follow...
)
# Check if $ElevatedParams even has a length to begin with (empty strings are falsy in PowerShell)
# Alternatively, this can check if $elevated == $true
if($ElevatedParams) {
# The string of parameters carried over through self-elevation has to be converted into a hash for easy iteration
# The resulting hash must be stored in a new variable
# It doesn't work if one attempts to overwrite $ElevatedParams with a completely different data type
$ElevatedHash = ConvertFrom-StringData -StringData $ElevatedParams
# Loop through all carried-over parameters
foreach($key in $ElevatedHash.Keys) {
try { # Try to parse the keyed value as a boolean... this captures switch parameters
$value = [bool]::Parse($ElevatedHash[$key])
}
catch { # If an error is thrown in the try block, the keyed value is not a boolean
$value = $ElevatedHash[$key]
}
finally { # Finally, push the key:value pair into the BoundParameters dictionary
$MyInvocation.BoundParameters.Add($key, $value)
}
}
}
But now, we have to make sure that we actually have a data string of the original parameters and their values. This can be built with a foreach loop somewhere between the above statements and the self-elevation code:
$parameters = ""
foreach($key in $MyInvocation.BoundParameters.keys) {
$parameters += ("`n","")[$parameters.Length -eq 0] + $key + "'" + $MyInvocation.BoundParameters[$key]
}
And then comes the guts of self-elevation:
# Function to test if the current security context is Administrator
function Test-Admin {
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
# If the script is not running as Administrator...
if ((Test-Admin) -eq $false) {
# Check if elevation attempt has been made
if ($elevated) {
# tried to elevate, did not work, aborting
throw "Unable to elevate to Administrator privileges. This application cannot perform its designed function. Aborting."
}
else { # Try to elevate script
Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -ElevatedParams "{1}" -elevated' -f ($myinvocation.MyCommand.Definition), $parameters)
}
exit
}
If your script is not as dependent on $MyInvocation.BoundParameters
having a particular parameter value set prior to the if ((Test-Admin) -eq $false)
statement like my script currently is, all of the above can be refactored as follows:
# Start of script
Param(
# Generic catch-all parameter
[Parameter()]
[string]$ElevatedParams,
# All other parameters follow...
)
function Rebuild-BoundParameters {
Param([string]$ParameterDataString)
# The string of parameters carried over through self-elevation has to be converted into a hash for easy iteration
# The resulting hash must be stored in a new variable
# It doesn't work if one attempts to overwrite $ParameterDataString with a completely different data type
$ParameterHash = ConvertFrom-StringData -StringData $ParameterDataString
# Loop through all carried-over parameters; does not execute if there are no parameters in the hash
foreach($key in $ParameterHash.Keys) {
try { # Try to parse the keyed value as a boolean... this captures switch parameters
$value = [bool]::Parse($ParameterHash[$key])
}
catch { # If an error is thrown in the try block, the keyed value is not a boolean
$value = $ParameterHash[$key]
}
finally { # Finally, push the key:value pair into the BoundParameters dictionary
$MyInvocation.BoundParameters.Add($key, $value)
}
}
}
# Function to test if the current security context is Administrator
function Test-Admin {
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
# If the script is not running as Administrator...
if ((Test-Admin) -eq $false) {
# Check if elevation attempt has been made
if ($elevated) {
# tried to elevate, did not work, aborting
throw "Unable to elevate to Administrator privileges. This application cannot perform its designed function. Aborting."
}
else { # Try to elevate script
# Build the parameter data string first
$parameters = ""
foreach($key in $MyInvocation.BoundParameters.keys) {
$parameters += ("`n","")[$parameters.Length -eq 0] + $key + "'" + $MyInvocation.BoundParameters[$key]
}
Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -ElevatedParams "{1}" -elevated' -f ($myinvocation.MyCommand.Definition), $parameters)
}
exit
}
else { # Script is running as Administrator
Rebuild-BoundParameters $ElevatedParams
}
The above solution may require modification if passing a file path in $ElevatedParams
to properly substitute the backslashes with escaped backslashes, and other such modifications, but the solution works so far.
Upvotes: 0