An Unconventional Exploit for the RpcEptMapper Registry Key Vulnerability

A few days ago, I released Perfusion, an exploit tool for the RpcEptMapper registry key vulnerability that I discussed in my previous post. Here, I want to discuss the strategy I opted for when I developed the exploit. Although it is not as technical as a memory corruption exploit, I still learned a few tricks that I wanted to share.

In the Previous Episode…

Before we begin, here is a brief summary of my last blog post. On Windows 7 / Server 2008 R2, two service registry keys are configured with weak permissions: RpcEptMapper and DnsCache. Basically, these permissions provide a regular user with the ability to create subkeys. This issue is pretty simple to leverage. One just has to create a Performance key and populate it with a few values, among which is the absolute path of a Performance DLL. In order for this DLL to be loaded by a privileged user, one just has to query the Performnance counters of the machine (by invoking Get-WmiObject Win32_Perf in PowerShell for example). When doing so, the WMI service should load the DLL as NT AUTHORITY\SYSTEM and execute three predefined functions that must be exported by the library (and that are also configured in the Performance registry key).

WMI and the Performance Counters

The documentation states that “Windows Performance Counters provide a high-level abstraction layer that provides a consistent interface for collecting various kinds of system data such as CPU, memory, and disk usage. Further, in the About section, you can read that there are four main ways to use Performance counters, and one of them is through the WMI Performance Counter Classes. This is more or less how I found out that you could query these counters with Get-WmiObject Win32_Perf in PowerShell. This type of interaction is potentially very interesting because it involves a very common type of IPC (Inter-Process Communication) on Windows which is RPC (Remote Procedure Call), or more precisely DCOM (Distributed Component Object Model) in this case (DCOM works on top of RPC). Such mechanism is especially required when a low-privileged process needs to interact with a more privileged one, such as the WMI service in our case.

This explains why, as a regular user, we were able to force the WMI service to load our DLL. However, this type of trigger is a double-edged sword. Indeed, when I initially worked on the Proof-of-Concept, I noticed that, on rare occasions, the DLL would not be loaded as NT AUTHORITY\SYSTEM and the exploit would thus fail. Why is that? The answer is: “impersonation”.

When interacting with another process, especially if it is a privileged service, impersonation is very common. To understand why it is so important, you have to keep in mind that, whenever you invoke a Remote Procedure Call, you literally ask another process to execute some code and, often, using parameters that are under your control. If you are able to force a service to do things such as move an arbitrary file to an arbitrary location as NT AUTHORITY\SYSTEM for example, this can have nasty consequences. To solve this problem, the Windows API provides almost as many impersonation functions as there are IPC mechanisms. For instance, you can invoke ImpersonateNamedPipeClient as a named pipe server or RpcImpersonateClient as an RPC server.

In the case of the Performance counters, when you instantiate the remote class Win32_Perf, I observed that the WMI service sometimes creates a dedicated wmiprvse.exe process that runs as NT AUTHORITY\LOCAL SERVICE. In this case, it always impersonates the client, as illustrated on the screenshot below.

Honestly, I haven’t spent any time trying to figure out why the service would sometimes load the DLL as LOCAL SERVICE, or as SYSTEM on other occasions. What I observed though is that, if you wait long enough and then try again, the DLL would be loaded as SYSTEM. This was enough for me because I didn’t want to spend too much time on this as I have other (more interesting) projects I want to work on. Therefore, in the rest of this article, we will just consider that we have the ability to load our DLL in the context of NT AUTHORITY\SYSTEM.

Exploit Development

The idea is to build a standalone exploit. In other words, as a pentester, I want to be able to drop a simple executable on a vulnerable machine and just execute it to get a SYSTEM shell, without having to configure the registry manually or compile a DLL every time.

Now, in order to define a proper strategy to get there, we need to list the starting conditions. First, we know that a machine reboot is not required. We already know that the DLL loading can be triggered through the instantiation of a WMI class. This is very easy to do with a high-level script engine such as PowerShell but doing the same thing in C/C++ will probably require a bit of work. Then, we know that, although we modify the configuration of the RpcEptMapper (or DnsCache) service, the DLL is actually loaded by the WMI service. We will see why this is important in a moment. Last but not least, we want to be able to get a SYSTEM shell in our console but the DLL is actually loaded by a service in a totally different session, so we will have to find a way to solve this problem.

Instantiate a WMI Class in C/C++

I really want to emphasize that, when you use a command such as Get-WmiObject Win32_Perf in PowerShell, there is a loooooot of stuff going on under the hood. These cmdlets are extremely powerful and make the life of system administrators and developers a lot easier. Unless you have at least tried to develop a client program for a DCOM interface in C/C++, you cannot really realize that. Fortunately, the documentation provided by Microsoft is pretty good and contains several detailed examples. They even provide a complete code snippet. In order to help you realize what I’ve just said, you should just know that this sample code is written in more than 250 lines of C/C++ and that the only thing it does is query a single counter, whereas Get-WmiObject Win32_Perf actually collects all the available counters. To me, this is really mind-blowing!

In the end, here is a completely stripped-down version of the code I eventually came up with to trigger the DLL loading within the WMI service.

CoInitializeEx(NULL, COINIT_MULTITHREADED);
CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, 0);
CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (void**)&pWbemLocator);
bstrNameSpace = SysAllocString(L"\\\\.\\root\\cimv2");
pWbemLocator->ConnectServer(bstrNameSpace, NULL, NULL, NULL, 0L, NULL, NULL, &pNameSpace);
CoCreateInstance(CLSID_WbemRefresher, NULL, CLSCTX_INPROC_SERVER, IID_IWbemRefresher, (void**)&pRefresher);
pRefresher->QueryInterface(IID_IWbemConfigureRefresher, (void**)&pConfig);
pConfig->AddEnum(pNameSpace, L"Win32_Perf", 0, NULL, &pEnum, &lID);
pRefresher->Refresh(0L);
pEnum->GetObjects(0L, dwNumObjects, apEnumAccess, &dwNumReturned);

The first function calls are pretty standard when working with DCOM. CoInitializeEx and CoInitializeSecurity are necessary to set up a basic communication channel between the client and the DCOM server. Then the first CoCreateInstance gives you an initial pointer to the IWbemServices interface for WMI. This allows you to access the root/cimv2 namespace once you have invoked ConnectServer. If you have ever used WMI queries in PowerShell, this should sound familiar. After that, you can access the class you want. Here I picked Win32_Perf because I knew it already worked with Get-WmiObject Win32_Perf. Finally a first call to GetObjects is required to get the number of objects that will be returned by the server. This way, the client can allocate enough memory and then call GetObjects a second time to get the actual data. Yeah, we are doing quite low-level stuff in comparison to PowerShell so you have to do this kind of things yourself… Anyway, in our case, this second call is not required as the first one is enough to trigger the collection of the Performance counters’ data.

Communicate with the WMI Service (or not?)

Here comes the interesting part. This part is the true reason why I wanted to write about this particular exploit in the first place. Indeed, I used quite an unconventional trick to have a SYSTEM shell spawn in the same console.

When dealing with a privilege escalation exploit that involves DLL loading (in a privileged service), a very common problem arises, especially when you want to develop a standalone tool. From your exploit tool, how do you interact with the code that is executed within your DLL? Let’s say you want to spawn a SYSTEM command prompt. You can invoke CreateProcess for example but then… what? Well, good job, the command prompt has just spawned on the service’s Desktop (in session 0) and you have no way to interact with it. To solve this problem, exploit writers usually use IPC mechanisms to create a communication channel so that the client (i.e. the exploit) can interact with the server (i.e. the exploited service). For example, you can set up some named pipes, a main one to accept client requests and then three other ones so that the client can access the stdin/stdout/stderr I/O of the created process. This is actually how PsExec works by the way. The same thing can also be achieved using a TCP socket. From the exploited service (i.e. within the code of the DLL), you could bind to a local TCP port, create a process and then redirect its input/output to the socket. This way, a client just needs to connect to the local TCP port to interact with the created process. This is how bind shells work.

These are well-known techniques that I also used in previous exploits. This time though, I wanted to opt for something a bit different. And by “a bit different”, I actually mean “completely different” because I did not use any IPC mechanism at all! Or at least, not a conventional one…

In our scenario, as opposed to a typical DLL hijacking for example, we start with a significant advantage that I intentionally omitted to mention: we control the full path, and most importantly the name of the DLL that will be loaded by the privileged service. I insist on this detail because the name of the DLL itself can convey all the information we need, without having to rely on a standard IPC mechanism.

Here is the plan in a few basic steps:

  1. Create a process in the background, in a suspended state.
  2. Write the embedded DLL payload to an arbitrary location, such as the user’s Temp folder.
  3. Create the Performance subkey and populate it with the appropriate values, especially the path of the DLL file that was previously created.
  4. Instantiate the WMI class Win32_Perf to trigger the DLL loading.

At step 1, the idea is to spawn a cmd.exe Process for example. As a result of the CreateProcess call, you will get the handle and the ID that are associated to the created Process and its main Thread. At step 2, when writing the payload DLL to the disk, we will include the ID of the Process that has just been created in the name of the file (e.g.: performance_1234.dll). You’ll see why in a moment. Step 3 and 4 were already mentioned in the introduction and are rather self-explanatory.

From the service’s standpoint, the DLL will first be loaded and DllMain will be invoked. Then, it will call OpenPerfData, which is one of the three functions that are exported by our library. As a side note, this is particularly convenient because we can write our code outside of the loader lock. The OpenPerfData function is where our payload will be executed. From there, we can first retrieve the name of the module (i.e. the name of the DLL file). Since the filename contains the PID of the Process we initially created as a regular user, we will be able to perform some “adjustments” on it, in the context of NT AUTHORITY\SYSTEM.

What I mean by “adjustments” is that we will actually replace its primary Token

Replace a Process Level Token

As a reminder, on Windows, a Token represents the security context of a user in a Process or Thread. It holds some information such as the groups a user belongs to or the privileges it has. They can be of two types: Primary or Impersonation. A Primary Token is associated to a Process whereas an Impersonation Token is associated to a Thread. Thread level (i.e. Impersonation) Tokens are relevant when a service wants to impersonate a client for example. Process level Tokens, on the other hand, are not really meant to be replaced at runtime.

Before working on this exploit, my assumption was that, unlike Thread level Tokens, Process level Tokens were immutable. As soon as a Process is created, I thought that you could not change its Primary Token, unless you were able to execute some arbitrary code in the Kernel. But I was wrong! Though, in my defense, I have to say that this involves some undocumented stuff.

The exploit steps I mentioned previously should make a little more sense now. From within the DLL (i.e. in the context of the privileged service), here are the steps we need to follow:

  1. Create a copy of the current Process’ Token (by calling DuplicateTokenEx).
  2. Open the the client’s Process. We can do that because we know its PID.
  3. Replace the client’s Process Token with the one we created at step 1.

The first step is very simple and self-explanatory. Steps 2 and 3 are self-explanatory as well but they actually require very specific privileges. Fortunately for us, it seems that the Token of the WMI service’s Process has all the privileges that exist on Windows, as illustrated on the screenshot below. Anyway, as long as we execute arbitrary code in the context of NT AUTHORITY\SYSTEM we can recover any privilege we want.

Below is a stripped-down version of the code that allows us to replace the Token of the Process that was created by the client with the copy of the current SYSTEM Token. It should be noted that, for this operation to succeed, the target Process must be in a SUSPENDED state.

PROCESS_ACCESS_TOKEN tokenInfo = { 0 };
tokenInfo.Token = hSystemTokenDup;
tokenInfo.Thread = NULL;

NtSetInformationProcess(
    hClientProcess,                 // Client's Process handle (PROCESS_ALL_ACCESS)
    ProcessInformationClassMax,     // Type of information to set
    &tokenInfo,                     // A reference to a structure containing the SYSTEM Token handle
    sizeof(tokenInfo)               // Size of the structure
);

As briefly mentioned previously, the NtSetInformationProcess function is not documented. It is part of the Native API so it is not directly accessible within the Windows SDK. You have to define its prototype in your own header file and then import it manually from the NTDLL at runtime. Anyway, this function is very powerful as it allows you to change the Token of a Process, even after it has been created. Pretty cool, isn’t it?

Once this is done, the client can simply resume the main Thread and that’s it! You get a nice SYSTEM shell in your console.

Conclusion

There are some implementation steps I did not mention in this post because the main point was to discuss how we could develop an exploit that does not require IPC communications. For example, I used a Global Event in order to synchronize the main exploit with the payload that is executed within the DLL. But, the exploit would have worked without it as well.

Then, when I say that I did not use Inter-Process Communications, one could argue that it is not completely true because I did use the name of the DLL to convey some information in the end. But, it turns out that it is not strictly required either. You could still do the exact same thing without using this trick. For example, you could implement something very similar to a “egg hunter”. You could create an egg (e.g.: a predefined string) in the memory of your process and then, from within the DLL, you could open every single Process and search their memory to find this egg. From there, you can get the ID of the main Process and thus determine the ID of its child Process as well. It was just way more convenient to communicate the PID directly through the filename here.

With this little exploit development process, I learned a few things that I wanted to share. So, as always, I hope you have learned something too by reading this. That’s it for today!