First of all I should point out that this isn’t really an exploit or security issue, its just some slightly weird behaviour that I found interesting.
I started looking at this after I read an interesting write up from 0xdf for the Hack The Box machine named Resolute, which you can find here: https://0xdf.gitlab.io/2020/06/01/resolute-more-beyond-root.html
The Problem
Unlike myself and most others, 0xdf didn’t use the DNS Admins group as the path to root on this HTB machine (see my video here if you’re not sure what that path is). Instead he took advantage of the fact that our user account had permission to create new services.
So we should just be able to create a new service, set it to run as Local System, tell it to run some malicious exe as the service executable, then start the service and boom, we have Local System access. Right? Not so fast.
Let’s try and do that using the built in sc.exe program in Windows (after granting my account permission to create new services, like we had on the HTB machine). First of all we create the service:
sc.exe create MyService binPath= "C:\nc.exe etc etc"
This completes successfully, but now when we try to start the service with the following command we get Access Denied:
sc.exe start MyService

This is because the default ACL assigned to new services does not allow regular users to start the service. We can see this if we look at the ACL on our new service with the sdshow option:

If you’re anything like me you probably struggle to read SDDL, so here’s the same service ACL shown in this GUI tool:

The only group in the ACL that a regular user is ever likely to find themselves in is the Interactive group, and that does not have permission to start the service as you can see in the screenshot above.
How You Normally Start Services
So the new service’s ACL says we can’t start the service… however it turns out there is actually a very simple way we can bypass the ACL entirely for our new service and be allowed full access to it.
But first of all let’s take a look at what happens when you use a tool like sc.exe to try and start a service. Essentially any program like that will do the following:
1: Get a handle to the SCM (Service Control Manager) by calling OpenSCManager
2: Get a handle to the service we want to start by calling OpenService and passing in the handle we got in step 1
3: Call StartService and pass in the handle we got in step 2
In each of the steps that involve requesting a handle, we have to specify what level of access we want for the service/SCM and if we don’t have the permissions required then the call will fail with access denied. You can find all of the different permissions here.
So in our example where we can’t start the service, we’re actually failing at step 2 because we’re going to request a handle with a desired access level of “SERVICE_START” and this will fail as we don’t have that permission. You can see in our original error message earlier it does actually mention that it is the call to OpenService that failed:

So we never get a handle to the service and therefore can’t perform step 3 which is passing the handle to the StartService function.
Bypassing The ACL For A New Service
So we know that we can’t create a new service and then get a handle to it that will let us start it, but it turns out when we create a new service we get a handle to the service returned back to us by the CreateService function.
At first glance this doesn’t seem very useful because the CreateService function requires us to specify the level of access this handle should be for just like OpenService does. So you would think if we asked CreateService to return us a handle with permission to start the new service, it would fail because the default service ACL doesn’t allow us to do that… but that is not the case!
We can actually request whatever level of access we like to the new service and CreateService will return a handle with that level of access. So then we can pass that handle to StartService or whatever other service functions we want and it will work perfectly fine without any access denied errors.
I’ve written some example .NET code that demonstrates this (full code with error handling etc can be found on my github here) :
' Connect to SCM (Service Control Manager) and get a handle that allows us to create services by specifying SC_MANAGER_CREATE_SERVICE.
' Would fail with access denied here if we don't have permission to create services
ScmHandle = WinApi.OpenSCManager(Nothing, Nothing, WinApi.SCM_RIGHTS.SC_MANAGER_CREATE_SERVICE)
' Create a new service and get back a handle to the service (that can then be used with StartService function and other service functions).
' We specify SERVICE_ALL_ACCESS so that this handle allows us full control over the service (to start it, stop it, delete it, etc) and for some
' reason this works. If we try to request this level of access afterwards by using the OpenService function, it fails with access denied
ServiceHandle = WinApi.CreateService(ScmHandle,
ServiceName,
Nothing,
WinApi.SERVICE_RIGHTS.SERVICE_ALL_ACCESS,
WinApi.SERVICE_WIN32_OWN_PROCESS,
WinApi.SERVICE_DEMAND_START,
WinApi.SERVICE_ERROR_NORMAL,
ExePath,
Nothing, Nothing, Nothing, "NT Authority\System", Nothing)
' If we successfully created the service, attempt to start it using the handle we got back from CreateService.
If Not ServiceHandle = IntPtr.Zero Then
WinApi.StartService(ServiceHandle, 0, IntPtr.Zero)
End If
Line 11 in the code above is the important part really. This is where we specify that we would like a handle that has full control over the service (why stop at just requesting permission to start the service eh). This does indeed give us a handle we can use to do anything we want to the service, even if the ACL doesn’t allow us to do that.
Closing Thoughts
If you think about it, there are a few reasons why this does kind of make sense and it definitely seems like a conscious design choice by MS rather than any kind of bug or oversight.
But it still feels very weird being able to call CreateService and get a handle that allows full control over the service, then one second later call OpenService against that same service and request the same level of access but get an access denied error.
Also just to clarify – even if an attacker didn’t do this and couldn’t manually start the service this way, there are still plenty of ways to get the service started (which is one reason why at the start I said this little trick isn’t really a security issue). The simplest way would be to specify that the service should start automatically on boot up, then just restart the machine or wait for it to be rebooted by the user.
At the end of the day if you’re granting someone permission to create new services, you’re basically giving them Local System regardless of what other permissions are in place.