MS Remote Desktop Gateway, HAProxy and password guessing

Hi friends!

There are many ways to connect from home to a workplace in an office. One of them is to use Microsoft Remote Desktop Gateway. This is RDP over HTTP. I don’t want to touch upon the configuration of RDGW itself, I don’t want to discuss why it is good or bad, let's treat it as one of the remote access tools. I want to talk about protecting your RDGW server from the evil Internet. When I set up the RDGW server, I immediately became preoccupied with protection, especially password protection. I was surprised that I did not find articles on the Internet on how to do this. Well, you have to do it yourself.

RDGW itself does not have any protections. Yes, it is possible to expose a bare rail interface in a white net and will work just fine. But the right administrator or IB'shnik will be restless from this. In addition, it will allow avoiding the situation of blocking an account when a negligent employee remembered the password of the corporate account on the home computer, and then changed his password.

A good way to protect internal resources from the external environment is through various proxies, publishing systems, and other WAFs. Recall that RDGW is all the same http, then it directly begs to stick a specialized solution between internal servers and the Internet.

I know there are cool F5, A10, Netscaler (ADC). As the administrator of one of these systems, I’ll say that it is also possible to set up protection against busting on these systems. And yes, these systems will protect you from any syn flood along the way.

But not every company can afford to buy such a solution (and find the administrator of such a system :), but it can take care of security!

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

Setting up debian itself is beyond the scope of this article. Briefly: on the white interface, close everything except 443 ports, on the gray interface - according to your policy, for example, close everything except 22 ports. Open only what is necessary for work (VRRP for example, for floating ip).

First of all, I configured haproxy to SSL bridging mode (aka mode http) and turned on logging to see what goes inside RDP. So to speak, climbed in the middle. So, the / RDWeb path specified in "all" articles on configuring RDGateway is missing. All that is there is /rpc/rpcproxy.dll and / remoteDesktopGateway /. In this case, standard GET / POST requests are not used, its own request type is RDG_IN_DATA, RDG_OUT_DATA.

Not much, but at least something.

Let's test.

I start mstsc, go to the server, I see four 401 errors (unauthorized) in the logs, then I enter the login / password and I see the answer 200. I

turn off, I start again, I see the same four 401 errors in the logs. I enter the wrong username / password and I see four again errors 401. What you need. This is what we will catch.

Since it was not possible to determine the login url, and besides, I don’t know how to catch the 401 error in haproxy, I will catch (not actually catch, but count) all 4xx errors. Also happy to solve the problem.

The essence of protection will be that we will count the number of 4xx errors (on the backend) per unit of time and if it exceeds the specified limit, then reject (on the frontend) all further connections from this ip for the specified time.

Technically, this will not be password protection, it will be 4xx error protection. For example, if you frequently request a non-existent url (404), then protection will also work.

The easiest and most working way is to count and beat off on the backend, if something superfluous has appeared:

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
    ...

    # , , 1000 ,   15 ,  -    10 
    stick-table type string len 128 size 1k expire 15s store http_err_rate(10s)
    # ip
    http-request track-sc0 src
    #  http  429,    10   4 
    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 a good option, complicate. We will count on the backend, and block it on the frontend.

We’ll act rudely with the attacker, we will drop the tcp connection to him.

frontend fe_rdp_tsc
    bind *:443 ssl crt /etc/haproxy/cert/ertelecom_ru_2020_06_11.pem
    mode http
    ...
    #  ip , 1000 ,   15 ,    
    stick-table type ip size 1k expire 15s store gpc0
    # 
    tcp-request connection track-sc0 src
    # tcp ,    >0
    tcp-request connection reject if { sc0_get_gpc0 gt 0 }
	
    ...
    default_backend be_rdp_tsc


backend be_rdp_tsc
    ...
    mode http
    ...
	
    #  ip , 1000 ,   15 ,  -   10 
    stick-table type ip size 1k expire 15s store http_err_rate(10s)
    # ,  -   10   8
    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
	
    ...
    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 the error http 429 (Too Many Requests)

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
    ...

Check: run mstsc and start randomly entering passwords. After the third attempt, it kicks me in 10 seconds, and mstsc gives an error. As can be seen in the logs.

Explanations I am far from a haproxy master. I do not understand why, for example,
http-request deny deny_status 429 if {sc_http_err_rate (0) gt 4}
allows you to make about 10 errors before it works.

I am confused in the numbering of the counters. Haproxy masters, I will be glad if you supplement me, correct me, do better.

In the comments, you can throw other ways to protect RD Gateway, it will 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; does not support current cipher, so I also had to leave the old ones.

For those who do not understand anything, just learning, and already want to do well, I will give the whole 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 two servers on the backend? Because this is how fault tolerance can be done. Haproxy can also do two with floating white ip.

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

Links:

Configuring rdp-gateway from HAProxy The only article I found where I was bothered with password cracking

All Articles