Summary: Boe Prox shows how to create a secure PSSession endpoint by using delegated administration and a proxy function.
Hey, Scripting Guy! There are some great things that I can do with a constrained Windows PowerShell endpoint. But I want to allow a user (who may not have the necessary rights) to run a query on a remote system. Is this possible?
—KP
Hello SH, Honorary Scripting Guy, Boe Prox, here today filling in for my good friend, The Scripting Guy. This is the fourth part in a series of five posts about Remoting Endpoints. The series includes:
- Introduction to PowerShell Endpoints
- Build Constrained PowerShell Endpoint Using Startup Script
- Build Constrained PowerShell Endpoint Using Configuration File
- Use Delegated Administration and Proxy Functions (today’s post)
- Build a Tool that Uses Constrained PowerShell Endpoint
Building a constrained Windows PowerShell endpoint provides a great avenue for users to connect to a remote system via Windows PowerShell remoting to perform a wide variety of commands, based on what is allowed by a configuration file or a startup script.
There may be a case where you have a user account that doesn’t need to have Administrator rights on a server, but the user still needs to run a command to pull some information. Let’s say I have a server that I want to query for stopped services that are set for automatic. I also want to start those stopped services. The problem is that I do not want to give the user complete access to my remote system. How do I go about providing access to the user so that he can still perform his job? The answer is to use delegated administration to accomplish this.
Note Delegated administration is only available in Windows PowerShell 4.0 and Windows PowerShell 3.0.
The idea of delegated administration is to have a delegated administrator account that has the necessary rights to run the endpoint. Anyone who has the proper rights can connect to the endpoint. The delegated administrator account will run any commands issued by the remote user on the user’s behalf as the administrator. By doing this, commands that would have been impossible for the user to run are now possible. However, because the account has the rights to connect to the remote endpoint doesn’t mean that it has the rights to run any commands on that endpoint.
Enter-PSSession –Computername ‘boe-pc’ –Credential ‘boe-pc\proxb’ –ConfigurationName Microsoft.PowerShell
Get-WMIObject –Class Win32_Service
As you can see, my account, proxb, has the necessary rights to access the remote endpoint, but it does not have the rights to perform a WMI query against the Win32_Service class.
To do this, we need to create a remote endpoint that uses an account that has the necessary rights on the server to run the commands that we need.
Register-PSSessionConfiguration -Name PowerShell.Session -RunAsCredential 'boe-pc\EndpointService' -Force
I supply the password for my account which will be used as the delegated administrator account. When the process has completed, we can check the new endpoint to see how it looks. By the way, take note of the warning that is displayed after you supply the credentials. Make sure that you are allowing very specific access to the remote endpoint in addition to restricting which commands will be run on it (more on that part in a bit).
Get-PSSessionConfiguration –Name PowerShell.Session
Now let’s go back to see what happens when I connect to the new endpoint by using my proxb account.
I was able to access the remote endpoint because I had the proper rights to access it. As you can see, when I ran the whoami command, it didn’t show my account. Instead, it showed the endpointservice account that I used for the delegation. Because of this, performing the same WMI query is now successful.
Of course, having this much power through a delegated account can only lead to potential issues if the account is not secured properly. Aside from locking down access to the remote endpoint, we need to narrow the scope of commands that are available to the users who have access to this remote endpoint. Rather than allowing a set of cmdlets to run and hope that nothing bad will happen, we can make use of a set of commands wrapped in a function that only does what we intend it to do. This is also known as a proxy function.
Proxy functions to hide commands
Although it is nice that we can delegate an account to allow a non-privileged account to have the necessary rights to a remote system to perform a task, the problem that begins to reveal itself is that you are running under the rights of an account that has plenty of access to not only the local server, but also to remote systems. (Yes, this is one way around the “second hop” other than using credssp.) This can lead to a lot of trouble if you aren’t careful.
Sure you can limit cmdlets by using a configuration file or startup script—but what happens when you need to use the Stop-Service cmdlet (or something potentially as destructive) for performing a task? That cmdlet is now available to use on not only that system, but also on remote systems. And it can be used for things other than what you intended (such as restarting the Windows Server Update Service). Even though you are only restarting the service, it still has the potential to impact the users who rely on that service.
To prevent this type of action, I will use a proxy function that will perform a specific set of actions and nothing more. In this case, I will specify a proxy function that will do a variety of WMI queries that will return some useful system information, even though Get-WMIObject will not be available. Also I will use Get-Process and Get-Counter to gather some extra information. For the purpose of an example, I am going to also have Get-Command (another proxy function) available to use.
My method for delivering this constrained endpoint will be via a startup script. Although the configuration file allows me to create new functions by using the FunctionDefinitions key, I am required to import the required modules for any cmdlets that I want to use under SessionType = ‘Empty’.
This means that none of the usual proxy functions (Get-Command, Get-FormatData, Select-Object, Get-Help, Measure-Object, Exit-PSSession, Out-Default) that would usually be created will be available, and I need to supply all of the cmdlets that I need for the session.
Note The seven proxy functions mentioned are required for an interactive session using Enter-PSSession, but they are not required if you want to use Invoke-Command instead to issue commands to the remote endpoint.
This creates issues because I cannot hide any cmdlet that is required by my function, and it becomes instantly available for a user to make use of. Instead, the startup script gives me enough flexibility to create the proxy function, and it makes all other commands unavailable to the user.
I discussed most of what I am about to show you in Tuesday’s blog post, Build Constrained PowerShell Endpoint Using Startup Script. There will be some items missing and a couple of things added that will help ensure that this remote endpoint is constrained to allow only the proxy functions I specified earlier and to allow no interactive sessions.
#Define Custom Proxy functions
Function Get-SystemReport {
$OS = Get-WMIObject -Class Win32_OperatingSystem
$CS = Get-WMIObject -Class Win32_ComputerSystem
$StoppedServices = Get-WmiObject -Class Win32_Service -Filter "StartMode='Auto' AND State!='Running'"
$Disk = Get-WmiObject Win32_LogicalDisk -Filter "DriveType='3'"
$PerfCounters = (Get-Counter "\processor(_total)\% processor time","\Memory\Available Bytes" |
Select -Expand CounterSamples)
[pscustomobject]@{
Computername = $env:COMPUTERNAME
OSCaption = $OS.Caption
OSVersion = $OS.Version
Model = $CS.Model
Manufacture = $CS.Manufacturer
TotalRAMGB = [math]::Round($CS.TotalPhysicalMemory /1GB,2)
AvailableRAMGB = [math]::Round(($PerfCounters[1].CookedValue / 1GB),2)
CPU = [math]::Round($PerfCounters[0].CookedValue,2)
RunningProcesses = (Get-Process).Count
LastBootUp = $OS.ConvertToDateTime($OS.LastBootUpTime)
Drives = $Disk | Select DeviceID, VolumeName,
@{L='Size';E={[math]::Round($_.Size/1GB,2)}},
@{L='FreeSpace';E={[math]::Round($_.FreeSpace/1GB,2)}}
StoppedServices = $StoppedServices | Select Name, DisplayName, State
}
}
#Proxy functions
[string[]]$proxyFunction = Get-SystemReport','Get-Command'
#Cmdlets
ForEach ($Command in (Get-Command)) {
If (($proxyFunction -notcontains $Command.Name)) {
$Command.Visibility = 'Private'
}
}
#Variables
Get-Variable | ForEach {
$_.Visibility = 'Private'
}
#Aliases
Get-Alias | ForEach {
$_.Visibility = 'Private'
}
$ExecutionContext.SessionState.Applications.Clear()
$ExecutionContext.SessionState.Scripts.Clear()
$ExecutionContext.SessionState.LanguageMode = "NoLanguage"
You will notice that this startup script is much smaller than the one we used previously. I only need the bare minimum here to allow access to the commands needed. I have my two proxy functions defined and they represent the only available commands to use.
With that, I now create a new remote endpoint with a delegated administrator account and use this startup script to ensure that only the commands that I choose are available.
Register-PSSessionConfiguration -Name PowerShell.ConstrainedSession `
-StartupScript "C:\PSSessions\ConstrainedSessionStartupScript.ps1" `
-RunAsCredential 'boe-pc\endpointservice' -ShowSecurityDescriptorUI –Force
Let’s kick the tires on this new endpoint and see what we can and cannot do:
Enter-PSSession –Computername ‘boe-pc’ –Credential ‘boe-pc\proxb’ –ConfigurationName PowerShell.ConstrainedSession
Invoke-Command –Computername ‘boe-pc’ –Credential ‘boe-pc\proxb’ –ConfigurationName PowerShell.ConstrainedSession -ScriptBlock {Get-Command}
Invoke-Command –Computername ‘boe-pc’ –Credential ‘boe-pc\proxb’ –ConfigurationName PowerShell.ConstrainedSession -ScriptBlock {Get-SystemReport}
As expected, my attempts to go into an interactive session (Enter-PSSession) on this endpoint failed. In fact, the error talks about a missing cmdlet (Measure-Object), which happens to be one of those proxy functions that would usually be created if we used a RestrictedRemoteServer configuration.
When we run the Get-Command cmdlet (via Invoke-Command) that we allowed for this instance, we can easily see that only two commands are available to us to use. The other command, Get-SystemReport runs as expected by returning a lot of information about the remote system. Nothing more and nothing less for this constrained remote endpoint.
KP, that is all there is to building a constrained Windows PowerShell endpoint that uses delegated administration and proxy functions. Remote Endpoint Week will conclude tomorrow when I will talk about building a tool that will use what we have learned this week about constrained remote endpoints.
I invite you to follow the Scripting Guys on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow.
Boe Prox, Honorary Scripting Guy