diff --git a/src/gsudo.Wrappers/Invoke-ElevatedCommand.ps1 b/src/gsudo.Wrappers/Invoke-ElevatedCommand.ps1 new file mode 100644 index 00000000..10635c0a --- /dev/null +++ b/src/gsudo.Wrappers/Invoke-ElevatedCommand.ps1 @@ -0,0 +1,182 @@ +<# +.SYNOPSIS +Executes a ScriptBlock or script file in a new elevated instance of PowerShell using gsudo. + +.DESCRIPTION +The Invoke-ElevatedCommand cmdlet serializes a ScriptBlock or script file and executes it in an elevated PowerShell instance using gsudo. The elevated command runs in a separate process, which means it cannot directly access or modify variables from the invoking scope. + +The cmdlet supports passing arguments to the ScriptBlock or script file using the -ArgumentList parameter, which can be accessed with the $args automatic variable within the ScriptBlock or script file. Additionally, you can provide input from the pipeline using $Input, which will be serialized and made available within the elevated execution context. + +The result of the elevated command is serialized, sent back to the non-elevated instance, deserialized, and returned. This means object graphs are recreated as PSObjects. + +Optionally, you can check for "$LastExitCode -eq 999" to determine if gsudo failed to elevate, such as when the UAC popup is cancelled. + +.PARAMETER ScriptBlock +Specifies a ScriptBlock to be executed in an elevated PowerShell instance. For example: { Get-Process Notepad } + +.PARAMETER FilePath +Specifies the path to a script file to be executed in an elevated PowerShell instance. + +.PARAMETER ArgumentList +Provides a list of elements that will be accessible inside the ScriptBlock or script as $args[0], $args[1], and so on. + +.PARAMETER LoadProfile +Loads the user profile in the elevated PowerShell instance, regardless of the gsudo configuration setting PowerShellLoadProfile. + +.PARAMETER NoProfile +Does not load the user profile in the elevated PowerShell instance. + +.PARAMETER RunAsTrustedInstaller +Runs the command as the TrustedInstaller user. + +.PARAMETER RunAsSystem +Runs the command as the SYSTEM user. + +.PARAMETER ClearCache +Clears the gsudo cache before executing the command. + +.PARAMETER NewWindow +Opens a new window for the elevated command. + +.PARAMETER KeepNewWindowOpen +Keeps the new window open after the elevated command finishes execution. + +.PARAMETER KeepShell +Keeps the shell open after the elevated command finishes execution. + +.PARAMETER NoKeep +Closes the shell after the elevated command finishes execution. + +.PARAMETER InputObject +You can pipe any object to this function. The object will be serialized and available as $Input within the ScriptBlock or script. + +.INPUTS +System.Object + +.OUTPUTS +The output of the ScriptBlock or script executed in the elevated context. + +.EXAMPLE +Invoke-ElevatedCommand { return Get-Content 'C:\My Secret Folder\My Secret.txt' } + +.EXAMPLE +Get-Process notepad | Invoke-ElevatedCommand { $input | Stop-Process } + +.EXAMPLE +$a = 1; $b = Invoke-ElevatedCommand { $args[0] + 10 } -ArgumentList $a; Write-Host "Sum returned: $b" +Sum returned: 11 + +.EXAMPLE +Invoke-ElevatedCommand { Get-Process explorer } | ForEach-Object { $_.Id } + +.LINK +https://github.com/gerardog/gsudo +#> +[CmdletBinding(DefaultParameterSetName='ScriptBlock')] +param +( + # The script block to execute in an elevated context. + [Parameter(Mandatory = $true, Position = 0, ParameterSetName='ScriptBlock')] [System.Management.Automation.ScriptBlock] +[ArgumentCompleter( { param () + # Auto complete with last 5 ran commands. + $lastCommands = Get-History | Select-Object -last 5 | % { "{ $($_.CommandLine) }" } + + if ($lastCommands -is [System.Array]) { + # Last one first. + $lastCommands[($lastCommands.Length - 1)..0] | % { $_ }; + } + elseif ($lastCommands) { + # Only one command. + $lastCommands; + } + } )] + + $ScriptBlock, + + # Alternarive file to be executed in an elevated PowerShell instance. + [Parameter(Mandatory = $true, ParameterSetName='ScriptFile')] [String] $FilePath, + + [Parameter(Mandatory = $false)] [System.Object[]] $ArgumentList, + + [Parameter(ParameterSetName='ScriptBlock')] [switch] $LoadProfile, + [Parameter(ParameterSetName='ScriptBlock')] [switch] $NoProfile, + + [Parameter()] [switch] $RunAsTrustedInstaller, + [Parameter()] [switch] $RunAsSystem, + [Parameter()] [switch] $ClearCache, + + [Parameter()] [switch] $NewWindow, + [Parameter()] [switch] $KeepNewWindowOpen, + [Parameter()] [switch] $KeepShell, + [Parameter()] [switch] $NoKeep, + + [ValidateSet('Low', 'Medium', 'MediumPlus', 'High', 'System')] + [System.String]$Integrity, + + [Parameter()] [System.Management.Automation.PSCredential] $Credential, + [Parameter(ValueFromPipeline)] [pscustomobject] $InputObject +) +Begin { + $inputArray = @() +} +Process { + foreach ($item in $InputObject) { + # Add the modified item to the output array + $inputArray += $item + } +} +End { + $gsudoArgs = @() + + if ($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent) { $gsudoArgs += '--debug' } + + if ($LoadProfile) { $gsudoArgs += '--LoadProfile' } + if ($RunAsTrustedInstaller) { $gsudoArgs += '--ti' } + if ($RunAsSystem) { $gsudoArgs += '-s' } + if ($ClearCache) { $gsudoArgs += '-k' } + if ($NewWindow) { $gsudoArgs += '-n' } + if ($KeepNewWindowOpen) { $gsudoArgs += '--KeepWindow' } + if ($NoKeep) { $gsudoArgs += '--close' } + if ($Integrity) { $gsudoArgs += '--integrity'; $gsudoArgs += $Integrity} + + if ($Credential) { + $CurrentSid = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value; + $gsudoArgs += "-u", $credential.UserName + + # At the time of writing this, there is no way (considered secure) to send the password to gsudo. So instead of sending the password, lets start a credentials cache instance. + $p = Start-Process "gsudo.exe" -Args "-u $($credential.UserName) gsudoservice $PID $CurrentSid All 00:05:00" -credential $Credential -LoadUserProfile -WorkingDirectory "$env:windir" -WindowStyle Hidden -PassThru + $p.WaitForExit(); + Start-Sleep -Seconds 1 + } + + if ($PSVersionTable.PSVersion.Major -le 5) { + $pwsh = "powershell.exe" + } else { + $pwsh = "pwsh.exe" + } + + if ($ScriptBlock) { + if ($NoProfile) { + $gsudoArgs += '-d'; + $gsudoArgs += $pwsh; + $gsudoArgs += '-NoProfile'; + $gsudoArgs += '-NoLogo'; + + if ($KeepShell) { $gsudoArgs += '--NoExit' } + } else { + if ($KeepShell) { $gsudoArgs += '--KeepShell' } + } + + if ($myInvocation.expectingInput) { + $inputArray | gsudo.exe @gsudoArgs $ScriptBlock -args $ArgumentList + } else { + gsudo.exe @gsudoArgs $ScriptBlock -args $ArgumentList + } + } else { + if ($myInvocation.expectingInput) { + $inputArray | gsudo.exe @gsudoArgs -args $ArgumentList + } else { + gsudo.exe @gsudoArgs -d $pwsh -File $FilePath -args $ArgumentList + } + } +} diff --git a/src/gsudo/Commands/RunCommand.cs b/src/gsudo/Commands/RunCommand.cs index 3669cfc5..0302ef66 100644 --- a/src/gsudo/Commands/RunCommand.cs +++ b/src/gsudo/Commands/RunCommand.cs @@ -31,54 +31,67 @@ public async Task<int> Execute() InputArguments.RunAsSystem = true; } - bool isRunningAsDesiredUser = IsRunningAsDesiredUser(); - bool isElevationRequired = IsElevationRequired(); - bool isShellElevation = !UserCommand.Any(); // are we auto elevating the current shell? - - if (isElevationRequired & SecurityHelper.GetCurrentIntegrityLevel() < (int)IntegrityLevel.Medium) - throw new ApplicationException("Sorry, gsudo doesn't allow to elevate from low integrity level."); // This message is not a security feature, but a nicer error message. It would have failed anyway since the named pipe's ACL restricts it. - - if (isRunningAsDesiredUser && isShellElevation && !InputArguments.NewWindow) - throw new ApplicationException("Already running as the specified user/permission-level (and no command specified). Exiting..."); - - var elevationMode = GetElevationMode(); - - if (!isRunningAsDesiredUser) - commandBuilder.AddCopyEnvironment(elevationMode); - - commandBuilder.Build(); - - int consoleHeight, consoleWidth; - ConsoleHelper.GetConsoleInfo(out consoleWidth, out consoleHeight, out _, out _); - - var elevationRequest = new ElevationRequest() - { - FileName = commandBuilder.GetExeName(), - Arguments = commandBuilder.GetArgumentsAsString(), - StartFolder = Environment.CurrentDirectory, - NewWindow = InputArguments.NewWindow, - Wait = (!commandBuilder.IsWindowsApp && !InputArguments.NewWindow) || InputArguments.Wait, - Mode = elevationMode, - ConsoleProcessId = Process.GetCurrentProcess().Id, - IntegrityLevel = InputArguments.GetIntegrityLevel(), - ConsoleWidth = consoleWidth, - ConsoleHeight = consoleHeight, - IsInputRedirected = Console.IsInputRedirected - }; - - if (isElevationRequired && Settings.SecurityEnforceUacIsolation) - AdjustUacIsolationRequest(elevationRequest, isShellElevation); - - SetRequestPrompt(elevationRequest); - - Logger.Instance.Log($"Command to run: {elevationRequest.FileName} {elevationRequest.Arguments}", LogLevel.Debug); + string originalWindowTitle = Console.Title; + try + { + bool isRunningAsDesiredUser = IsRunningAsDesiredUser(); + bool isElevationRequired = IsElevationRequired(); + bool isShellElevation = !UserCommand.Any(); // are we auto elevating the current shell? - if (isRunningAsDesiredUser || !isElevationRequired) // already elevated or running as correct user. No service needed. - { - return RunWithoutService(elevationRequest); + if (isElevationRequired & SecurityHelper.GetCurrentIntegrityLevel() < (int)IntegrityLevel.Medium) + throw new ApplicationException("Sorry, gsudo doesn't allow to elevate from low integrity level."); // This message is not a security feature, but a nicer error message. It would have failed anyway since the named pipe's ACL restricts it. + + if (isRunningAsDesiredUser && isShellElevation && !InputArguments.NewWindow) + throw new ApplicationException("Already running as the specified user/permission-level (and no command specified). Exiting..."); + + var elevationMode = GetElevationMode(); + + if (!isRunningAsDesiredUser) + commandBuilder.AddCopyEnvironment(elevationMode); + + commandBuilder.Build(); + + int consoleHeight, consoleWidth; + ConsoleHelper.GetConsoleInfo(out consoleWidth, out consoleHeight, out _, out _); + + var elevationRequest = new ElevationRequest() + { + FileName = commandBuilder.GetExeName(), + Arguments = commandBuilder.GetArgumentsAsString(), + StartFolder = Environment.CurrentDirectory, + NewWindow = InputArguments.NewWindow, + Wait = (!commandBuilder.IsWindowsApp && !InputArguments.NewWindow) || InputArguments.Wait, + Mode = elevationMode, + ConsoleProcessId = Process.GetCurrentProcess().Id, + IntegrityLevel = InputArguments.GetIntegrityLevel(), + ConsoleWidth = consoleWidth, + ConsoleHeight = consoleHeight, + IsInputRedirected = Console.IsInputRedirected + }; + + if (isElevationRequired && Settings.SecurityEnforceUacIsolation) + AdjustUacIsolationRequest(elevationRequest, isShellElevation); + + SetRequestPrompt(elevationRequest); + + Logger.Instance.Log($"Command to run: {elevationRequest.FileName} {elevationRequest.Arguments}", LogLevel.Debug); + + if (isRunningAsDesiredUser || !isElevationRequired) // already elevated or running as correct user. No service needed. + { + return RunWithoutService(elevationRequest); + } + + return await RunUsingService(elevationRequest).ConfigureAwait(false); + } + finally + { + try + { + Console.Title = originalWindowTitle; + } + catch + { } } - - return await RunUsingService(elevationRequest).ConfigureAwait(false); } private static void SetRequestPrompt(ElevationRequest elevationRequest)