CreateService API Bypasses Service Permissions

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.

Kerberos Protocol Explained

This blog post is intended to supplement the Kerberos explanation video that just went live on my Youtube channel:

As such, I’m not going to explain everything in detail here. This post is intended as a quick reference for all of the diagrams I made for that video. Hopefully they can be useful on their own even if you don’t watch the video, but of course I’d encourage you to watch it to get a full explanation of everything here.

So let’s start off with the simple high level summary of the Kerberos authentication process:

Step 1 and 2 in the diagram above happen once, when the user logs on to their PC. Steps 3 and 4 happen the first time they try to authenticate with the network service (SQL Server in this example). The service ticket they receive in step 4 will get cached, so then step 5 happens every time they access the service and uses that cached ticket (until they log off or until the service ticket expires and then they need to repeat step 3 and 4 again).

Now for a slightly more in depth look at that same process:

To understand how this really works, we need to look at the network messages that get sent for each of those steps.

First of all we have the AS-REQ and AS-REP which cover step 1 and 2 in the previous diagrams:

Then for step 3 and 4 we have the TGS-REQ and TGS-REP messages:

Then finally the AP-REQ message for step 5. Note that this message won’t usually be easy to see in network captures because it will be sent over whatever protocol the client communicates with the network service with (e.g HTTPS, SQL’s network protocol, or some proprietary protocol created just for this service):

Of course there’s more to some of these structures, but I’ve picked out the interesting parts and tried to keep things as simple as possible whilst still being accurate.

Towards the end of the video mentioned at the start of this post, you’ll see how each of these diagrams relate to a real world Wireshark network capture of this whole process happening. So yeah, go watch that 🙂

Using .NET To Get Process Command Lines

No exploits in this one – just some good old fashioned coding tips.

I wanted to show the command line used to launch each process that’s running on the machine (the executable file path along with arguments passed to it, i.e. C:\Windows\system32\svchost.exe -k netsvcs).

Now there’s plenty of examples online showing how to do this from C++ but things get slightly more awkward when doing it from C#.NET or VB.NET, especially when it comes to 64 bit processes. It took me a while to get it all working, so I thought I’d document it here for anyone else wanting to do the same thing.

There’s going to be lots of Win32 API interop here, so buckle up!


The first thing we need to do is call the native Win32 API NtQueryInformationProcess.

The MSDN description states that this function “Retrieves information about the specified process”. So we can call it and get back a PROCESS_BASIC_INFORMATION structure which contains the address of the PEB (Process Environment Block) for the process we specified.

Why do we want the address of the PEB? Well, if we look at the structure of the PEB we can see it contains a member called ProcessParameters which is an RTL_USER_PROCESS_PARAMETERS structure. If we then look at the definition of that structure we see it contains a member called CommandLine which is described as “The command-line string passed to the process”. Sounds like exactly what we are after!

Just to make it a bit clearer, here’s how we’re going to go from calling the API to getting the command line string we want:

So let’s get started by calling the NtQueryInformationProcess function. Due to this being a Win32 API, to use it in our .NET project we need to take the C++ definition we got from MSDN:

__kernel_entry NTSTATUS NtQueryInformationProcess(
  IN HANDLE           ProcessHandle,
  IN PROCESSINFOCLASS ProcessInformationClass,
  OUT PVOID           ProcessInformation,
  IN ULONG            ProcessInformationLength,
  OUT PULONG          ReturnLength
);

and declare it like so in our .NET project (I’m using VB.NET but its very easy to convert this to C#.NET) :

        <DllImport("ntdll.dll", EntryPoint:="NtQueryInformationProcess", SetLastError:=True)>
        Public Shared Function NtQueryInformationProcess(ByVal ProcessHandle As IntPtr,
                                                         ByVal Processinformationclass As UInteger,
                                                         ByRef ProcessInformation As PROCESS_BASIC_INFORMATION,
                                                         ByVal ProcessInformationLength As Integer,
                                                         ByRef ReturnLength As UInteger) As Integer
        End Function

We also need to take the PROCESS_BASIC_INFORMATION structure definition from MSDN:

typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PPEB PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

and implement that in .NET:

        <StructLayout(LayoutKind.Sequential)> _
        Public Structure PROCESS_BASIC_INFORMATION
            Public Reserved1 As IntPtr
            Public PebBaseAddress As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved2() As IntPtr
            Public UniqueProcessID As IntPtr
            Public Reserved3 As IntPtr
        End Structure

As you can see, we have to convert the C++ definitions into their equivalent .NET types. For example if a member is declared as PVOID in C++ then we declare it as IntPtr in .NET.

NOTE: I’ll do another blog post soon explaining this process in a lot more detail and showing how to get started with marshalling things between native Win32 API and .NET. Maybe I’ll do a video on it as as well.

I’ll add a link here once that post/video is up.

Unfortunately we can’t just read the PebBaseAddress pointer from our PROCESS_BASIC_INFORMATION structure and treat it like a normal pointer though. This is because the pointer is relative to the target process’ memory, not our own process’s memory.

This is true for all of the pointers in the PEB and its members as well.

So if we take one of those pointers and try to grab the data it is pointing to by using something like PtrToStructure then it will fail because the pointer doesn’t make any sense in our own process’ memory address space. What we need to do is use the ReadProcessMemory API to read data from the target process’ memory. With that, we can use these pointers that relate to the target process’ virtual memory.

To make this ReadProcessMemory API easier to use, I wrapped it up in a .NET class called ProcessMemoryReader which you can find below:

Public Class ProcessMemoryReader : Implements IDisposable

    Private _TargetProcess As Process = Nothing
    Private _TargetProcessHandle As IntPtr = IntPtr.Zero

    Public Sub New(ByVal ProcessToRead As Process)
        If ProcessToRead Is Nothing Then
            Throw New ArgumentNullException("ProcessToRead")
        End If
        _TargetProcess = ProcessToRead
        Me.Open()
    End Sub

    Public Function Read(ByVal MemoryAddress As IntPtr, ByVal Count As Integer) As Byte()
        If _TargetProcessHandle = IntPtr.Zero Then
            Open()
        End If
        Dim Bytes(Count - 1) As Byte
        Dim Result As Boolean = WindowsApi.Win32.ReadProcessMemory(_TargetProcessHandle, MemoryAddress, Bytes, CUInt(Count), 0)
        If Result Then
            Return Bytes
        Else
            Return Nothing
        End If
    End Function

    Public Sub Open()
        If _TargetProcess Is Nothing Then
            Throw New ApplicationException("Process not found")
        End If
        If _TargetProcessHandle = IntPtr.Zero Then
            _TargetProcessHandle = WindowsApi.Win32.OpenProcess(WindowsApi.Win32.ProcessAccess.VMRead Or WindowsApi.Win32.ProcessAccess.QueryInformation, True, CUInt(_TargetProcess.Id))
            If _TargetProcessHandle = IntPtr.Zero Then
                Throw New ApplicationException("Unable to open process for memory reading. The last error reported was: " & New System.ComponentModel.Win32Exception().Message)
            End If
        Else
            Throw New ApplicationException("A handle to the process has already been obtained, " & _
                                           "close the existing handle by calling the Close method before calling Open again")
        End If
    End Sub

    Public Sub Close()
        If Not _TargetProcessHandle = IntPtr.Zero Then
            Dim Result As Boolean = WindowsApi.Win32.CloseHandle(_TargetProcessHandle)
            If Not Result Then
                Throw New ApplicationException("Unable to close process handle. The last error reported was: " & _
                                               New System.ComponentModel.Win32Exception().Message)
            End If
            _TargetProcessHandle = IntPtr.Zero
        End If
    End Sub


#Region "IDisposable Support"

    Private disposedValue As Boolean

    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposedValue Then
            If Not _TargetProcessHandle = IntPtr.Zero Then
                Try
                    WindowsApi.Win32.CloseHandle(_TargetProcessHandle)
                Catch ex As Exception
                    Debug.WriteLine("Error closing handle - " & ex.Message)
                End Try
            End If
        End If
        Me.disposedValue = True
    End Sub

    Protected Overrides Sub Finalize()
        Dispose(False)
        MyBase.Finalize()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

#End Region


End Class

So now that we have a way to read the target process’ memory and make use of the pointers in the PEB, we can put everything together to do what we want.

In the final code below,you can see I’ve added a few more API definitions to help handle 32 bit processes on 64 bit operating systems, and created definitions for the various structures we need to interact with.

I’ve written a lot of comments in the code so hopefully it all makes sense, but if you prefer to step through it yourself you can download the full VS project on GitHub here: https://github.com/VbScrub/ProcessCommandLineDemo

Imports System.Runtime.InteropServices

Public Class WindowsApi

    Public Shared Function GetCommandLine(ByVal TargetProcess As Process) As String
        'If we're on a 64 bit OS then the target process will have a 64 bit PEB if we are calling this function from a 64 bit process (regardless of
        'whether or not the target process is 32 bit or 64 bit).
        'If we are calling this function from a 32 bit process and the target process is 32 bit then we will get a 32 bit PEB, even on a 64 bit OS. 
        'The one situation we can't handle is if we are calling this function from a 32 bit process and the target process is 64 bit. For that we need to use the
        'undocumented NtWow64QueryInformationProcess64 and NtWow64ReadVirtualMemory64 APIs
        Dim Is64BitPeb As Boolean = False
        If Environment.Is64BitOperatingSystem() Then
            If Is32BitProcessOn64BitOs(Process.GetCurrentProcess) Then
                If Not Is32BitProcessOn64BitOs(TargetProcess) Then
                    'TODO: Use NtWow64ReadVirtualMemory64 to read from 64 bit processes when we are a 32 bit process instead of throwing this exception
                    Throw New InvalidOperationException("This function cannot be used against a 64 bit process when the calling process is 32 bit")
                End If
            Else
                Is64BitPeb = True
            End If
        End If

        'Open the target process for memory reading
        Using MemoryReader As New ProcessMemoryReader(TargetProcess)
            Dim ProcessInfo As Win32.PROCESS_BASIC_INFORMATION = Nothing
            'Get basic information about the process, including the PEB address
            Dim Result As Integer = Win32.NtQueryInformationProcess(TargetProcess.Handle, 0, ProcessInfo, Marshal.SizeOf(ProcessInfo), 0)
            If Not Result = 0 Then
                Throw New System.ComponentModel.Win32Exception(Win32.RtlNtStatusToDosError(Result))
            End If
            'Get pointer from the ProcessParameters member of the PEB (PEB has different structure on x86 vs x64 so different structures needed for each)
            Dim PebLength As Integer
            If Is64BitPeb Then
                PebLength = Marshal.SizeOf(GetType(Win32.PEB_64))
            Else
                PebLength = Marshal.SizeOf(GetType(Win32.PEB_32))
            End If
            'Read the PEB from the PebBaseAddress pointer
            'NOTE: This pointer points to memory in the target process' address space, so Marshal.PtrToStructure won't work. We have to read it with the ReadProcessMemory API 
            Dim PebBytes() As Byte = MemoryReader.Read(ProcessInfo.PebBaseAddress, PebLength)
            'Using GCHandle.Alloc get a pointer to the byte array we read from the target process, so we can use PtrToStructure to convert those bytes to our PEB_32 or PEB_64 structure
            Dim PebBytesPtr As GCHandle = GCHandle.Alloc(PebBytes, GCHandleType.Pinned)
            Try
                Dim ProcParamsPtr As IntPtr
                'Get a pointer to the RTL_USER_PROCESS_PARAMETERS structure (again this pointer refers to the target process' memory)
                If Is64BitPeb Then
                    Dim PEB As Win32.PEB_64 = GcHandleToStruct(Of Win32.PEB_64)(PebBytesPtr.AddrOfPinnedObject)
                    ProcParamsPtr = PEB.ProcessParameters
                Else
                    Dim PEB As Win32.PEB_32 = GcHandleToStruct(Of Win32.PEB_32)(PebBytesPtr.AddrOfPinnedObject)
                    ProcParamsPtr = PEB.ProcessParameters
                End If
                'Now that we've got the pointer from the ProcessParameters member, we read the RTL_USER_PROCESS_PARAMETERS structure that is stored at that location in the target process' memory
                Dim ProcParamsBytes() As Byte = MemoryReader.Read(ProcParamsPtr, Marshal.SizeOf(GetType(Win32.RTL_USER_PROCESS_PARAMETERS)))
                'Again we use GCHandle.Alloc to get a pointer to the byte array we just read
                Dim ProcParamsBytesPtr As GCHandle = GCHandle.Alloc(ProcParamsBytes, GCHandleType.Pinned)
                Try
                    'Convert the byte array to a RTL_USER_PROCESS_PARAMETERS structure
                    Dim ProcParams As Win32.RTL_USER_PROCESS_PARAMETERS = GcHandleToStruct(Of Win32.RTL_USER_PROCESS_PARAMETERS)(ProcParamsBytesPtr.AddrOfPinnedObject)
                    'Get the CommandLine member of the RTL_USER_PROCESS_PARAMETERS structure
                    Dim CmdLineUnicodeString As Win32.UNICODE_STRING = ProcParams.CommandLine
                    'The Buffer member of the UNICODE_STRING structure points to the actual command line string we want, so we read from the location that points to
                    Dim CmdLineBytes() As Byte = MemoryReader.Read(CmdLineUnicodeString.Buffer, CmdLineUnicodeString.Length)
                    'Convert the bytes to a regular .NET String and return it
                    Return System.Text.Encoding.Unicode.GetString(CmdLineBytes)
                Finally
                    'Clean up the GCHandle we created for the RTL_USER_PROCESS_PARAMETERS bytes
                    If ProcParamsBytesPtr.IsAllocated Then
                        ProcParamsBytesPtr.Free()
                    End If
                End Try
            Finally
                'Clean up the GCHandle we created for the PEB bytes
                If PebBytesPtr.IsAllocated Then
                    PebBytesPtr.Free()
                End If
            End Try
        End Using
    End Function

    'Using this generic function just to make the code in the GetCommandLine function easier to read and save some casting 
    Private Shared Function GcHandleToStruct(Of T)(Pointer As IntPtr) As T
        Return DirectCast(Marshal.PtrToStructure(Pointer, GetType(T)), T)
    End Function

    Public Shared Function Is32BitProcessOn64BitOs(ByVal TargetProcess As Process) As Boolean
        Dim IsWow64 As Boolean = False
        If MethodExistsInDll("kernel32.dll", "IsWow64Process") Then
            Win32.IsWow64Process(TargetProcess.Handle, IsWow64)
        End If
        Return IsWow64
    End Function

    Public Shared Function MethodExistsInDll(ByVal ModuleName As String, ByVal MethodName As String) As Boolean
        Dim ModuleHandle As IntPtr = Win32.GetModuleHandle(ModuleName)
        If ModuleHandle = IntPtr.Zero Then
            Return False
        End If
        Return (Win32.GetProcAddress(ModuleHandle, MethodName) <> IntPtr.Zero)
    End Function

    Public Class Win32

        <StructLayout(LayoutKind.Sequential)>
        Public Structure UNICODE_STRING
            Public Length As UInt16
            Public MaximumLength As UInt16
            '64 bit version of this actually has 4 bytes of padding here (after MaximumLength and before Buffer), but the default Pack size for structs handles this for us
            Public Buffer As IntPtr
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure RTL_USER_PROCESS_PARAMETERS
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
            Public Reserved1() As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=10)>
            Public Reserved2() As IntPtr
            Public ImagePathName As UNICODE_STRING
            Public CommandLine As UNICODE_STRING
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure PEB_32
            <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved1() As Byte
            Public BeingDebugged As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=1)> _
            Public Reserved2() As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)> _
            Public Reserved3() As IntPtr
            Public Ldr As IntPtr
            Public ProcessParameters As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=3)> _
            Public Reserved4() As IntPtr
            Public AtlThunkSListPtr As IntPtr
            Public Reserved5 As IntPtr
            Public Reserved6 As UInteger
            Public Reserved7 As IntPtr
            Public Reserved8 As UInteger
            Public AtlThunkSListPtr32 As UInteger
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=45)> _
            Public Reserved9() As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=96)> _
            Public Reserved10() As Byte
            Public PostProcessInitRoutine As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=128)> _
            Public Reserved11() As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=1)> _
            Public Reserved12() As IntPtr
            Public SessionId As UInteger
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure PEB_64
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved1() As Byte
            Public BeingDebugged As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=21)>
            Public Reserved2() As Byte
            Public LoaderData As IntPtr
            Public ProcessParameters As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=520)>
            Public Reserved3() As Byte
            Public PostProcessInitRoutine As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=136)>
            Public Reserved4() As Byte
            Public SessionId As UInteger
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure PROCESS_BASIC_INFORMATION
            Public Reserved1 As IntPtr
            Public PebBaseAddress As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved2() As IntPtr
            Public UniqueProcessID As IntPtr
            Public Reserved3 As IntPtr
        End Structure

        <Flags()>
        Public Enum ProcessAccess As UInteger
            AllAccess = CreateThread Or DuplicateHandle Or QueryInformation Or SetInformation Or Terminate Or VMOperation Or VMRead Or VMWrite Or Synchronize
            CreateThread = &H2
            DuplicateHandle = &H40
            QueryInformation = &H400
            QueryLimitedInformation = &H1000
            SetInformation = &H200
            Terminate = &H1
            VMOperation = &H8
            VMRead = &H10
            VMWrite = &H20
            Synchronize = &H100000
        End Enum

        <DllImport("ntdll.dll", EntryPoint:="RtlNtStatusToDosError", SetLastError:=True)> _
        Public Shared Function RtlNtStatusToDosError(NtStatus As Integer) As Integer
        End Function

        <DllImport("kernel32.dll", EntryPoint:="IsWow64Process", SetLastError:=True)> _
        Public Shared Function IsWow64Process(ByVal hProcess As IntPtr, <Out()> ByRef Wow64Process As Boolean) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <DllImport("kernel32.dll", EntryPoint:="GetModuleHandle", SetLastError:=True)> _
        Public Shared Function GetModuleHandle(ByVal ModuleName As String) As IntPtr
        End Function

        <DllImport("kernel32.dll", EntryPoint:="GetProcAddress", SetLastError:=True)> _
        Public Shared Function GetProcAddress(ByVal hModule As IntPtr, ByVal MethodName As String) As IntPtr
        End Function

        <DllImport("ntdll.dll", EntryPoint:="NtQueryInformationProcess", SetLastError:=True)>
        Public Shared Function NtQueryInformationProcess(ByVal handle As IntPtr,
                                                         ByVal Processinformationclass As UInteger,
                                                         ByRef ProcessInformation As PROCESS_BASIC_INFORMATION,
                                                         ByVal ProcessInformationLength As Integer,
                                                         ByRef ReturnLength As UInteger) As Integer
        End Function

        <DllImport("kernel32.dll", EntryPoint:="ReadProcessMemory", SetLastError:=True)> _
        Public Shared Function ReadProcessMemory(ByVal hProcess As IntPtr,
                                                 ByVal lpBaseAddress As IntPtr,
                                                 <Out()> ByVal lpBuffer As Byte(),
                                                 ByVal nSize As UInteger,
                                                 <Out()> ByRef lpNumberOfBytesRead As UInteger) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <DllImport("kernel32.dll", EntryPoint:="OpenProcess", SetLastError:=True)> _
        Public Shared Function OpenProcess(ByVal dwDesiredAccess As ProcessAccess,
                                           <MarshalAs(UnmanagedType.Bool)> ByVal bInheritHandle As Boolean,
                                           ByVal dwProcessId As UInteger) As IntPtr
        End Function

        <DllImport("kernel32.dll", EntryPoint:="CloseHandle", SetLastError:=True)> _
        Public Shared Function CloseHandle(ByVal Handle As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function


    End Class




End Class

Hope that helps save someone some time, as it took me longer than I’d like to admit to get some parts of this working.

VbRev Early Alpha Released

As per my previous post, I’ve been working on building a GUI reverse shell utility for the last week or so. The first alpha version has just been released and you can find more information along with a download link on the dedicated page for the tool here: https://vbscrub.com/tools/vbrev/

I’ve also just uploaded a quick video demonstrating what it can do in its current state:

So next time you’re exploring a Windows machine via text based reverse shell, maybe you can give this a try instead 🙂 If you want to report bugs or feature suggestions, please do so here: https://github.com/VbScrub/VbRev/issues

VbRev – A Reverse Shell GUI

We’ve all used netcat reverse shells and similar alternatives to explore remote machines, but I recently found myself thinking it would be nice if I could just explore the file system through a GUI and skip all the constant typing needed to navigate between directories and download files etc.

So I’m developing this little tool that will accept a connection from a remote machine and then let you explore the remote machine with a simple GUI. This is still very early in development and I’ll be posting more updates here and on my twitter soon, but for now here’s a quick look at how it works and what it does.

Just like with netcat reverse shell, the first thing we need to do is start a listener on a specific TCP port on our machine:

Now on the remote machine we run our VbRev.exe program and provide the IP address to connect to (our own machine’s IP address) along with the port that we are listening on. This is very similar to what you would do with netcat’s nc.exe:

Now back on our machine, the GUI has received the connection and shows us the contents of the current directory on the remote machine:

From here we can explore the file system on the remote machine by just clicking on folder names in the address bar or double clicking files/folders in the main file list. You can also type a full path into the address bar:

Clicking on the details link next to the machine name gives us some basic information about the remote machine:

Like I said this is very early in development, so that’s about all I can show for now. Of course you will be able to download and upload files to the remote machine and as you can see from the other tabs in the screenshots there will be plenty more features on top of the basic file system exploration abilities.

I’ll hopefully have an alpha version available for download in a few days with just the basic features working, so keep an eye out for that.

More updates will be posted on my Twitter here:

Port Forwarding Explained (with PT.exe Download)

I recently uploaded a video explaining port forwarding, or port tunnelling as I like to call it. This is a technique that allows you to remotely connect to TCP ports that would not normally allow inbound remote connections (either due to firewall or the way the listener is configured).

EDIT: A few people have asked for my PT.exe program, so here’s a download link (needs more testing and optimising really, so maybe treat this as a BETA version) : https://www.dropbox.com/s/64aebproalc3f0t/PT.zip?dl=1

You can see how to use it, and how it works, in the video below

Ricoh Printer Exploit (Priv Esc To Local System)

This is going to be a bit of a weird post, because this priv esc exploit isn’t actually a successful exploit yet if you’re attacking a server OS – unless you can get into the Print Operators group. It works perfectly well on a workstation OS though so still worth bringing to people’s attention.

First of all, read this: https://www.pentagrid.ch/en/blog/local-privilege-escalation-in-ricoh-printer-drivers-for-windows-cve-2019-19363/

TL;DR:

  • A few Ricoh printer driver DLLs have “Everyone – Full Control” permissions by default
  • When a new printer is added using this driver, the print spooler service (which runs as Local System) will load these DLLs
  • Due to the bad permissons on the DLLs, any user can replace one of them with a malicious DLL. Then they can add a new local printer using the existing driver and at this point the malicious DLL will get loaded by the print spooler service. So now you’ve got your code being executed as Local System
  • Regular users have permission to add new printers as long as they use a  driver that is already installed (so this exploit only works if one of the vulnerable drivers is already installed, as regular users cannot install new drivers)

So if you’re attacking a workstation, you’re good. There’s even a metasploit module that will do all the work for you as usual.

For a server OS though, there’s a problem…

I tried to use this on a Windows Server 2019 machine that had the vulnerable driver installed and it failed. After some testing I discovered that this is because users do not have permission to add printers on a server, even if the driver is already installed. Recall from the bullet points above that we need to add a printer to trigger the print spooler service to load our malicious DLL.

In fact on a server OS, a regular user can’t even run the Powershell command Get-Printer to see a list of printers installed on the system:

The first thing I noticed in both the POC and the metasploit module code is that neither of them call the Windows AddPrinter API directly. The metasploit code calls the built in prnmngr.vbs script (which internally uses WMI) and the POC calls the PrintUIEntry function with rundll32.exe.

So just to make sure this permissions issue was not being caused by something in any of those scripts, I wrote a program that will call the AddPrinter API directly. I also made it call the EnumPrinters API to see if we can at least list existing printers, as even that fails with access denied when tried from Powershell as a non admin user.

Nothing crazy but for what its worth, the full code can be found on my github here: https://github.com/VbScrub/VbAddPrinter

Both of these operations worked perfectly fine as a regular user on my Windows 10 machine. So I moved them over to the target machine which is running Windows Server 2019 but there adding a printer fails with Access Denied:

The enum part of my program does succeed though, so at least we bypassed the Get-Printer access denied issue by calling the EnumPrinters API directly:

I noticed if we replace the original driver’s watermark.dll with a malicious DLL and then do anything that would cause it be accessed, the genuine DLL gets instantly copied back to the directory and overwrites our malicious DLL. This must be why in the original POC they are monitoring the file system for changes to detect when the original DLL gets copied back and then they quickly replace it again with the malicious DLL before it actually gets loaded by the print spooler service. I’ve implemented something similar using the .NET FileSystemWatcher class:

This worked as expected and now our malicious DLL always ends up being loaded instead of the original DLL (although making the program copy the DLL a few hundred times to make sure it sticks was maybe a bit overkill).

So now we have our malicious DLL in place, the problem is that we can only get it to be loaded by a process running as our regular user and not a system service. For example when we try to print to the printer from notepad or access the printer properties etc, our malicious DLL does get loaded but it is loaded by the notepad.exe process which is obviously running as our regular user and not Local System. So it seems like the only time the print spooler service loads these DLLs is when adding a new printer.

One other thing I noticed was that when we fail to add a new printer as a normal user, we see this in the Process Monitor stack trace:

When we do the same thing but as an admin, we don’t see this “AddPrinterCompletedInProc” function and instead we see a load of RPC calls. So I tried using the Samba RPC Client to call the AddPrinter RPC function directly (in the hopes that maybe we bypass the permissions check that is causing it to fail before it tries the RPC stuff), but this also fails with access denied:

I assumed the print spooler service would load all of the printer DLLs when it starts up, so perhaps we could crash the service and then when it auto restarts it would load our DLL. Unfortunately when I restarted the service I found it did not load any of these DLLs. To me it seems quite strange that it even loads the watermark DLL at all when the user adds a printer, because the print spooler service itself doesn’t seem to actually need it (otherwise it would load it itself when a user prints, or when the service starts).

One final weird observation is that in my tests I could only get the attack to work even with the user in the Print Operators group if the driver was set to run in isolated mode. This makes the print spooler service start a separate PrintIsolationHost.exe process when the driver is used and it is that process that loads the watermark.dll when we add a printer. No such issues on a workstation though (which is good as there’s no easy way to specify driver isolation there AFAIK)

In summary: Whilst this works perfectly on a Windows 10 machine, I couldn’t get it to work on a server OS as a regular user unless they were a member of the Print Operators group. Hopefully someone else can take my findings and finish the job 🙂

DC Sync Attacks Explained (Video)

Just uploaded a new video taking an in depth look at how a DC Sync attack works and making use of another Impacket script called secretsdump.py: