Relaunching PowerShell script as admin user - powershell

I have a quite a few computer systems which we need to deploy software. I've been using a simple method for detecting if a user is a local admin, then detecting if they have admin rights. If needed, the script relaunches with elevated privileges. If the user is not a local admin, the script relaunches using a different credentials (local admin). The script works great on systems which have a later version of PowerShell such as Windows 8 and Windows 10.
The problem is when the user is not an admin and the script is running on Windows 7. The script uses $PSScriptPath to relaunch the script. I don't think this works in earlier versions of PowerShell. So I tried setting $PSScriptRoot myself if the Major PowerShell version is < 3. The problem is then the script gets stuck in a loop of some sort where it just constantly opens and closes windows and then I have to kill it... If I don't define $PSScriptRoot I get the error
Cannot bind argument to parameter 'Path' because it is null
I assume this is because $PSScriptRoot isn't defined in PowerShell 2.0.
Here's an example of what I'm trying to do:
#Check if PowerShell version is greater than 2. If not, set $PSSriptRoot.
if ($PSVersionTable.PSVersion.Major -lt 3) {
$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition
}
#Check if the user is a local admin. If they are, set $LocalAdmin to $True.
$LocalAdmin = $false
if ((net localgroup administrators) -match ([System.Environment]::UserDomainName + "\\" + [System.Environment]::Username)) {
$LocalAdmin = $true
}
if ($LocalAdmin) {
#Check if the local admin needs to run the script as administrator
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
$arguments = "& '" + $MyInvocation.MyCommand.Definition + "'"
Start-Process powershell -Verb runas -ArgumentList $arguments
break
}
} else {
#Not a local admin. Relaunch script as admin user.
Start-Process -Credential $credential (Join-Path $PSHome powershell.exe) -ArgumentList (#("-File",
(Join-Path $PSScriptRoot $MyInvocation.MyCommand)) + $args)
exit
}

Don't re-define automatic variables. Nothing good will come of it.
Besides, why do you want to anyway? The only thing you use $PSScriptRoot for is to reconstruct the script path you already have. Just assign that path to a variable and use that in your script.
$script = $MyInvocation.MyCommand.Definition
$ps = Join-Path $PSHome 'powershell.exe'
$isLocalAdmin = [bool]((net localgroup administrators) -match "$env:USERDOMAIN\\$env:USERNAME")
if ($isLocalAdmin) {
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]'Administrator')) {
Start-Process $ps -Verb runas -ArgumentList "& '$script'"
exit
}
} else {
Start-Process $ps -ArgumentList (#('-File', $script) + $args) -Credential $credential
exit
}

Related

Running PowerShell with admin credentials for users

I have a PowerShell script that restarts the Spooler service, then locates all printers with a specific name, removes those printers, then adds the printers back. I want to be able to go on a user's PC and run the script and input my admin credentials, but when I do, it doesn't find the printers because printers are per user not per PC. Is there a way to run the script as the user with elevated permissions in a single PS instance?
# Check if ps is running as admin otherwise open this script as admin
if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs; exit }
# Local printer name to search for
$FindName = "*DSM*"
# Store local printer object for later use
$Printers = Get-Printer -Name $FindName
if(!$Printers){
$NotFound = "No printer with the name "
$NotFound2 = " was found."
$NotFound + $FindName + $NotFound2
}else{
Write-Output "Printer found"
# Restart spooler service
Write-Output "Restarting Spooler"
Restart-Service -Name Spooler -Force
# Wait for Spooler to come back online
Start-Sleep -s 5
# loop through all printers found and re-add each one
foreach($Printer in $Printers){
# Remove printer
Write-Output "`nRemoving " $Printer.Name
Remove-Printer -Name $Printer.Name
# Add printer
Write-Output "Re-adding " $Printer.Name
Add-Printer -ConnectionName $Printer.Name
}
Read-Host -Prompt "Press Enter to exit"
}
Basically, you need to run the service management pieces as an admin user (presumably, end users don't have admin on their workstations) but the printer management must happen from the end user's account. You'll essentially use Start-Process to run the service management piece as your admin account, but let the rest of the script run in the end user's context:
# Restart spooler Service
# Splatting used here for readability
$psPath = "$env:SystemRoot/System32/WindowsPowerShell/v1.0/powershell.exe"
$spArgs = #{
Wait = $True
Credential = Get-Credential DOMAIN.tld\adminuser
FilePath = $psPath
ArgumentList = '-Command "$p = Start-Process -PassThru -Wait -FilePath \"{0}\" -Verb RunAs -ArgumentList \"Restart-Service -Force -EA Stop spooler\"; exit $p.ExitCode"' -f $psPath
PassThru = $True
ErrorAction = 'Stop'
}
$p = Start-Process #spArgs
if( $p.ExitCode -ne 0 ) {
# The service failed to restart. Handle the failure case.
}
The way this works:
Run a new powershell.exe process as your admin user, then making sure to elevate permissions by running powershell.exe a second time with the RunAs Verb. You will need to input the credential each time the script is run.
As indicated in the comments, -Credential and -Verb are mutually exclusive.
There is not really a graceful way to do this, to make this most readable I have use a literal string with the format operator -f to avoid an even worse escape-hell.
The -Command parameter needs to be provided within double-quotes when executed via Start-Process, or else it will only render the literal string and not actually execute the nested command.
The youngest PowerShell process will perform the service restart.
Use the -Wait flag to wait until the process exits for both invocations of Start-Process. Start-Process does not block execution by default regardless of application type.
Make Restart-Service throw a terminating error with -EA Stop if it encounters a problem. This will guarantee a non-zero exit code is returned on failure without requiring additional boilerplate code.
Specifying the full path to powershell.exe is optional but a general good practice. You could also simply use powershell.exe in place of the full path to the binary.
Programs run with Start-Process do not set $LASTEXITCODE. Therefore, we return the Process object with -Passthru, then check that its exit code is 0. Any other exit code indicates failure for this operation.
You can remove your admin check from the script since this no longer needs administrative permissions, and Get-Credential will essentially elevate for the service restart.
Note that if you want to perform this from a user who can elevate but you don't want the PowerShell script itself to run elevated, you can run the script without elevation and use the technique above but omit the -Credential parameter which becomes redundant. It would still work but no reason to authenticate when you don't have to.

Trying to check for a powershell module, install if it doesn't exist, then rerun the same script

So I am trying to write a script that will check if the user has a certain module installed, and if it doesn't, to install it, then rerun itself.
When I try to run this, the script just keeps rerunning and trying to install. I have to use a setup.exe and I have it waiting for the window to cl
$mypath = $MyInvocation.MyCommand.Path
$Path = Split-Path $mypath -Parent
$Location = "$Path" + "\setup.exe"
if (Get-Module -ListAvailable -Name ActiveRolesManagementShell) {
Write-Host "QAD Is installed"
pause
}
else{
Write-Host "Installing QAD"
Start-Process $Location
Wait-Process -Name "setup"
Pause
$CommandLine = "-File `"" + $MyInvocation.MyCommand.Path + "`" " + $MyInvocation.UnboundArguments
Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine
Pause
}
Normally, modules are installed using Install-Module. Then you might need to tell PowerShell about it for the current session to be able to use it.
if (-not(Get-Module -ListAvailable -Name ActiveRolesManagementShell)) {
Install-Module ActiveRolesManagementShell
Import-Module ActiveRolesManagementShell # Might not be needed
}
# Add code that uses ActiveRolesManagementShell module
But with regards to ActiveRolesManagementShell, what version are you using?
The old Quest module was probably meant to work on PowerShell v2. And I can't find a current version...

Unable to install program in self extracting cabinet using Invoke-Command

I'm writing a script to set up a test SharePoint server for trusted (AD FS) authentication on a stamped test environment that consists of a SharePoint server (server 2016) and a domain controller (server 2008R2). I'm writing the script to run on the SharePoint server and use a remote session to configure the DC because the DC only has PowerShell 2.0 which is missing some convenient functionality.
I have a specific segment of the script that runs a script block on the DC which downloads the AD FS 2.0 installer, a self extracting cabinet, and tries to install it. Every line of the block executes except for the actual installation. If I log onto the machine and run those same lines it works perfectly.
Invoke-Command -Session $domainControllerSession -ScriptBlock {
$installerUrl = "https://download.microsoft.com/download/F/3/D/F3D66A7E-C974-4A60-B7A5-382A61EB7BC6/RTW/W2K8R2/amd64/AdfsSetup.exe"
$filename = "$($PWD.Path)\AdfsSetup.exe"
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($installerUrl, $filename)
Start-Process -FilePath $filename -ArgumentList "/quiet" -Wait
}
I tried manually extracting the contents (using /x:) and then executing the setup file but there was no change in result (Note: The files are extracted but the extractor process never exits, this doesn't seem pertinent to the problem). I also moved to the DC and created a session to localhost and got the same exact behavior.
PS C:\Users\Administrator> $session = New-PSSession -ComputerName Localhost
PS C:\Users\Administrator> Invoke-Command -Session $session -ScriptBlock {
>> $filename = "$($PWD.Path)\AdfsSetup.exe"
>> write-host $filename
>> Test-Path -Path $filename
>> Start-Process -FilePath $filename -ArgumentList "/quiet" -Wait
>> Test-Path -Path 'C:\Program Files\Active Directory Federation Services 2.0'
>> }
>>
C:\Users\Administrator\Documents\AdfsSetup.exe
True
False
PS C:\Users\Administrator>
Update 1
I ran the process with the /Logfile parameter and confirmed that the installation is failing due to an access denied error. I've also confirmed that, as expected, the remote session is running under the same administrator account that I am using to initiate the session. I am assuming that the missing ingredient here is that the remote session is not running in an elevated shell. However, I can't seem to get that working either.
Invoke-Command -Session $session -ScriptBlock {
Start-Process PowerShell -Verb RunAs -ArgumentList "& C:\Users\Administrator\Documents\AdfsSetup.exe /quiet /Logfile C:\Users\Administrator\Documents\AdfsSetup.log" -Wait -PassThru
}
The error is the same.

Creating a simple PowerShell based installer with elevated privileges

I am trying to make a script that performs a bunch of installation operations, including executing other files, such as .reg files to update the registry. I created a batch file to kick it off and have code in the powershell script to self elevate to run as admin. The problem is that once run as admin, the working path is C:\Windows\system32, after which the script cannot file to other (relative) files it needs to run.
Install.bat:
powershell InstallSteps.ps1
InstallSteps.ps1:
param([switch]$Elevated)
function Test-Admin {
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
if ((Test-Admin) -eq $false) {
if ($elevated)
{
# tried to elevate, did not work, aborting
}
else {
Start-Process powershell.exe -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition))
}
exit
}
# THIS SHOWS THE PATH IS "C:\WINDOWS\SYSTEM32"
Write-Host $pwd
# THIS FAILS, BECAUSE IT CAN'T FIND THE FILE
reg IMPORT EnableAllTrustedApps.reg
I have not found a way to pass in the relative path to these to make into the working path. It seems the "Start-Process" method of elevating loses all context of where the script was originally located.
I recommend always using this function to call external things. It's more reliable than relative paths.
function Get-Script-Directory
{
$scriptInvocation = (Get-Variable MyInvocation -Scope 1).Value
return Split-Path $scriptInvocation.MyCommand.Path
}
e.g.
$script_home = Get-Script-Directory
reg.exe IMPORT "$script_home\EnableAllTrustedApps.reg"
Note - in 3.0 or above you can use this new built-in variable instead of the function.
$PSScriptRoot
You might also want to try specifying the -WorkingDirectory parameter for Start-Process e.g. Start-Process -FilePath powershell.exe -verb RunAs -WorkingDirectory $PWD.Path

Run powershell commands in parallel

How can I run a PowerShell script in parallel on multiple computers?
$TempFolder = "C:\DOWNLOADED_APPLICATIONS"
if(!(test-path $TempFolder))
{New-Item -path $TempFolder -type directory}
Write-Host ""
Write-Host "Downloading .NET framework v4.0 installation package" -ForegroundColor Yellow;
$src = "\\onesoul\tools\DOTNET45\dotNetFx45_Full_x86_x64.exe"
$dest = "$TempFolder" + "\" + "dotNetFx45_Full_x86_x64.exe"
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($src, $dest)
$args = " /q /norestart"
Write-Host "Installing .NET framework v4.0" -ForegroundColor Yellow;
Start-Process -FilePath $dest -ArgumentList $args -Wait
Write-Output "Dot net 4.5 installed"
This script works fine if I run it remotely on one computer at a time. How do I make it run in parallel?
To run scripts on multiple remote computers you use PowerShell remoting. This requires that every computer you connect to has to have PowerShell remoting enabled which you do with the command:
Enable-PSRemoting -Force
Then from your PC, running an elevated console, use the Invoke-Command to run a script against multiple computers e.g.:
Invoke-Command -Computer server1,server2,server3 -Auth CredSSP `
-FilePath c:\script.ps1 -ArgumentList scriptParameter1, scriptParameter2
I suggest you use the -Auth CredSSP to avoid a second hop issue with your credentials since you're accessing a network share from the remote computer. Also the FilePath parameter will take the path to a local script and copy its contents across the wire to each remote computer. That script should not rely on other scripts unless they exist on all the remote computers. Finally, if you use your credentials, you should admin privs on all the remote computers. If you don't, use the -Credential parameter to provides credentials for an account that does.