Post

Windows Server 2008R2-2019 NetMan DLL Hijacking

What if I told you that all editions of Windows Server, from 2008R2 to 2019, are prone to a DLL Hijacking in the %PATH% directories? What if I also told you that the impacted service runs as NT AUTHORITY\SYSTEM and that the DLL loading can be triggered by a normal user, on demand, and without the need of a machine reboot? Provided that you found some %PATH% directories configured with weak permissions, this would probably be the most straightforward privilege escalation technique I know. I don’t know why there hasn’t been any publication about this yet. Anyway, I’ll try to fill this gap.

Foreword

To start things off, I probably don’t need to clarify this but DLL hijacking is not considered as a vulnerability by Microsoft (source). I tend to agree with this statement because, by default, even if a DLL is loaded from the %PATH% directories by a process running with higher privileges, this behavior cannot be exploited by a normal user. Though in practice, and especially in corporate environments, it’s quite common to see third-party applications configured with weak folder permissions. In addition, if they add themselves to the system’s %PATH%, the entire system is then put at risk. My personal opinion on the subject is that Microsoft should prevent these uncontrolled DLL loadings as far as possible in order to prevent a minor configuration issue affecting a single application from becoming a privilege escalation attack vector with a way higher impact.

Back to Basics: Searching for DLL Hijacking Using Procmon

This discovery is the unexpected result of some research I was doing on Windows 2008 R2. Although the system is no longer supported, it’s still widespread in corporate networks and, I was looking for the easiest way of exploiting binary planting through my CVE-2020-0668 exploit. I’ve done a lot of research on Windows 10 Worsktation during the past few months and working back on Windows 7/2008 R2 required me to forget about some techniques I’ve learned and to restart from the beginning. My original problem was: how to easily exploit arbitrary files writes on Windows 2008 R2?

My first instinct was to start with the IKEEXT service. On a default installation of Windows 2008 R2, this service is stopped, and it tries to load the missing wlbsctrl.dll library whenever it’s started. A normal user can easily trigger this service simply by attempting to initiate a dummy VPN connection. However, starting it only once affects its start mode, it goes from DEMAND_START to AUTOMATIC. Leveraging this service under such circumstances would therefore require a machine reboot, which makes it a far less interesting target. So, I had to look for other ways. I also considered the different DLL hijacking opportunities documented by Frédéric Bourla in his article entitled “A few binary plating 0-days for Windows” but they are either not easy to trigger or appear quite randomly.

I decided to begin my research process with firing up Process Monitor and checking for DLL loading events failing with a NAME NOT FOUND error. In the context of an arbitrary file write exploit, the research doesn’t have to be limited to the %PATH% folders so this yields a lot of results! To refine the research, I therefore added a constraint. I wanted to filter out processes which try to load a DLL from the C:\Windows\System32\ folder and then find it in another Windows folder, especially if they need it to function properly. The objective is to avoid a Denial of Service as far as possible.

I considered 3 DLL hijacking cases:

  • A program loads a DLL which doesn’t exist in C:\Windows\System32\ but exists in another Windows directory, C:\Windows\System\ for example. Since the C:\Windows\System32\ folder has a higher priority, this could be a valid candidate.
  • A program loads a non-existing DLL but uses a safe DLL search order. Therefore, it only tries to load it from the C:\Windows\System32\ folder for example.
  • A program loads a non-existing DLL and uses an unrestricted DLL search order.

The first case might lead to Denial of Service so I left it aside. The second case is interesting but can be a bit difficult to spot amongst all the results returned by Procmon. The third case is definetly the most interesting one. If the DLL doesn’t exist, the risk of causing a Denial of Service when hijacking it is reduced and it’s also easy to spot in Procmon.

To do so, I didn’t add a new filter in Process Monitor. Instead, I simply added a rule which highlights all the paths containing WindowsPowerShell. Why this particular keyword, you may ask. On all (modern) versions of Windows, C:\Windows\System32\WindowsPowerShell\v1.0\ is part of the default %PATH% folders. Therefore, whenever you see a program trying to load a DLL from this folder, it most probably means that it is prone to DLL Hijacking.

I then tried to start/stop each service or scheduled task I could. And, after having spent a few hours staring at Procmon’s output, I finally saw this:

Wait, what?! Is this really what I think it is? :astonished: Is this a non-existing DLL being loaded by a service running as NT AUTHORITY\SYSTEM? My first thought was: “if wlanhlp.dll is a hijackable DLL, I should already know about it, I must have made a mistake somewhere or I must have installed some third-party app causing this”. But then I remembered. Firstly, I’m using a fresh install of Windows Server 2008 R2 in a dedicated VM. The only third party application is “VMware Tools”. Secondly, all the research I’ve done so far was mostly on Worstation editions of Windows because it’s often more convenient. Could it be the reason why I saw this event only now?

Fortunately, I have another VM with Windows 7 installed so I quickly checked. It turns out that this DLL exists on a Workstation edition!

If you think about it, if wlanhlp.dll is really related to Wlan capabilities as its name implies, it would make sense. The Wlan API is only available on Workstation editions by default and must be installed as an additional component on Server editions. Anyway, I must be on to something…

NetMan and the Missing Wlan API

Let’s start by looking at the properties of the event in Procmon and learn more about the service.

The process runs as NT AUTHORITY\SYSTEM, that’s some good news for us. It has the PID 972 so let’s check the corresponding service in the Task Manager.

Three services run inside this process. Looking at the Stack Trace of the event in Procmon, we should be able to determine the name of the one which tried to load this DLL.

We can see an occurrence of netman.dll so the corresponding service must be NetMan (a.k.a. Network Connections). That’s one problem solved. If we take a closer look at this Stack Trace, we also notice several lines containing references to RPCRT4.dll or ole32.dll. That’s a good sign. It means that this event was most probably triggered through RPC/COM. If so, there is a chance we can also trigger this event as a normal user with a few lines of code but I’m getting ahead of myself.

This DLL hijacking opportunity is due to the fact that the Wlan API is not installed by default on a server edition of Windows 6.1 (7 / 2008 R2). The question is: does the same principle apply to other versions of Windows? :thinking:

Luckily, I use quite a lot of virtual machines for my research and I had instances of Windows Server 2012 R2 and 2019 already set up so it didn’t take long to verify.

On Windows Server 2012 R2, wlanhlp.dll doesn’t show up in Procmon. However wlanapi.dll does instead. Looking at the details of the event, it turns out that it is identical. This means that Windows 6.3 (8.1 / 2012 R2) is also “affected”.

Ok, this version of Windows is pretty old now, Windows 2019 cannot be affected by the same issue, right? Let’s check this out…

The exact same behavior occurs on Windows Server 2019 as well! :smirk: I ended up checking this on all possible versions of Windows Server from 2008 to 2019. I won’t bore you with the details, all the versions are prone to this DLL hijacking. The only one which I couldn’t test thoroughly was Server 2008, I wasn’t able to reproduce the issue on this one.

How to Trigger this DLL Hijacking Event on Demand?

Let’s summarize the situation. On all versions of Windows Server, the NetMan service, which runs as NT AUTHORITY\SYSTEM, tries to load the missing wlanhlp.dll or wlanapi.dll DLL without using a safe DLL search order. Therefore it ends up trying to load this DLL from the directories which are listed in the system’s %PATH% environement variable. That’s a great start I’d say! :slightly_smiling_face:

The next step is to figure out if we can trigger this event as a normal user. I already mentionned that this behavior was due to some RPC/COM events but it doesn’t necessarily mean that we can trigger it. This event could also be the result of two services communicating with each other through RPC.

Anyway, let’s hope for the best and start by checking the Stack Trace once again but, this time, using an instance of Procmon configured to use the public symbols provided by Microsoft. To do so, I switched to the Windows 10 VM I use for security research.

We can see that the CLanConnection::GetProperties() method is called here. In other events, the CLanConnection::GetPropertiesEx() method is called instead. Let’s see if we can find these methods by inspecting the COM objects exposed by NetMan using OleViewDotNet.

Simply based on the name of the class, the LAN Connection Class seems like a good candidate. So, I created an instance of this class and checked the details of the INetConnection interface.

Here it is! We can see the CLanConnection::GetProperties() method. We’re getting close! :ok_hand:

At this point, I was thinking that all of this looked too good to be true. First, I saw this DLL hijacking which I had never seen before. Then, I saw that it was triggered by an RPC/COM event. Finally, finding it with OleViewDotNet was trivial. There had to be a catch! Though, only one problem could arise now: restrictive permissions on the COM object.

COM objects are securable too and they have ACLs which define who is allowed to use them. So, we need to check this before going any further.

When I first saw Administrators and NT AUTHORITY\..., I thought for a second, “crap, this can only be triggered by high-privileged accounts”. And then I saw NT AUTHORITY\INTERACTIVE, phew… :sweat_smile:

What this actually means is that this COM object can be used by normal users only if they are authenticated using an interactive session. More specifically, you’d need to logon locally on the server. Not very useful, right?! Well, it turns out that when you connect through RDP (this includes VDI), you get an interactive session as well so, under these circumstances, this COM object could be used by a normal user. Otherwise, if you tried to use it in a WinRM session for example, you’d get an “Access denied” error. That’s not as good as I expected initially but that’s still a seemingly interesting trigger.

The below screenshot shows a command prompt opened in an RDP session on Windows Server 2019.

At this point, the research part is over so let’s write some code! Fortunately, the INetConnection interface is documented (here). This makes things a lot easier. Secondly, while searching how to enumerate the network interfaces with INetConnection->EnumConnections(), I stumbled upon an interesting solution posted by Simon Mourier on StackOverflow here. Yes, I copied some code from StackOverflow, that’s a bit lame, I know… :neutral_face:

Here is my final Proof-of-Concept code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// https://stackoverflow.com/questions/5917304/how-do-i-detect-a-disabled-network-interface-connection-from-a-windows-applicati/5942359#5942359

#include <iostream>
#include <comdef.h>
#include <netcon.h>

int main()
{
    HRESULT hResult;

    typedef void(__stdcall* LPNcFreeNetconProperties)(NETCON_PROPERTIES* pProps);
    HMODULE hModule = LoadLibrary(L"netshell.dll");
    if (hModule == NULL) { return 1; }
    LPNcFreeNetconProperties NcFreeNetconProperties = (LPNcFreeNetconProperties)GetProcAddress(hModule, "NcFreeNetconProperties");

    hResult = CoInitializeEx(0, COINIT_MULTITHREADED);
    if (SUCCEEDED(hResult))
    {
        INetConnectionManager* pConnectionManager = 0;
        hResult = CoCreateInstance(CLSID_ConnectionManager, 0, CLSCTX_ALL, __uuidof(INetConnectionManager), (void**)&pConnectionManager);
        if (SUCCEEDED(hResult))
        {
            IEnumNetConnection* pEnumConnection = 0;
            hResult = pConnectionManager->EnumConnections(NCME_DEFAULT, &pEnumConnection);
            if (SUCCEEDED(hResult))
            {
                INetConnection* pConnection = 0;
                ULONG count;
                while (pEnumConnection->Next(1, &pConnection, &count) == S_OK)
                {
                    NETCON_PROPERTIES* pConnectionProperties = 0;
                    hResult = pConnection->GetProperties(&pConnectionProperties);
                    if (SUCCEEDED(hResult))
                    {
                        wprintf(L"Interface: %ls\n", pConnectionProperties->pszwName);
                        NcFreeNetconProperties(pConnectionProperties);
                    }
                    else
                        wprintf(L"[-] INetConnection::GetProperties() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
                    pConnection->Release();
                }
                pEnumConnection->Release();
            }
            else
                wprintf(L"[-] IEnumNetConnection::EnumConnections() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
            pConnectionManager->Release();
        }
        else
            wprintf(L"[-] CoCreateInstance() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
        CoUninitialize();
    }
    else
        wprintf(L"[-] CoInitializeEx() failed. Error code = 0x%08X (%ls)\n", hResult, _com_error(hResult).ErrorMessage());
    
    FreeLibrary(hModule);
    wprintf(L"Done\n");
}

The below screenshot shows the final result on Windows Server 2008 R2. As we can see, we can trigger the DLL loading simply by enumerating the Ethernet interfaces of the machine. No need to say that the machine must have at least one Ethernet interface, otherwise this technique doesn’t work. :smile:

The screenshot below shows an attempt to run the same executable as a normal user connected through a remote PowerShell session on Windows Server 2019.

(2020-04-13 update) Dealing with the INTERACTIVE restriction

A couple days after the publication of this blog post, @splinter_code brought to my attention that it was technically possible to spawn an interactive process from a non-interactive one.

Then, I had the chance to exchange a few words with him. It turns out that he developped a tool called RunasCs which implements among other things a generic way for spawning an interactive process. He also took the time to explain to me how it works. This trick involves some Windows internals subtleties which are not commonly well known. I won’t detail the technique here because it would require a dedicated blog post in order to explain everything clearly but I’ll try to give a high-level explanation. I hope we will see a blog post from the author himself soon! :slightly_smiling_face:

To put it simple, you can call CreateProcessWithLogon() in order to create an interactive process. This function requires the name and the password of the target user. The problem is that if you try to do that from a process running in session 0 (where most of the services live), the child process will immediately die. A typical example is when you connect remotely through WinRM. All your commands are executed through a subprocess running in session 0 with your identity.

Why is it a problem? You may ask. The thing is, an interactive process is called this way because it interacts with a desktop, which is a particular securable object in the Windows world. However, in the case of our WinRM process which runs in session 0, you wouldn’t (and you shouldn’t) be allowed to interact with this desktop. What @splinter_code found is that you can edit the ACL of the desktop object in the context of the current process in order to grant the current user access to this object. Child processes will then inherit these permissions and therefore have a desktop to interact with. Really clever!

As you can see on the below screenshot, using this trick, we can spawn an interactive process and therefore run NetManTrigger.exe as if we were logged in locally. :slightly_smiling_face:

Conclusion

Following this analysis, I can say that the NetMan service is probably the most useful target for DLL Hijacking I know about. It comes with a small caveat though. As a normal user you would need an interactive session (RDP / VDI), which makes it quite useless if you’re logged on through a remote PowerShell session for instance. But there is another interesting case, if you’ve compromised another service running as LOCAL SERVICE or NETWORK SERVICE, then you would still be able to trigger the NetMan service to elevate your privileges to SYSTEM.

With this discovery, I also learned a lesson. Focusing your attention and your research on a particular environment may sometimes prevent you from finding interesting stuff, which turns out to be particularly relevant in the context of a pentest.

Last but not least, I integrated this in my Windows Privilege Escalation Check script - PrivescCheck. Depending on the version of Windows, the Invoke-HijackableDllsCheck function will tell you which DLL may potentially be hijacked through the %PATH% directories. Thanks @1mm0rt41 for suggesting the idea! :thumbsup:

This post is licensed under CC BY 4.0 by the author.