How to run w32tm in non-elevated powershell - powershell

I am writing a helper script that will go through a list of servers and verify they are in sync with the NTP. The script shall be run as a normal script on request by the facility operator and shall request for Admin credentials if the target is not in sync. We unfortunately cannot change the NTP configuration at the moment so we have to make workaround.
What I have right now (and it works beautifully if the script is run as administrator) is a command ("w32tm /query /status" of a remote computer) that is executed via "Invoke-Command" so I can pass it Admin credentials.
My idea was to avoid using WinRM since the hostname resolution is not working properly in our system (it requires some painful host-to-IP-and-back-to-proper-hostname resolution) which makes the WinRM useless.
The command w32tm can obtain status of a remote computer but it needs to be run as administrator for it.
In both cases (run as administrator and run as normal user and later providing the credentials) the $script is executed as domain\administrator (confirmed with the check of Admin role and the "WhoAmI" command) but the status is only obtained when the whole script is executed as administrator.
For the execution as normal user I receive the error:
The following error occurred: Access is denied. (0x80070005)
All machines I use obviously allow remote execution since it works with administrator user.
So basically my question is why is the "w32tm ..." command not allowed in the $script if the role of the user is appropriate (it is administrator role) for the task?
The part of the script which I can't resolve:
function synchronize_remote_machine ($target, $username, $cred)
{
$script = {
param( [String] $compName )
$user = [Security.Principal.WindowsIdentity]::GetCurrent();
$userIsAdmin = (New-Object Security.Principal.WindowsPrincipal $user).`
IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
if (-not $userIsAdmin)
{
Write-Warning "You do not have Administrator rights to run this script!`n
Please re-run this script as an Administrator!"
}
else
{
whoAmI
w32tm /query /status /computer:$compName
#w32tm /resync /rediscover /computer:$compName
}
}
# resync via WinRM
try
{
#execute_resync_command $target $username $cred
if ($username -eq 'Administrator')
{
# when run as admin
invoke-command -ScriptBlock $script -ArgumentList $target;
}
else
{
# for normal user the initalized credential cals is used
invoke-command -computername "localhost" -Credential $cred -ScriptBlock $script -ArgumentList $target
}
}
catch
{
Write-Error "Error executing resync command on host $target."# -foregroundcolor "red"
throw
}
}

Rather than (re-)running the script with elevated privileges, I'd grant the operators group the SeSystemtimePrivilege on those servers. You can do that either with a group policy or by running ntrights.exe from the Server 2003 Resource Kit Tools on each server:
ntrights +r SeSystemtimePrivilege -u DOMAIN\operators

Even if you execute it as administrator, do to try to run you script in an elevated process ?
You can acheive that using Start-Process CmdLet.
start-process 'c:\system32\WindowsPowerShell\v1.0\powershell.exe' -verb runas -argumentlist "-file YourScript.ps1"

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.

MSI installation command works locally, but not over PowerShell Remoting

I am trying to use Invoke-Command to install an MSI on a remote computer. I am creating the session using credentials for a domain admin account, and have verified it connects correctly.
# DEBUG: Test that the connection is running as admin.
$IsAdmin = Invoke-Command -Session $Session -ScriptBlock {[bool] (net session 2>$null)}
Write-Verbose "Is running as admin: $IsAdmin"
# Now we need to run the MSI install file.
$TempInstallFile = Join-Path -Path $TempDir -ChildPath $InstallFile
$Script = "Start-Process $TempInstallFile -Wait -ArgumentList '/quiet COLLECTORAGENTLIST=`"$CollectorIP`:$CollectorPort`" PORTPOOLRANGES=`"$Ports`" MAXPORTRANGESPERSESSION=""2"" PORTNUMPERALLOC=`"200`" LOGLEVEL=`"3`" SSLMODE=`"2`"'"
Write-Verbose "Installation command: $Script"
Invoke-Command -Session $Session -ScriptBlock {$Script}
The verbose output:
VERBOSE: PSSession to RDPServer created
VERBOSE: Is running as admin: True
VERBOSE: Installation command: Start-Process C:\temp\TSAgent_Setup_5.0.0301.msi -Wait -ArgumentList '/quiet COLLECTORAGENTLIST="REMOVED:8002" PORTPOOLRANGES="1024-49151" MAXPORTRANGESPERSESSION="2" PORTNUMPERALLOC="200" LOGLEVEL="3" SSLMODE="2"'
VERBOSE: PSSession to RDPServer closed
Nothing happens though. The program is not installed on the remote computer and there are no errors. When I copy the "Installation Command" directly from the output, however, and run it in an admin PowerShell on the remote machine, it installs the program correctly. So there must be some issue with the PowerShell remote session, no? I am fairly new to this, what am I missing here?

Run Invoke-Command in remote computer as administrator

I'm trying to run invoke-command to launch a powershell script in a powershell file on a remote computer. I'm using the credentials for a user with Administrator privilege. The command needs to be executed by running powershell as an administrator. There are licensing issues with the application that i'm trying to invoke using the powershell script, so i cannot change the credentials to Administrator but need to run with that particular user itself. I have tried using -RunAsAdministrator at the end of the Invoke-Command, but i got an error saying:
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
$command = {
cd Folder
C:\Folder\build.ps1
}
Invoke-Command -ComputerName $RemoteSystemIP -ScriptBlock $command -credential $Credentials1 -ErrorAction Stop -AsJob
I'm trying to execute this as a background job that's why i added the -AsJob parameter.
Its been several days and i haven't found a solution yet.
tl;dr
The only way to get a remote PowerShell session to execute elevated (with admin privileges) is to connect with a user account (either implicitly or via -Credential) that has admin privileges on the target machine.
With such an account, the session automatically and invariably runs elevated.
The Invoke-Command's -RunAsAdministrator switch can only be used with (virtualization) containers (-ContainerId parameter), not regular remoting (-ComputerName parameter).
You cannot elevate on demand in a remote session (the way you can locally, interactively with Start-Process -Verb RunAs).[1]
Instead, you must make sure that the credentials you're passing to Invoke-Command -Credential to connect to the remote machine with refer to a user account that (also) has administrative privileges on the target machine, in which case the remote session automatically and invariably runs elevated (with admin privileges).[2]
If you cannot pass such credentials, I think you're out of luck.
To test if the current user has administrative privileges:
# Returns $true if elevated, otherwise $false.
[Security.Principal.WindowsPrincipal]::new(
[Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
Separately, here's a simple test you can run from inside a session to determine whether it is running with elevation:
# Returns $true if elevated, otherwise $false.
[bool] (net session 2>$null)
[1] Unless the session at already is elevated, -Verb RunAs presents a pop-up UAC dialog that a user must confirm interactively, which is not supported in a remote session.
[2] The same applies if you use "loopback remoting", i.e. if you target the local machine via remoting, using Invoke-Command -ComputerName ., for instance, with additional restrictions, however: You cannot use a user that is authorized for remoting but isn't part of the local Administrators group, and if you use the current user (whether or not with explicit credentials), the calling session must itself be elevated.
I think you should do this:
$command = {
Start-Process "powershell" -Verb runas -Workingdirectory "C:\Folder\" -ArgumentList "build.ps1"
}
Invoke-Command -ComputerName $RemoteSystemIP -ScriptBlock $command -credential $Credentials1 -ErrorAction Stop -AsJob

Running a uninstaller as administrator remotely over Powershell

I am working on a small software deployment script which uninstalls and installs a new version of Check_MK remotely by using powershell.
Everything is working great - i am able to localize the old service, stop it and determine the installation path of it. The problem is to run the "uninstall.exe" of the service.
This can be easily done by using a Powershell session logging on the server with credential parameter in use.
$passwd = convertto-securestring -AsPlainText -Force -String "password"
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist "domain\user",$passwd
$session = new-pssession -computername "192.xxx.xxx.xxx" -credential $cred
Enter-PSSession $session
The problem is you can't use PSSession in a script - it is made for personal use and not automation.
So i tried to use Powershells Invoke-Command cmdlet.
Invoke-Command -Computername "192.xxx.xxx.xxx" -credential $cred -argumentlist $cred -ScriptBlock {
# Lots of stuff and enter uninstall directory
uninstall.exe /S
}
Calling the exe file ends in a query for administrator credentials.
I also tried to remote start a elevated Powershell session and use it for uninstallation..
Start-Process powershell -Credential $cred
Executing a script located on the server for starting uninstall.exe using the elevated Powershell session ends in a UAC query (not querying for credentials, but asking for executing).
Is there any other solution to handle this? I tried a lot more, but nothing worked.
Here's another approach, using Schedule Tasks.
Create a task locally using the logged-on user credentials, you must have admin rights on the remote computer of course.
Function Run-RemoteCommand
{
Param(
[Parameter(Mandatory = $true)]
[String]
$Computer,
[Parameter(Mandatory = $true)]
[String]
$Task,
[Switch]
$Background
)
$TaskName = "TempTask"
$Explorer=gwmi win32_process -ComputerName $Computer | ? {$_.ProcessName -eq "explorer.exe" }
$OWner = $Explorer.GetOwner().Domain + "\" + $Explorer.GetOwner().User
if ($Background)
{
$Create = "SCHTASKS /Create /TN ""$TaskName"" /RU System /TR ""$Task"" /SC Once /ST 22:30"
}
Else
{
$Create = "SCHTASKS /Create /TN ""$TaskName"" /RU $Owner /TR ""$Task"" /SC Once /ST 22:30"
}
$Runit = "SCHTASKS /Run /TN ""$TaskName"""
$Delete = "SCHTASKS /Delete /TN ""$TaskName"" /F"
$WMI = Get-WmiObject -List Win32_Process -ComputerName $Computer
$WMI.Create($Create)
$WMI.Create($Runit)
Sleep 2
$WMI.Create($Delete)
}
To Run it:
Run-RemoteCommand -Computer "192.xxx.xxx.xxx" -Task 'c:\path\uninstall.exe /S'
if you want to run it as 'NT AUTHORITY\SYSTEM' add the -Background Switch
The best way would be to always have the local shell run as an elevated user. This allows you to start invoke-command without providing credentials in a script.
Once you have a Powershell running as an administrative user, and you can run your script using invoke-command. I do it all the time.
If you are scheduling this task, you will have to have the task run as the elevated user. This again, stops you from having to put the password in plain text in the script.
You don't mention if this is in a domain context or not.
If it is in a domain context, you could use a startup script in a Group Policy Object to accomplish your goal. It will execute on the next reboot of the workstation.
You can also do it without a group policy, but it means visiting each computer individually and changing the local group policy object - at that point, you might as well just run the powershell script.
Another solution: you could create a .zap file and deploy that using Group Policy's software deployment functionality. This would be a bit of an abuse of the system. .zap files are meant to install software that isn't packaged in MSI files, but you could also use it to launch a script that uninstalls your software. Ordinarily, .zap files cannot be uninstalled.
A third option: download Chocolatey from http://www.chocolatey.org. This is a PowerShell script that can manage the installation of a lot of standard software for you. Configure it on each workstation to run automatically (using the scheduler or so). Create a NuGet package for your software (NuGet is what Chocolatey runs under the hood), and use chocolatey to deploy or undeploy your software as needed.
As an aside, instead of rolling your own installer, you may want to consider converting it to an MSI file - those can easily be installed and uninstalled, even remotely, without excessive UAC hassle. You could also use a NullSoft installer, but those are harder to automate remotely. MSI is Microsoft's official installation method.
Some people create MSI wrappers around standard .exe-type installers, too.

win32_process create fails with Unknown failure for service account user

I have a windows service account user, using which i'm trying to create a background process using the WMI win32_proces. But fails with Unknown Failure.
(Tried this with administrator, nonadmin, domain admin, domain nonadmin users. works fine)
$process = [WMICLASS]"\\$computername\ROOT\CIMV2:win32_process"
$processinfo = $process.Create("powershell.exe -WindowStyle Hidden test.ps1")
Write-Host $processinfo.returncode
As explained in this msdn blog post: Win32_Process.Create fails if user profile is not loaded, the WMI call is hardcoded to access the users profile through the registry.
If the user profile is not already loaded in HKU, WMI tries to load it into the registry using RegLoadKey.
This fails unless the user account in question have the following privileges on the local machine:
SeRestorePrivilege
SeBackupPrivilege
So, either
Grant these privileges to the account in question
Call LoadUserProfile for the user in question prior to calling Win32_Process.Create
Or use Start-Process instead of WMI!
# Set up service account credentials
$Username = "domain\svcaccount"
$Password = "7oPs3çürEûN1c0deZ"
$Credential = New-Object pscredential -ArgumentList $Username,$(ConvertTo-SecureString $Password -AsPlainText -Force)
# Establish a session on the remote machine
$RemoteSession = New-PSSession -ComputerName $computername -Credential $Credential
# Launch the process with Start-Process -LoadUserProfile
Invoke-Command -Session $RemoteSession {
Start-Process 'powershell.exe' -LoadUserProfile:$true -Argumentlist 'test.ps1' -WindowStyle Hidden
}
# Cleanup
Remove-PSSession -Session $RemoteSession
Remove-Variable -Name Username,Password,Credential
To Mathias suggestions in below comments:
Start-Process works in background only when invoked through interactive prompt. If run from a .ps1 script, the process created through start-process exits if the .ps1 script exits.
Inside your script. You can create a New-PSsession and then pass this session to invoke-command to trigger start-process.
But again to use New-PSsession and ExitPSSession, you must have Enable-PSRemoting and other setting enabled if you are lacking permissions. http://blogs.msdn.com/b/powershell/archive/2009/11/23/you-don-t-have-to-be-an-administrator-to-run-remote-powershell-commands.aspx