This post has been written by me and two friends: @splinter_code and 0xea31

This is the “unintended” result of a research we did on Juicypotato exploit in order to find a possible bypass on restrictions MS applied in latest Windows versions.

We all know that, up to Windows 2016 and Windows 10 1803, it’s possible for a Windows “Service User” or any user with Impersonation Privileges to escalate privileges to SYSTEM, by abusing the NTLM local authentication via reflection during the initialization of “fake” COM Storage Objects. (here and here the detailed explanation of this exploit).

Starting from Windows Server 2019 and Windows 10 1809, MS finally fixed this exploit (but still works on previous versions).

Nevertheless, we decided to do some further research in order to understand if any bypass of the new OXID resolver restrictions, which in fact inhibits resolver requests over a port different to 135, is still possible.

After some brainstorming (we firmly believe that 1+1+1=4) we found a solution by doing some redirection and implementing our own “Oxid Resolver” . The good news is that yes, we got a SYSTEM token! The bad one: it was only an identification token, really useless (more on this in a future post). But, you know, sometimes you are looking for something and you simply run into something you did not expected.

We were back to network layer when some packets in the capture on the loop-back interface got our attention:

While instantiating the BITS object on our fully patched Windows 10 1909 machine, a connection attempt on WinRM port (5985) popped (and pops) out, and, as far as we knew, in Windows 10, Remote Management is disabled by default.

Weird, isn’t it?

After some more investigation, we realized that this strange behavior happens every time the BITS service is started. And since BITS service will stop automatically after 2 minutes of inactivity, this can be triggered, almost, at will.

But, what’s is going on when BITS shouts to nowhere? We set up a netcat listener on port 5985 and issue a simple “bitsadmin /list” from a command prompt to have the answer:

C:\temp>netcat -lnvp 5985 listening on [any] 5985 ... connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 49690 POST /wsman HTTP/1.1 Connection: Keep-Alive Content-Type: application/soap+xml;charset=UTF-16 Authorization: Negotiate YGwGBisGAQUFAqBiMGCgGjAYBgorBgEEAYI3AgIKBgorBgEEAYI3AgIeokIEQE5UTE1TU1AAAQAAALeyCOIJAAkANwAAAA8ADwAoAAAACgBjRQAAAA9XSU4tR0UwTDEwMjBVSlFXT1JLR1JPVVA= User-Agent: Microsoft WinRM Client Content-Length: 0 Host: localhost:5985

This was interesting! We got an NTLM negotiate message from… who? An unknown user? And what did the the base64 encoded NTLM message contain?

We decoded the message and the SPNEGO header was followed by an NTLM type 1 message indicating that a Local Authentication would take place (COMPUTERNAME/DOMAINNAME were present):

It looked very promising, so we enabled WinRM and captured the whole negotiation:

In frame 10 we could see the details of the authentication:

The type 3 message was indicating that the “local authentication” was successful, granting the “Local System” Account access! Why Local System? The best guess was because BITS service was running under the SYSTEM account.

So we put together an alternative plan: implement a fake WinRM on 5985 and kindly ask BITS to start.

What we had to get was as simple as a normal NTLMHttpAuthentication handshaking:

Negotiate (Type 1) -> Challenge (Type 2) -> Authenticate (Type 3)

Now things are going to be a little bit more technical, for “TL;DR” fans just keep in mind that we implemented an NTLM local authentication server which would return us the user’s token (SYSTEM in this case) .

The client sent us a Negotiate Type 1 base64 encoded message in the “Authorization” header. We extracted the NTLMSSP packet, SPNEGO headers and called the InitTokenContextBuffer() in order to generate a server side context (Challenge Response) using the AcceptSecurityContext() API call.

Next we took this server context, which represented our Challenge Type 2 message, added the SPNEGO headers, base64 encoded all the packet and sent it through a “401 Unauthorized” HTTP response in the WWW-Authenticate header.

We expected an Authorization Type 3 message from the client (BITS) demonstrating that the authentication flow was working. This was the result:

The client was authenticating to our fake WinRM listener! We decoded the NTLMSSP part of the Authorization Type 3 message and sent it again to our AcceptSecurityContext() call.

This last step allowed us to create a Security Context in our process represented by a CtxtHandle. Once we had the handle, a call to QuerySecurityContextToken() would hopefully return the token of the user authenticating to us (SYSTEM) residing in our process memory.

It worked, so we got the token. In order to confirm that this token was from the local system account currently running BITS service, we reused the “JuicyPotato” function to check the token user:

[+] authresult 0

NT AUTHORITY\SYSTEM

Perfect! Last step was the creation of a process impersonating this token using the CreateProcessWithTokenW() and CreateProcessAsUser() API calls and the magic happened:

Great! We got an impersonation SYSTEM token that we were able to use!

After some further testing we discovered that this trick only worked for BITS service. Knowing that we used an easier way to trigger the BITS service without using the Unmarshalling of a fake IStorageInterface:

bool triggerBits(void) { bool status=false; HRESULT result = -1; CLSID clsid; IUnknown* unknown1 = NULL; CoInitialize(nullptr); CLSIDFromString(OLESTR("{4991d34b-80a1-4291-83b6-3328366b9097}"), &clsid); result = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IUnknown, (void**)&unknown1); if (result == S_OK) { status = true; unknown1->Release(); } else { printf("CoCreateInstance failed with error 0x%x

", result); status = false; } CoUninitialize(); return status; }

We used the CoCreateInstance() function asking to instantiate a BITS object (using BITS clsid) in order to trigger the service (and by consequence the auth on port 5985). Note that the Release() of the object is a crucial part in order to decrease the references counter of the BITS allocated objects and let it time out after 2 minutes.

Side Note: Some days after our findings, James Forshaw posted this article where he also observed the same strange behavior: “As if a metaphor for our existence I observed the BITS service shouting into void, desperately trying to ask a question of the WinRM service that will never be answered.” Further investigation on this may worth a try: our best guess is that BITS keeps asking via WinRM the status of pending jobs etc.., maybe a detailed tracing of WinRM activity could solve the mystery.

Boundary conditions



This EoP works only if WinRM is disabled. This is the default on Windows 10 but NOT on Windows Servers. Impersonation privilege is needed (typically service users hold it) BITS must not be already running. If a background transfer job is in progress, this could take a long time for waiting it to finish (imagine a Windows Update…) As far as we know, this EoP works on all version Windows 10 starting from 1809. We tested it on Windows Server 2019 and Windows Server 2016, also. It works only if WinRM is stopped (which is not the default status). Bits normally shouts to port 5985, but we have noticed that on some versions it shouts to port 47001 (WinRM service with no listener configured)



We have released RogueWinRM that “exploits” this vulnerability in order to escalate privileges from a Service Account to Local System. You can find the source code here.

Disclosure Timeline:

01/11/2019 – We reported this issue to MS

15/11/2019 – MS said that they were going to close the case because it required Administrator privileges.

15/11/2019 – We provided a video showing the EoP from a local service account (RCE on a IIS webserver scenario)

22/11/2019 – MS answered “game over”, stating that elevating from a Local Service process (with SeImpersonate) to SYSTEM is an “expected behavior”, referring to this MS public page

23/11/2019 – We sent a detailed argumentation about the difference of a “by design” feature vs “attack path”.

27/11/2019 – pinged MS to have an answer if they closed the case or not.

02/12/2019 – pinged MS to have an answer if they closed the case or not, saying that in case of no answer before 03/12/2019, close of business, we would publish a blog post on this issue.

04/12/2019 – The case manager asked us if it was possible to postpone the release of our blog post because he did not forward our Nov 23rd email to the engineering team. We replied that we would have delayed the public release until Friday 6th December, 21:00 CET (12:00 PST), waiting for acknowledgment that the case is still open.

06/12/2019 (18:30 CET – 09:30 PST) – Pinged MS for “last call” for acknowledgment that the case is still open and if they are going to fix it or not.

06/12/2019 (21:00 CET – 12:00 PST) – Still no answer from MS. 30 and more days since our notification without a proper acknowledgment. Publishing this issue.

Possible countermeasures:

Given that MS won’t fix this vulnerability in a near future, firewalling TCP port 5985 on systems where WinRM is not running could be an option.

Decoder’s note: Thumbs up to @splinter_code who has written most of the exploit code

Enjoy 🙂