Downgrading Kerberos Encryption & Why It Doesn’t Work In Server 2019

This took me a while to get my head around recently so I figured I’d document it here in case it helps someone else.

Thanks to @exploitph for answering several questions about this and @harmj0y for writing this blog post which explains the entire process. Whilst that post does go into a lot of detail, it also covers several other things as well and at the time it was written Windows Server 2019 was not a thing. So I decided to make a post that keeps things relatively brief and explains why this trick no longer works in Server 2019.

A quick Kerberos refresher

Here’s a simplified description of some things you need to know for this post to make any sense:

  • A TGT is a special ticket that a user receives from a domain controller when they first log on
  • You use the TGT to request service tickets, which let you access services like SQL server, SMB, etc
  • Service tickets are encrypted using the password of the user/computer account that runs that service
  • Kerberoasting is the name of an attack where we request service tickets and then try to crack them to get the service account’s password

If you don’t fully understand any of these things then my Kerberos video and Kerberoasting video should help.

Why do we want to get our own TGT?

There are some built in functions in Windows that let us request Kerberos authentication to a particular service, but the problem for us as an attacker is that these APIs are designed to be opaque. Windows handles everything for us, so we can’t directly control anything in these requests (other than which service we want a ticket for).

This means that the service tickets we get back will be encrypted with whatever level of encryption Windows decides to use. If the user account running the service is configured to support AES encryption then that is what will be used to encrypt the service tickets, which makes the process of cracking the passwords much slower and practically impossible in some cases.

If we could get hold of a usable TGT, then we could use it to craft our own service ticket requests and specify that they only support the weaker RC4 encryption.

Getting a user’s TGT and using it for Kerberoasting

If we know the user’s password (or even just a hash of their password) then we can request a TGT with Rubeus like so:

rubeus.exe asktgt /domain:mydomain.local /user:mydomain.local\username /password:Passw0rd

or in the GUI version:

Then all we need to do is copy the base64 that it gave us (or export to a .kirbi file) and then supply that to the Kerberoast function.

First of all though let’s look at what happens if we try Kerberoasting without supplying our own TGT, just using the normal Windows APIs to request service tickets:


When we let Windows handle things for us we get a service ticket encrypted using the highest level of encryption that the service account supports (in this case AES-128, as the account supports RC4 and AES-128).

Now let’s try again but supply our own TGT that we exported earlier:


As you can see with this method we have successfully downgraded the encryption to RC4, which will be much easier to crack.

So problem solved… right?

Well in this case yes, but it turns out there are two scenarios where this approach won’t work.

Problem 1: The DC we request a TGT from is running Windows Server 2019

If we try the same technique to downgrade the encryption but the domain controller we get a TGT from is running Server 2019 or newer, something different happens.

We supply our own TGT and send the same request as before that says we only support RC4 encryption, as you can see in Wireshark (ARCFOUR = RC4):

But this time the server responds with a service ticket encrypted with AES-128 instead of RC4:


Unfortunately it turns out that any DC running Server 2019 will ignore the encryption type that the client requested and will always use the highest level of encryption that the service account supports.

I’m not aware of any workaround for this at the moment. You’re stuck with AES encryption if the service account is marked as supporting it. Luckily AES is not enabled on new user accounts by default, but it is very easy to enable it.

Problem 2: We have code executing as the user but don’t know their password

Without knowing the user’s password (or a hash of their password) we can’t request a TGT for them like we did before.

Luckily there is another way we can get their TGT, which is known as the TGT delegation trick.

This trick takes advantage of the fact that domain controllers are configured to allow unconstrained delegation by default:

This just means that any service running on this machine is allowed to impersonate users that authenticate with it – a perfectly legitimate thing that services often need to do.

However, for the server to be able to impersonate a user it needs a TGT for that user. So when a client gets a ticket for a service that supports delegation it also sends an additional request to get a new TGT that can be passed to that service for it to use for impersonation.

As an attacker executing code as a domain user we can mimic this behaviour just using the standard Windows APIs, but instead of sending the new TGT to a service we just extract it and use it for Kerberoasting like we did in the original demo.

This is what happens if you use the /tgtdeleg option when Kerberoasting with Rubeus, or select this option in the GUI

This is a clever solution to the problem (assuming you can get Rubeus to run as the user without getting blocked by AV) but as mentioned above it won’t work with 2019 DCs.

Further reading

I simplified some things in this post as I didn’t want it to be overly long and complex (not sure I succeeded there…) but if you want to understand specific details then these resources should help:


How Windows Stops Kerberos Usernames Being Case Sensitive

We’re all used to Windows treating usernames as not being case sensitive, i.e. a username of JOE.BLOGGS is the same as joe.bloggs. However this idea falls apart when you use that username as part of the salt for AES encryption, which is case sensitive. This is exactly what happens when we perform Kerberos preauthentication using AES encryption (the default encryption level in current versions of Windows).

So why doesn’t this cause problems when we supply the username in the incorrect case?

For example:

In AD we have a user setup with a username that has uppercase letters in it, in this case “joe.BLOGGS”

Then we run a command like Net Use and specify the username in all lowercase:

Net Use \\server.kerb.local\Share /user:kerb.local\joe.bloggs

This does use Kerberos and does use the correct case username for the AES salt even though we didn’t supply it like that, but how?

The short answer:

Windows sends an authentication request without any encrypted preauth data to begin with (so the AES salt is not needed yet). The DC responds with an error saying preauth is required but it also includes the username in the exact same case it is stored as in AD. So then Windows just uses that as the salt for the encryption and tries again, which should be successful.

The long answer:

When we execute something like the Net Use command above, regardless of whether we specify the username in the correct case this is what happens:

1: Windows sends an authentication request (AS-REQ) without encrypting any preauthentication data, but telling the server that it supports AES encryption.

2: The server responds with a message that contains the error “PREAUTH_REQUIRED” but importantly it also includes the following section because the original request said it supported AES:

As you can see this tells us the exact salt to use for this user, which is the domain name in uppercase (KERB.LOCAL) followed by the username in the exact same case that was in the user’s AD account (joe.BLOGGS).

3: So now Windows can just send another AS-REQ but this time using the correct salt to encrypt the preauth data (which is a timestamp to avoid replay attacks, as I explained in this video):

4: This time the server responds with an AS-REP which contains the user’s TGT and then they can go ahead and use that to request service tickets.


If the user account has preauth disabled then none of this matters as the client doesn’t need to encrypt anything using their username as a salt. So after the first AS-REQ the server just responds with the AS-REP containing the user’s TGT (message 4 in the info above).

Also some attacker tools skip straight to the second AS-REQ that includes encrypted preauth data, so they do need the username to be in the correct case. For example if you the “brute” command in Rubeus and supply usernames and passwords but with the usernames not matching the case they are in AD, it will tell you that no valid credentials were found even though you did have a correct username and password combo. I’m hoping to work around this in my Rubeus GUI tool but I’ll make another post about that soon. EDIT: After way more work than I anticipated, I’ve now made my Rubeus GUI tool handle this correctly so usernames are no longer case sensitive 🙂

Lastly, if any of the Kerberos lingo above didn’t make much sense to you, check out my in depth explanation of Kerberos here:

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:

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,
                                                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:

    PVOID Reserved1;
    PPEB PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;

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
    End Sub

    Public Function Read(ByVal MemoryAddress As IntPtr, ByVal Count As Integer) As Byte()
        If _TargetProcessHandle = IntPtr.Zero Then
        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
            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
            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
                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()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
    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:

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
                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))
                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)
                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
                    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)
                    '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)
                    'Clean up the GCHandle we created for the RTL_USER_PROCESS_PARAMETERS bytes
                    If ProcParamsBytesPtr.IsAllocated Then
                    End If
                End Try
                'Clean up the GCHandle we created for the PEB bytes
                If PebBytesPtr.IsAllocated Then
                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

        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

        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

        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

        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

        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

        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:

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:

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) :

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

Ricoh Printer Exploit (Priv Esc To Local System)

First of all I should point out that at the moment this priv esc exploit only works on a workstation OS and not on a server OS (unless you can get into the Print Operators group). This post is about me trying, and failing, to get it to work on a server OS.

For more info on the exploit itself, read this:


  • 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:

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 🙂