0

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...
2
  • Does your script begin with [CmdletBinding()]? If you don't have the CmdletBinding, it doesn't know about -Debug. Commented Oct 19, 2022 at 18:33
  • I just now tried that and it didn't work. I still have to manually set the value of a $Debug variable with the hack I found. Commented Oct 19, 2022 at 18:45

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.