Elevation with Impersonation
In the previous section we discussed how the privileges of an access token
decide the access rights of an authenticated user.
Now let’s discuss how we can leverage certain privileges for escalation.
In the past, security researchers have identified nine different privileges
that may allow for privilege escalation from medium integrity to either
high integrity or system integrity, or enable compromise of processes
running as another authenticated user.
Explaining all nine privileges in-depth
and how they may be used to escalate privileges
is beyond the scope of this module,
but we’ll focus on
SeImpersonatePrivilege.
This privilege allows us to impersonateany token
for which we can get a reference, or handle.
SeImpersonatePrivilege is quite interesting
since the built-in Network Service account,
the LocalService account and the default IIS account
have it assigned by default.
Because of this, gaining code execution on a web server
will often give us access to this privilege
and potentially offer the possibility to escalate our access.
If we have SeImpersonatePrivilege
we can often use the DuplicateTokenEx Win32 API,
to create a primary token from an impersonation token
and create a new process in the context of the impersonated user.
When no tokens related to other user accounts are available in memory,
we can likely force the SYSTEM account to give us a token which we can impersonate.
To leverage the SeImpersonatePrivilege privilege,
in this section we are going to use a post exploitation attack
that relies on Windows pipes.
Pipes are a means of interprocess communication,
just like RPC, COM or even network sockets.
A pipe is a section of shared memory inside the kernel
that processes can use for communication.
One process, the pipe server, can create a pipe while other processes,
pipe clients, can connect to the pipe and read/write information from/to it,
depending on the configured access rights for a given pipe.
Anonymous pipes are typically used for communication
between parent and child processes, while named pipes are more broadly used.
In our examples we’ll make use of named pipes,
because they have more functionality and more importantly,
they support impersonation.
The attack that we are going to simulate, based on a technique
developed by the security researcher Lee Christensen,
can force the SYSTEM account to connect to a named pipe set up by an attacker.
While the technique was originally developed as part of
an Active Directory attack it can also be used locally.
It is based on the print spooler service which is started by default
and runs in a SYSTEM context.
We’ll discuss the technique in more detail later.
For now it’s important to understand that the attack is based
on the fact that the print spooler monitors printer object changes
and sends change notifications to print clients by connecting
to their respective named pipes.
If we can create a process running with the SeImpersonatePrivilege
that simulates a print client,
we will obtain a SYSTEM token that we can impersonate.
To demonstrate this, let’s create a C# application that creates a pipe server,
waits for a connection and attempts to impersonate the client
that connects to it.
The first key component of this attack is the ImpersonateNamedPipeClient API,
which allows impersonation of the token from the account that
connects to the pipe if the server has SeImpersonatePrivilege.
When ImpersonateNamedPipeClient is called,
the calling thread will use the impersonated token
instead of its default token.
In order to create our first proof of concept
we’ll have to use the CreateNamedPipe,
ConnectNamedPipe
and ImpersonateNamedPipeClient Win32 APIs.
As the name suggests, CreateNamedPipe creates a pipe.
This API accepts a number of relatively simple arguments.
The first, and most important,
is the pipe name.
All named pipes must have a standardized name format
and must be unique on the system.
The second argument describes the mode the pipe is opened in.
We’ll specify a bi-directional pipe
with the PIPE_ACCESS_DUPLEX enum using its numerical equivalent of 3.
The third argument describes the mode the pipe operates in.
We’ll specify PIPE_TYPE_BYTE to directly write and read bytes,
along with PIPE_WAIT to enable blocking mode.
This will allow us to listen on the pipe until it receives a connection.
We’ll specify the combination of these two modes with the numerical value 0.
The maximum number of instances for the pipe is specified through nMaxInstances.
This is primarily used to ensure efficiency in larger applications,
and any value between 1 and 255 works for us.
nOutBufferSize and nInBufferSize
define the number of bytes to use for the input and output buffer respectively.
We’ll choose one memory page of 0x1000 bytes.
The second-to-last argument defines the default time-out value
that is used with the WaitNamedPipe API.
nce we are using a blocking named pipe,
we don’t care about this and can choose the default value of 0.
For the last argument, we must submit a SID
detailing which clients can interact with the pipe.
We’ll set this to NULL to allow the SYSTEM
and local administrators to access it.
At this point we will create a new Visual Studio solution,
and insert the P/Invoke DllImport statement
along with the call to CreateNamedPipe:
This code expects the pipe name to be passed on the command line.
Next we must invoke ConnectNamedPipe.
The first argument is a handle to the pipe which is returned by CreateNamedPipe,
and the second is a pointer to a structure used in more advanced cases.
In our case, we’ll simply set this to NULL.
Next we add the code required to import and call ConnectNamedPipe .
After we have called ConnectNamedPipe,
the application will wait for any incoming pipe client.
Once a connection is made, we’ll call ImpersonateNamedPipeClient
to impersonate the client.
ImpersonateNamedPipeClient accepts the pipe handle
as its only argument per its function prototype.
The rather simple code additions importing
and calling ImpersonateNamedPipeClient are added next.
At this point, our code will start a pipe server,
listen for incoming connections and impersonate them.
If everything works correctly,
ImpersonateNamedPipeClient will assign the impersonated token
to the current thread,
but we have no way of confirming this
in our current application.
To verify the success of our attack
we can open the impersonated token
with OpenThreadToken,
and then use GetTokenInformation to obtain the SID associated with the token.
Finally, we can call ConvertSidToStringSid
to convert the SID to a readable SID string.
While this confirmation does not have to be part of our final exploit
it helps us understand the attack.
Let’s add these APIs to our code.
Let’s examine the function prototype for OpenThreadToken.
According to the function prototype,
we must first supply a handle to the thread associated with this token.
Since the thread in question is the current thread,
we’ll use the GetCurrentThread Win32 API,
which does not require any arguments and simply returns the handle.
Next we must specify the level of access we want to the token.
To avoid any issues, we’ll ask for all permissions
with its numerical value.
OpenAsSelf specifies whether the API should use
the security context of the process or the thread.
Since we want to use the impersonated token,
we’ll set this to false.
Finally, we must supply a pointer
that will be populated with a handle
to the token that is opened.
Let’s add the required code.
Next we’ll invoke GetTokenInformation.
This API can return a variety of information,
but we’ll simply request the SID.
The first argument is the token we obtained from OpenThreadToken,
and the second argument specifies the type of information
we want to obtain.
TOKEN_INFORMATION_CLASS is an enum
that contains values specifying the type of information we can retrieve
from an access token via GetTokenInformation.
Since we simply want the SID,
we can pass TokenUser,
which has the numerical value of 1
for the TOKEN_INFORMATION_CLASS argument.
TokenInformation is a pointer to the output buffer
that will be populated by the API,
and TokenInformationLength is the size of the output buffer.
Since we don’t know the required size of the buffer,
the recommended way of using the API is to call it twice.
The first time we set these two arguments values to NULL and 0 respectively,
and then ReturnLength will be populated with the required size.
After this we can allocate an appropriate buffer and call the API a second time.
To allocate the TokenInformation buffer
we’ll use the .NET Marshal.AllocHGlobal method,
which can allocate unmanaged memory.
As the final step we’ll use ConvertSidToStringSid
to convert the binary SID to a SID string which we can read.
The first argument is a pointer to the SID.
The SID is in the output buffer that was populated by GetTokenInformation,
but we must extract it first.
One way to do this is to define the TOKEN_USER structure,
which is part of the TOKEN_INFORMATION_CLASS
used by GetTokenInformation,
and then marshal a pointer to it with Marshal.PtrToStructure.
For the last argument, we’ll supply the output string.
Here we can simply supply an empty pointer
and once it gets populated, marshal it to a C# string
with Marshal.PtrToStringAuto.
Let’s add the required structures,
import,
and code.
On the last line of code, we print the SID associated with the token to the console,
showing which user we impersonated.
Now we have finally written all the code we need
to start our test and better understand
the use of named pipes for impersonation and privilege escalation.
As previously mentioned,
we must execute the code in the context of a user account
that has the SeImpersonatePrivilege access right.
For our attack demonstration
we’ll log into “appsrv01” as the domain user admin,
and use PsExec to open a command prompt as the built-in “Network Service” account.
Before we execute our application, we can verify the user
and the presence of SeImpersonatePrivilege in the new command prompt.
Now we can transfer it to “appsrv01”.
Next we execute it and supply a random pipe name.
To simulate a connection we can open an elevated command prompt
and write to the pipe.
When we switch back to the command prompt running our application,
we find that a SID has been printed.
Our code has impersonated a token and resolved the associated SID.
To verify that this SID belongs to the administrator account
we can switch back to the elevated command prompt and dump it.
This proves that we have indeed impersonated
the built-in domain administrator account.
More importantly, we can impersonate anyone who connects to our named pipe.
It’s now time to test our application leveraging the print spooler service.
Communication to the spooler service is done through
Print System Remote Protocol,
which dates back to 2007 and is not well documented.
Fortunately for us, the MS-RPRN works through named pipes.
The potential for abuse comes from the RpcOpenPrinter
and RpcRemoteFindFirstPrinter ChangeNotification functions.
RpcOpenPrinter allows us to retrieve a handle for the printer server
which is used as an argument to the second API.
RpcRemoteFindFirst PrinterChangeNotification
essentially monitors printer object changes
and sends change notifications to print clients.
Once again, this change notification
requires the print spooler to access the print client.
If we ensure that the print client is our named pipe
it will obtain a SYSTEM token that we can impersonate.
Sadly, unlike regular Win32 APIs, MS-RPRN APIs can not be called directly.
Print spooler functionality resides in an unmanaged library,
and is called through a proxy function,
that uses a binary format to pass and invoke underlying functions.
The implementation of these calls are beyond the scope of this module.
Luckily, we can use the SpoolSample C# implementation
written by Lee Christensen
or the PowerShell code written by Vincent Le Toux.
When we use SpoolSample,
we must specify the name of the server to connect to,
and the name of the server we control, also called the capture server.
Since we are performing the attack locally, both servers are the same.
This presents a challenge.
The print spooler service, running as system on the victim,
needs to contact the simulated print client through our pipe
but since they are on the same host they in effect require the same pipe name.
Because of this, we cannot create the named pipe with the required name.
In order to find a solution, we first must understand the problem in detail.
To do this, we will monitor the target system with SysInternals Process Monitor
while executing SpoolSample.exe against an arbitrary pipe name.
First, we’ll configure a capture filter with Filter -> Filter,
and select “Process Name” from the drop down menu,
setting this to “spoolsv.exe”.
We’ll then click “Add” followed by “Apply” and exit the filter menu by selecting “OK”
Then, we’ll execute SpoolSample and
specify the current hostname followed by an arbitrary pipe name.
Although the application output
indicates that a printer notification callback was configured,
Process Monitor shows that no access to the arbitrary pipe name has occurred.
The reason for this is that before attempting to access the client pipe,
the print spooler service validates the pipe path,
making sure it matches the default name.
Our arbitrary pipe “test” fails this validation
and consequently,
the print spooler service doesn’t even attempt to connect to the client.
This is why we don’t see any connections in Process Monitor.
Unfortunately, as mentioned before, we cannot specify “spoolss” as a name
since it is already in use by the print spooler service we are targeting.
At this point it is useful to know what happens when a file path
is supplied to a Win32 API.
When directory separators are used as a part of the file path,
they are converted to canonical form.
Specifically, forward slashes will be converted to backward slashes.
This is also known as file path normalization.
Interestingly enough, a security researcher …
discovered that if we provide SpoolSample with an arbitrary pipe name
containing a forward slash after the hostname,
the spooler service will not interpret it correctly
and it will append the default name,
to our own path before processing it.
This effectively bypasses the path validation and the resulting path,
is then normalized before the spooler service attempts to send
a print object change notification message to the client.
This obviously can help us because this pipe name
differs from the default one used by the print spooler service,
and we can register it to simulate a print client.
To verify this, we can repeat our last example,
this time supplying an arbitrary pipe name that contains a forward slash
in the print client name.
We receive an error,
and Process Monitor confirms the theory.
First, the path we supplied has been switched
to a canonical form as part of the full path.
Second, spoolsv.exe attempted to access the named pipe.
Since we have not created a pipe server by that name, the request failed.
At this point, we just need to create a pipe server
with that name and simulate a print client.
When we execute SpoolSample, the print spooler service will connect to our pipe.
To do this, we’ll open another command prompt as “Network Service”,
because we are demonstrating a scenario where we have exploited a process
that has the SeImpersonatePrivilege, and we are trying to escalate to SYSTEM.
and launch our application.
Now we’ll invoke SpoolSample
to trigger the change notification against the capture server.
Our application reveals a connection from the “S-1-5-18” SID.
This SID value belongs to the SYSTEM account
proving that our technique worked.
Excellent!
We now have a way of forcing the SYSTEM account to authenticate to our named pipe
which allows us to impersonate it.
To complete this attack we must now take advantage of the impersonated token,
which we will do by first launching a command prompt as SYSTEM.
The Win32 CreateProcessWithTokenW API can create a new process based on a token.
The token must be a primary token,
so we’ll first use DuplicateTokenEx
to convert the impersonation token to a primary token.
First, we’ll supply the impersonation token,
by recovering it with OpenThreadToken.
We’ll request full access to the token with the numerical value 0xF01FF
for the dwDesiredAccess argument.
For the third argument,
we’ll use a default security descriptor for the new token
by setting this to NULL.
ImpersonationLevel must be set to SecurityImpersonation,
which is the access type we currently have to the token.
This has a numerical value of “2”.
For the TokenType, we’ll specify a primary token by setting this to “1”.
The final argument is a pointer that will be populated with the handle
to the duplicated token.
With the token duplicated as a primary token,
we can call CreateProcessWithToken to create a command prompt as SYSTEM.
First, we’ll supply the newly duplicated token,
followed by a logon option which we set to its default of 0.
For the third and fourth arguments, we’ll supply NULL
and the full path of cmd.exe respectively.
The creation flags,
environment block and current directory arguments can be set to 0,
NULL and NULL respectively to select the default options.
For the two last arguments we must pass STARTUPINFO
and PROCESS_INFORMATION structures,
which are populated by the API during execution.
Neither of these are defined in P/invoke imports
so we must define them ourselves as shown in the following code:
With all the code written, we’ll compile it,
and transfer it to the Windows Server 2019 machine.
We’ll execute this just as before,
by first launching our application in the context of the network service
to create the pipe server on appsrv01.
Next, we’ll launch SpoolSample with the capture server set to
appsrv01/pipe/test,
which will force the SYSTEM account to connect to our named pipe
and a new command prompt is opened.
When we interact with it and display the user we find it to be SYSTEM:
With this attack, we can elevate our privileges
from an unprivileged account that has SeImpersonatePrivilege
to SYSTEM on any modern Windows system
including Windows 2019 and the newest versions of Windows 10.
Nice!
Most native and third-party services that do not require administrative permissions
run as Network Service or Local Service, partly due to Microsoft’s recommendation.
This attack technique means that compromising a unprivileged service
is just as valuable as a SYSTEM service.
In the next section,
we’ll demonstrate how to impersonate tokens from other authenticated users
instead of simply advancing straight to SYSTEM.