#Self Elevating Command

1 messages · Page 1 of 1 (latest)

timber salmon
#

I'm wondering whether it's possible to create a self-elevating command?

Imagine a codeblock as follows:

function Foobar {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string]
        $ArgOne,
        [Parameter(Position = 1, Mandatory = $true)]
        [string]
        $ArgTwo,
        [Parameter(Position = 2, Mandatory = $true)]
        [string]
        $ArgThree
    )

    if (RunAsAdministrator { Foobar $ArgOne $ArgTwo $ArgThree }) {
        return
    }

    Write-Host "From an elevated prompt..."
}

The function I've been trying to write is RunAsAdministrator and it would work as follows:

  • If the shell is elevated, it returns false, which means the write-host below will run.
  • If the shell is not elevated, it runs the codeblock in an elevated shell and returns true, which means the write-host will not run a second time.

Simplified call graph:

Foobar (unlevated)
-> RunAsAdministrator (unelevated)
  -> Foobar (elevated in new shell, prints message)

But I just can't figure out how to get RunAsAdministrator working. This is currently what it looks like:

function RunAsAdministrator {
    param(
        [Parameter(Position = 0)]
        [scriptblock]
        $Command
    )

    if (Test-AdministratorStatus) {
        $false
    }
    else {
        Start-Process "pwsh" -Verb RunAs -ArgumentList @(
            # temp flags for debugging only
            "-NoExit",
            "-NoProfile",
            "-Interactive",
            # This doesn't work!
            "-Command { Invoke-Command -ScriptBlock {$Command} }"
            
        )
        $true
    }
}

When I try to run this, the elevated pwsh prompt pops up but it just displays the following text rather than running anything

 Invoke-Command -ScriptBlock { Foobar $ArgOne $ArgTwo $ArgThree }

My question is, is this even possible to achieve, and if so, how?

void grotto
#

There is also Start-Process -Verb RunAs which is what sudo.ps1 does.

#

I prefer gsudo . Some of these types of patterns open various risk on your machine.

#

Also there is no need for Invoke-Command Just do -Command { 'my code here' }

timber salmon
finite ivy
#

trying to provide a scriptblock as a string argument like that can be tricky due to command line escaping happening

#

it's easiest to do something like -EncodedCommand but that has other drawbacks as well

void grotto
#

But yeah like jb says, it appears you are in "quoting hell".

#

this works. so you probably have to remove the {}

start pwsh -Args '-nop','-noe', '-command ps | select -f 1 '
#

But that only gets you so far...

#

there are so many nuances with pwsh and powershell and cmd parsing. it gets complicated really fast.

timber salmon
#

I've gotten the scriptblock to actually run now, but I'm still having trouble with evaluating arguments.

function Test {
    param(
        [string]
        $Value
    )
    RunAsAdministrator({ Write-Host $value })
}
function RunAsAdministrator {
    param(
        [Parameter(Position = 0)]
        [scriptblock]
        $Command
    )

    $bytes = [System.Text.Encoding]::Unicode.GetBytes("& { Write-Host 'beginning'; $Command; Write-Host 'done' }")
    $e = [Convert]::ToBase64String($bytes)
    Start-Process "pwsh" -Verb RunAs -ArgumentList @(
        "-NoExit",
        "-Interactive",
        "-EncodedCommand $e"
    )
}

Produces the output in the elevated shell:

beginning

done

So the Write-Host $value is being executed (as evident by the blank line). However, given it prints nothing, it means $Value is not present in the spawned shell. Which makes sense.

void grotto
#

we need a much simpler example to work with first off.

#

have you looked at sudo.ps1? any reason you want to reinvent the wheel? is this for learning?

void grotto
timber salmon
void grotto
#

so i would recommend tearing out all the functions and using code like the below to get what you want.

start pwsh -Args '-nop','-noe', '-command ps | select -f 1; write-host hi'
#

and then build up from there.

finite ivy
#

Another option is to use Base64 to encode your script and parameters like so

Function Invoke-AsAdmin {
    [OutputType([int])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ScriptBlock]
        $ScriptBlock,

        [Parameter()]
        [System.Collections.IDictionary]
        $Param = @{},

        [Parameter()]
        [switch]
        $IgnoreExitCode,

        [Parameter()]
        [switch]
        $PassThru
    )

    $payload = @{
        S = $ScriptBlock.ToString()
        P = $Param
    } | ConvertTo-Json -Compress -Depth 99
    $payloadB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($payload))

    $cmd = '$m=ConvertFrom-Json ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(''{0}''))) -AsHashtable;$p=$m.P;& ([ScriptBlock]::Create($m.S)) @p' -f $payloadB64
    $procParams = @{
        FilePath = "$PSHome\pwsh.exe"
        ArgumentList = "-Command $cmd"
        Verb = 'RunAs'
        Wait = $true
        PassThru = $true
    }
    $proc = Start-Process @procParams
    if ($proc.ExitCode -and -not $IgnoreExitCode) {
        $err = [System.Management.Automation.ErrorRecord]::new(
            [Exception]::new("Elevated process failed with exit code $($proc.ExitCode)"),
            "NonZeroExitCode",
            [System.Management.Automation.ErrorCategory]::InvalidResult,
            $proc.ExitCode)
        $PSCmdlet.ThrowTerminatingError($err)
    }

    if ($PassThru) {
        $proc.ExitCode
    }
}

The downside of this approach is that you have a command line length limit so you can only do this for smaller scriptblocks.

#

An example of this with your Foobar is

function Foobar {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string]
        $ArgOne,
        [Parameter(Position = 1, Mandatory = $true)]
        [string]
        $ArgTwo,
        [Parameter(Position = 2, Mandatory = $true)]
        [string]
        $ArgThree
    )

    $isAdmin = ([System.Security.Principal.WindowsPrincipal][System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
        [System.Security.Principal.WindowsBuiltInRole]::Administrator)

    if (-not $isAdmin) {
        Invoke-AsAdmin $function:Foobar $PSBoundParameters
        return
    }

    Write-Host "From an elevated prompt - ArgOne '$ArgOne' ArgTwo '$ArgTwo' ArgThree '$ArgThree'"
    Read-Host -Prompt "Press enter to exit"
}

Foobar -ArgOne one -ArgTwo two -ArgThree three
timber salmon
#

That's amazing! Works exactly as I'd like. The scriptblock limit won't be a problem either for my use cases.

#

The solution I came up with is much less elegant, but works somewhat similarly. Just a bit more crude I guess:

if (-not $isAdmin) {
    RunAsAdmin "Foobar" $ArgOne $ArgTwo $ArgThree
}

Using $PSBoundParameters is much nicer as I don't have to worry about the passed parameters falling out of sync with the function param block.

I thought there had to be a way to achieve this elegantly given how flexible powershell is, but it can be quite difficult to find relevant search results for advanced powershell concepts. Especially given the somewhat unique terminology powershell uses. I would liken this case of parameter passing to partial function application or similar functional programming concepts, but nothing relevant shows up for powershell. I also completely forgot about the $function:whatever notation.

#

Many thanks for the help!

finite ivy
#

with the new sudo tool from Windows this could even be expanded to get the output back to the limited process as well allowing you to have some form of complex object exchange

timber salmon
#

That would be cool. I may have a look into that, though for now I'm not too fussed about getting the elevated output back.

worthy plinth
void grotto
#

gsudo comes with an module function that dumps the objects back.

#

i'm kind of curious about OPs original method and jb's method. Not sure the benefit or difference. gsudo and sudo can both prevent input and output and overtaking the elevated process, I believe.

#

sudo is more of an interactive cli deal, right? If there is some automation going in, that would just run as admin automatically?

worthy plinth
#

with "real" sudo you can allow stuff on a much more granular level without requiring a password

void grotto
#

yeah but OPs whole inversion of the typical use case. like just call myfunction instead of sudo myfunction.

#

Just a different way to do it, I suppose. I prefer just doing sudo { myfunction }

void grotto
#

never heard or seen anyone use JEA really though

worthy plinth
#

yea, calling the windows version of sudo is pretty confusing because it really isn't anything like the linux variant

void grotto
#

yeah. do you use gsudo or the like? how do you quickly elevate on windows ?

worthy plinth
#

nah i dont use gsudo, sometimes i use sudo but if i need to do some stuff with admin i just open a terminal as admin.. its pretty rare that i need local admin

void grotto
#

i'll need admin for enable-wsmancredssp or the like

#

or choco upgrades.

timber salmon
#

I have a few functions for customizing windows/changing settings that can't be done through gui. They often need elevation to e.g. modify registry keys. I always forget they need elevation, and it started to piss me off how I forget each time I rarely use them and have to restart the shell as admin etc etc. So I thought, surely, I can make the function, if it detects its not elevated, spawn an elevated shell and run itself again.

#

Why didn't I consider I could just do {press up} {press home} type sudo at the front and run again? Great question... ¯_(ツ)_/¯
That would've been simpler, albeit requiring a few more keypresses to run. So really, this is an extremely over-engineered solution to a minor ergonomics problem. I have noticed I seem to have a tendency to squeez out every micro ergonomics gain no matter the difficulty of implementation, for better or worse lol

#

For actual automation this is pretty bonkers obviously

void grotto
#

yeah i hear that. i've got a couple functions that just wrap the application into a call with sudo.

timber salmon
#

My dream wish would be for windows to support elevating a process alive, without having to start a new instance. But I know that's never gonna happen so...

void grotto
#

i've never seen the whole json encoded deal before so that is pretty neat.