MS Remote Desktop Gateway, HAProxy, and password guessing

Hello, friends!

There are many ways to connect from home to your workplace in the office. One of them is to use Microsoft Remote Desktop Gateway. This is RDP over HTTP. I don't want to discuss the configuration of RDGW here, nor do I want to debate whether it is good or bad; let's treat it as one of the remote access tools. I want to talk about securing your RDGW server from the evil internet. When I set up the RDGW server, I immediately focused on security, especially protection against password guessing. I was surprised that I couldn't find any articles on the internet about how to do this. Well, I guess I'll have to figure it out myself.

RDGW itself does not have any protections. Yes, you can leave it exposed with a bare interface on the public network, and it will work perfectly. But a proper administrator or security professional would be uneasy about it. Moreover, this will prevent the situation of account blocking, when an employee forgets the corporate account password on their home computer and then changes their password.

A good way to protect internal resources from the external environment is through various proxies, publication systems, and other WAFs. Remember that RDGW is still HTTP, so it makes sense to insert a specialized solution between internal servers and the internet.

I know there are great solutions like F5, A10, Netscaler (ADC). As an administrator of one of these systems, I can say that it's also possible to set up protection against password guessing on these systems. And yes, these systems will also protect you from various SYN floods.

However, not every company can afford to purchase such a solution (and find an administrator for the system :), but security can be taken care of!

It is entirely possible to install the free version of HAProxy on a free operating system. I tested it on Debian 10, and in the stable repository, the version of haproxy is 1.8.19. I also tested version 2.0.xx from the testing repository.

We will leave the setup of Debian beyond the scope of this article. In short: on the public interface, close everything except port 443; on the gray interface, do so according to your policy, for example, close everything except port 22. Open only what is necessary for operation (such as VRRP for floating IP).

First, I configured HAProxy in SSL bridging mode (also known as HTTP mode) and enabled logging to see what traffic is going through RDP. So to speak, I positioned myself in the middle. However, the path /RDWeb mentioned in 'all' the RDGateway configuration articles is missing. All that's available is /rpc/rpcproxy.dll and /remoteDesktopGateway/. Furthermore, standard GET/POST requests are not used; instead, a custom request type RDG_IN_DATA, RDG_OUT_DATA is utilized.

Not much, but at least something.

Let's start testing.

I launch mstsc, navigate to the server, and see four 401 (unauthorized) errors in the logs. Then I enter my login/password and see a 200 response.

I disconnect, restart, and see the same four 401 errors in the logs. I enter incorrect login/password and see four 401 errors again. That's what we need. We will track this.

Since I couldn't determine the login URL, and I also don't know how to catch the 401 error in HAProxy, I will track (actually count) all 4xx errors. That will also suffice for solving the task.

The purpose of the protection will be to count the number of 4xx errors (on the backend) in a given time frame, and if it exceeds the specified limit, deny (on the frontend) all further connections from that IP for a specified duration.

Technically, this will not protect against password brute-forcing; it will protect against 4xx errors. For example, if a non-existent URL (404) is frequently requested, the protection will also trigger.

The simplest and most effective way is to count and block on the backend if anything excessive appears:

frontend fe_rdp_tsc
    bind *:443 ssl crt /etc/haproxy/cert/desktop.example.com.pem
    mode http
    ...
    default_backend be_rdp_tsc


backend be_rdp_tsc
    ...
    mode http
    ...

    # create a string table with 1000 entries, expires after 15 seconds, record the error count for the last 10 seconds
    stick-table type string len 128 size 1k expire 15s store http_err_rate(10s)
    # remember IP
    http-request track-sc0 src
    # deny with HTTP error 429 if more than 4 errors in the last 10 seconds
    http-request deny deny_status 429 if { sc_http_err_rate(0) gt 4 }
	
	...
    server rdgw01 192.168.1.33:443 maxconn 1000 weight 10 ssl check cookie rdgw01
    server rdgw02 192.168.2.33:443 maxconn 1000 weight 10 ssl check cookie rdgw02

Not the best option; let's complicate it. We will count on the backend and block on the frontend.

We will take a rough approach with the attacker by dropping their TCP connection.

frontend fe_rdp_tsc
    bind *:443 ssl crt /etc/haproxy/cert/ertelecom_ru_2020_06_11.pem
    mode http
    ...
    #create a table of IP addresses, 1000 entries, expires after 15 seconds, to store from global counter
    stick-table type ip size 1k expire 15s store gpc0
    #take the source
    tcp-request connection track-sc0 src
    #reject the TCP connection if the global counter >0
    tcp-request connection reject if { sc0_get_gpc0 gt 0 }
	
    ...
    default_backend be_rdp_tsc


backend be_rdp_tsc
    ...
    mode http
    ...
	
    #create a table of IP addresses, 1000 entries, expires after 15 seconds, to store error count for 10 seconds
    stick-table type ip size 1k expire 15s store http_err_rate(10s)
    #many errors if the number of errors for 10 seconds exceeds 8
    acl errors_too_fast sc1_http_err_rate gt 8
    #mark the attack in the global counter (increment the counter)
    acl mark_as_abuser sc0_inc_gpc0(fe_rdp_tsc) gt 0
    #reset the global counter
    acl clear_as_abuser sc0_clr_gpc0(fe_rdp_tsc) ge 0
    #take the source
    tcp-request content track-sc1 src
    #reject, mark as an attack
    tcp-request content reject if errors_too_fast mark_as_abuser
    #allow, reset attack flag
    tcp-request content accept if !errors_too_fast clear_as_abuser
	
    ...
    server rdgw01 192.168.1.33:443 maxconn 1000 weight 10 ssl check cookie rdgw01
    server rdgw02 192.168.2.33:443 maxconn 1000 weight 10 ssl check cookie rdgw02

the same thing, but politely, we will return an HTTP 429 (Too Many Requests) error

frontend fe_rdp_tsc
    ...
    stick-table type ip size 1k expire 15s store gpc0
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc0_get_gpc0 gt 0 }
    ...
    default_backend be_rdp_tsc

backend be_rdp_tsc
    ...
    stick-table type ip size 1k expire 15s store http_err_rate(10s)
    acl errors_too_fast sc1_http_err_rate gt 8
    acl mark_as_abuser sc0_inc_gpc0(fe_rdp_tsc) gt 0
    acl clear_as_abuser sc0_clr_gpc0(fe_rdp_tsc) ge 0
    http-request track-sc1 src
    http-request allow if !errors_too_fast clear_as_abuser
    http-request deny deny_status 429 if errors_too_fast mark_as_abuser
    ...

I check: I run mstsc and start randomly entering passwords. After the third attempt within 10 seconds, I'm kicked out, and mstsc reports an error. This is also visible in the logs.

Explanations. I am by no means an expert on haproxy. I do not understand, for example
http-request deny deny_status 429 if { sc_http_err_rate(0) gt 4 }
allows for about 10 errors before it triggers.

I am confused about the counter numbering. Haproxy experts, I would be glad if you could add, correct, or improve it.

In the comments, feel free to suggest other ways to protect the RD Gateway, it would be interesting to study.

Regarding the Windows Remote Desktop client (mstsc), it is worth noting that it does not support TLS1.2 (at least in Windows 7), so I had to leave TLS1; it does not support current ciphers, so I also had to leave the old ones.

For those who are just learning and want to do things well, I will provide the entire config.

haproxy.conf

global
        log /dev/log    local0
        log /dev/log    local1 notice
        chroot /var/lib/haproxy
        stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
        stats timeout 30s
        user haproxy
        group haproxy
        daemon

        # Default SSL material locations
        ca-base /etc/ssl/certs
        crt-base /etc/ssl/private

        # See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
        #ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
        ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
        ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
        #ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
        ssl-default-bind-options no-sslv3
        ssl-server-verify none


defaults
        log     global
        mode    http
        option  httplog
        option  dontlognull
        timeout connect 5000
        timeout client  15m
        timeout server  15m
        errorfile 400 /etc/haproxy/errors/400.http
        errorfile 403 /etc/haproxy/errors/403.http
        errorfile 408 /etc/haproxy/errors/408.http
        errorfile 500 /etc/haproxy/errors/500.http
        errorfile 502 /etc/haproxy/errors/502.http
        errorfile 503 /etc/haproxy/errors/503.http
        errorfile 504 /etc/haproxy/errors/504.http


frontend fe_rdp_tsc
    bind *:443 ssl crt /etc/haproxy/cert/dektop.example.com.pem
    mode http
    capture request header Host len 32
    log global
    option httplog
    timeout client 300s
    maxconn 1000

    stick-table type ip size 1k expire 15s store gpc0
    tcp-request connection track-sc0 src
    tcp-request connection reject if { sc0_get_gpc0 gt 0 }

    acl rdweb_domain hdr(host) -i beg dektop.example.com
    http-request deny deny_status 400 if !rdweb_domain
    default_backend be_rdp_tsc


backend be_rdp_tsc
    balance source
    mode http
    log global

    stick-table type ip size 1k expire 15s store http_err_rate(10s)
    acl errors_too_fast sc1_http_err_rate gt 8
    acl mark_as_abuser sc0_inc_gpc0(fe_rdp_tsc) gt 0
    acl clear_as_abuser sc0_clr_gpc0(fe_rdp_tsc) ge 0
    tcp-request content track-sc1 src
    tcp-request content reject if errors_too_fast mark_as_abuser
    tcp-request content accept if !errors_too_fast clear_as_abuser

    option forwardfor
    http-request add-header X-CLIENT-IP %[src]

    option httpchk GET /
    cookie RDPWEB insert nocache
    default-server inter 3s    rise 2  fall 3
    server rdgw01 192.168.1.33:443 maxconn 1000 weight 10 ssl check cookie rdgw01
    server rdgw02 192.168.2.33:443 maxconn 1000 weight 10 ssl check cookie rdgw02


frontend fe_stats
    mode http
    bind *:8080
    acl ip_allow_admin src 192.168.66.66
    stats enable
    stats uri /stats
    stats refresh 30s
    #stats admin if LOCALHOST
    stats admin if ip_allow_admin

Why have two servers in the backend? Because it allows for fault tolerance. You can also set up HAProxy with two floating public IPs.

Computing resources: you can start with 'two gigs, two cores, a gaming PC.' According to Wikipedia this will be more than enough.

Links:

RDP gateway configuration via HAProxy
The only article I found that addresses password cracking

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster