Windows Unprivileged - Services JEA
This article will try to explain how to run services in unprivileged contexts and how to access services from unprivileged accounts. It is a continuation of the post on Windows Services.
Note that absolutely none of this is authoritative or directly based on relevant documentation. It’s mostly what I found and figured out and guessed and (in some cases) made up. Some of it may be wrong or dangerous or lead to disaster or confusion. I am not taking responsibility here for anything, not even spelling. Read and digest at your own peril!
Using GeneralTestService as an example (download at download generaltestservice) we will look at a JEA configuration for Windows services management.
While granting access to services and the Service Control Manager using ACLs is possible, it does come with its shares of problems:
- A new service will have a default ACL giving access to only the system account and the Administrators group. There is no default resource group. (Recall that a resource group is a group solely existing to grant access to a certain resource, like a service.)
- There is no easy way to edit a service’s ACL. The best way is still to use sc.exe sdshow and sc.exe sdset and the security descriptor definition language (SDDL).
- There is no way to give non-administrators permission to create a service that does not also give permission to create a service that runs as LocalSystem.
- People simply do not like ACLs for some reason.
But Windows supports three different ways of granting permissions.
There are object permissions, the access control lists, ACLs, which regulate who can do what to a specific object. ACLs are stored in security descriptors. Most ACLs grant write access only to the Administrators group. (The Unix equivalent are the file permissions.)
Then there are subject permissions, privileges, which regulate what a specific security principal, like a user, can do to any object. The Administrators group holds most privileges. (There is no Unix equivalent.)
And finally there are action permissions. PowerShell Just-Enough-Admin allows specific actions to be done by specific people to any object. The Administrators group can already do most of these actions. (The Unix equivalent are the setuid bit and by extension the sudo command.)
I have covered before how Just-Enough-Admin (JEA) works. The most simple summary is probably in the post on Windows Features JEA.
So let’s proceed with the creation of a JEA configuration for services.
PS C:\Program Files\WindowsPowerShell\Modules\JEA> New-LocalGroup JEA_Service # create a resource group to represent the JEA configuration
Name Description
---- -----------
JEA_Service
PS C:\Program Files\WindowsPowerShell\Modules\JEA> Add-LocalGroupMember JEA_Service benoit # add someone to it
PS C:\Program Files\WindowsPowerShell\Modules\JEA> New-PSSessionConfigurationFile -Path Service.pssc -SessionType RestrictedRemoteServer -RunAsVirtualAccount -RoleDefinitions @{'JEA_Service'=@{'RoleCapabilities'='Service'}}
PS C:\Program Files\WindowsPowerShell\Modules\JEA> New-PSRoleCapabilityFile .\RoleCapabilities\Service.psrc
PS C:\Program Files\WindowsPowerShell\Modules\JEA>Now edit Service.psrc to say this:
@{
# ID used to uniquely identify this document
GUID = 'a170d3c7-898d-4299-bae3-1f17ca93cb88'
# Author of this document
Author = 'ajbrehm'
# Description of the functionality provided by these settings
Description = 'Allows creating, querying, stopping and starting services.'
# Company associated with this document
CompanyName = 'Unknown'
# Copyright statement for this document
Copyright = '(c) 2026 ajbrehm. All rights reserved.'
# Modules to import when applied to a session
# ModulesToImport = 'MyCustomModule', @{ ModuleName = 'MyCustomModule'; ModuleVersion = '1.0.0.0'; GUID = '4d30d5f0-cb16-4898-812d-f20a6c596bdf' }
ModulesToImport = 'Microsoft.PowerShell.LocalAccounts'
# Cmdlets to make visible when applied to a session
VisibleCmdlets = 'Get-Service','Start-Service','Restart-Service','Stop-Service'
# Functions to make visible when applied to a session
# VisibleFunctions = 'Invoke-Function1', @{ Name = 'Invoke-Function2'; Parameters = @{ Name = 'Parameter1'; ValidateSet = 'Item1', 'Item2' }, @{ Name = 'Parameter2'; ValidatePattern = 'L*' } }
VisibleFunctions = 'New-Service'
# Providers to make visible when applied to a session
# VisibleProviders = 'Item1', 'Item2'
VisibleProviders = 'FileSystem'
# Functions to define when applied to a session
# FunctionDefinitions = @{ Name = 'MyFunction'; ScriptBlock = { param($MyInput) $MyInput } }
FunctionDefinitions = @{
Name = 'New-Service'
ScriptBlock = {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$Name,
[Parameter(Mandatory=$true)][string]$DisplayName,
[Parameter(Mandatory=$true)][string]$BinaryPathName,
[Parameter(Mandatory=$true)][string]$Description,
[Parameter(Mandatory=$true)][string]$StartupType,
[Parameter(Mandatory=$true)][PSCredential]$Credential,
[Parameter(Mandatory=$false)][string[]]$DependsOn,
[Parameter(Mandatory=$false)][string]$ResourceGroupName,
[Parameter(Mandatory=$false)][string]$ResourceUserName,
[Parameter(Mandatory=$false)][uint32]$FailureReset,
[Parameter(Mandatory=$false)][string]$FailureCommand,
[Parameter(Mandatory=$false)][string]$FailureActions
)
Microsoft.PowerShell.Management\New-Service -Name $Name -DisplayName $DisplayName -BinaryPathName $BinaryPathName -Description $Description -StartupType $StartupType -Credential $Credential -DependsOn $DependsOn | Out-File C:\New-Service.log -Append
if ($ResourceGroupName) {
$sid = (Microsoft.PowerShell.LocalAccounts\Get-LocalGroup $ResourceGroupName).SID.Value
$sddl = (sc.exe sdshow $Name)[1]
$sddl = $sddl -replace "S:","(A;;0x10034;;;$sid)S:"
sc.exe sdset $Name $sddl
}#if
if ($ResourceUserName) {
$sid = (Microsoft.PowerShell.LocalAccounts\Get-LocalUser $ResourceUserName).SID.Value
$sddl = (sc.exe sdshow $Name)[1]
$sddl = $sddl -replace "S:","(A;;0x10034;;;$sid)S:"
sc.exe sdset $Name $sddl
}#if
if ($FailureReset -and $FailureActions) {
$FailureActions = """$FailureActions"""
if ($FailureCommand) {
$FailureCommand = """$FailureCommand"""
sc.exe failure $Name reset=$FailureReset actions=$FailureActions command=$FailureCommand
} else {
sc.exe failure $Name reset=$FailureReset actions=$FailureActions
}#if
}#if
}#scriptblock
}
}(You can use a different GUID in case New-PSRoleCapability created a psrc file with a different GUID.)
What this does:
- Imports the Microsoft.PowerShell.LocalAccounts module which is required for the Get-LocalUser cmdlet used to get the SID of a principal for editing a service ACL.
- Allows privileged access to the following cmdlets: Get-Service, Start-Service, Restart-Service, Stop-Service. (Exercise: Find out what they do.)
- Replaces the original New-Service cmdlet with a new secure New-Service function.
- Allows the JEa configuration access to the file system provider which is apparently necessary to set permissions on services…
- Defines a new New-Service function that does the following:
- Takes a mandatory Name, DisplayName, Description, StartupType, and Credential arguments.
- Takes optional DependsOn, ResourceGroupName, and ResourceUserName arguments.
- Creates a service with the Name, DisplayName, Description, StartupType, and Credential given.
- Adds the dependencies if DependsOn was given.
- Adds the group given as ResourceGroupName and/or the account given as ResourceUserName to the service with permissions to query, start, stop, modify, and delete the service.
These are common accessmask bits for services:
- SERVICE_QUERY_CONFIG (0x0001): Read the service configuration.
- SERVICE_CHANGE_CONFIG (0x0002): Change the service configuration.
- SERVICE_QUERY_STATUS (0x0004): Ask the service for its current status (running, stopped).
- SERVICE_ENUMERATE_DEPENDENTS (0x0008): Enumerate services dependent on this service.
- SERVICE_START (0x0010): Start the service.
- SERVICE_STOP (0x0020): Stop the service.
- SERVICE_PAUSE_CONTINUE (0x0040): Pause or continue the service.
- SERVICE_INTERROGATE (0x0080): Send an interrogate request to the service.
- SERVICE_USER_CONTROL (0x0100): Register a user-defined control code.
- DELETE (0x00010000): Delete the service object.
To allow starting, stopping, modifying, and deleting we must add together 4 (query), 0x10 (start), 0x20 (stop) and 0x10000 (delete). This results in 0x10034. Perhaps. I am not good at maths.
Now, I realise that permissions to query, start, and stop services are redundant since the JEA configuration already grants privileged access to the cmdlets that do that. But I was hoping you would realise that you can remove those cmdlets from the list of allowed cmdlets if you don’t need or want them. Permission to delete only services created via the JEA configuration is probably a useful thing though. (If you don’t want delete permissions, change the access mask 0x10034 to 0x34.)
Let’s try it out.
Adminstrator part:
PS C:\Program Files\WindowsPowerShell\Modules\JEA> Register-PSSessionConfiguration Service -Path .\Service.pssc
WARNING: Register-PSSessionConfiguration may need to restart the WinRM service if a configuration using this name has recently been unregistered, certain system data structures may still be cached. In that case, a
restart of WinRM may be required.
All WinRM sessions connected to Windows PowerShell session configurations, such as Microsoft.PowerShell and session configurations that are created with the Register-PSSessionConfiguration cmdlet, are disconnected.
WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Plugin
Type Keys Name
---- ---- ----
Container {Name=Service} Service
WARNING: Set-PSSessionConfiguration may need to restart the WinRM service if a configuration using this name has recently been unregistered, certain system data structures may still be cached. In that case, a restart of
WinRM may be required.
All WinRM sessions connected to Windows PowerShell session configurations, such as Microsoft.PowerShell and session configurations that are created with the Register-PSSessionConfiguration cmdlet, are disconnected.
WARNING: Register-PSSessionConfiguration may need to restart the WinRM service if a configuration using this name has recently been unregistered, certain system data structures may still be cached. In that case, a
restart of WinRM may be required.
All WinRM sessions connected to Windows PowerShell session configurations, such as Microsoft.PowerShell and session configurations that are created with the Register-PSSessionConfiguration cmdlet, are disconnected.Unprivileged user part:
S C:\Users\benoit> $s = New-PSSession -ConfigurationName Service
PS C:\Users\benoit> $cr = Get-Credential moulinsart\benoit
PS C:\Users\benoit> Invoke-Command $s {param($cr); New-Service -Name TestService -DisplayName TestService -Description TestService -BinaryPathName C:\GeneralTestService\GeneralTestService.exe -StartupType Manual -Credential $cr -ResourceUserName benoit} -ArgumentList $cr
[SC] SetServiceObjectSecurity SUCCESS
PS C:\Users\benoit> sc.exe qc TestService
[SC] QueryServiceConfig SUCCESS
SERVICE_NAME: TestService
TYPE : 10 WIN32_OWN_PROCESS
START_TYPE : 3 DEMAND_START
ERROR_CONTROL : 1 NORMAL
BINARY_PATH_NAME : C:\GeneralTestService\GeneralTestService.exe
LOAD_ORDER_GROUP :
TAG : 0
DISPLAY_NAME : TestService
DEPENDENCIES :
SERVICE_START_NAME : .\benoit
PS C:\Users\benoit> sc.exe delete TestService
[SC] DeleteService SUCCESS
PS C:\Users\benoit> sc.exe qc TestService
[SC] OpenService FAILED 1060:
The specified service does not exist as an installed service.
PS C:\Users\benoit> Stop-Service Themes
Stop-Service : Service 'Themes (Themes)' cannot be stopped due to the following error: Cannot open Themes service on computer '.'.
At line:1 char:1
+ Stop-Service Themes
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : CloseError: (System.ServiceProcess.ServiceController:ServiceController) [Stop-Service], ServiceCommandException
+ FullyQualifiedErrorId : CouldNotStopService,Microsoft.PowerShell.Commands.StopServiceCommand
PS C:\Users\benoit> Invoke-Command $s {Stop-Service Themes}
PS C:\Users\benoit> Get-Service Themes
Status Name DisplayName
------ ---- -----------
Stopped Themes Themes
PS C:\Users\benoit> Remove-PSSession $s
PS C:\Users\benoit>The above does the following:
- Creates a PowerShell session that uses the Service JEA configuration.
- Creates a credential $cr to use for a newly created service. (Note that the account used must have SeServiceLogonRight.)
- Creates a new service TestService running as moulinsart\benoit (the local user benoit) and with permissions for said benoit to start, stop, and delete the service.
- Checks on the service (sc.exe qc TestService)
- Deletes the service.
- Checks on the service again. It’s gone.
- Tries to stop the service Themes. Cannot.
- Tries to stop the service Themes using the JEA session. Can.
- Checks on the service Themes. It’s stopped.
- Removes the PowerShell session.
This should allow for a whole lot of required access.
Next: TBD