MS Remote Desktop Gateway, HAProxy and password brute force

Friends, hello!

There are many ways to connect from home to the workplace in the office. One of them is to use the Microsoft Remote Desktop Gateway. It's RDP over HTTP. I do not want to touch on the configuration of the RDGW itself here, I do not want to argue 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 servers, I immediately became concerned about security, especially password protection. I was surprised that I did not find articles on the Internet on how to do this. Well, you'll have to do it yourself.

By itself, RDGW has no protections. Yes, you can put it with a bare interface in a white network and it will work fine. But the right administrator or IB'shnik will be restless from this. In addition, it will allow you to avoid the situation of account blocking, when a negligent employee remembers the corporate account password on his home computer, and then changes 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 still http, then it just begs to stick a specialized solution between internal servers and the Internet.

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

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

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

We will leave the configuration of debian itself outside the article. Briefly: on the white interface, close everything except port 443, on the gray interface - according to your policy, for example, also close everything except port 22. Open only what is necessary for work (VRRP for example, for a floating ip).

First of all, I configured haproxy to SSL bridging mode (aka http mode) and enabled logging to see what goes inside RDP. So to speak, got in the middle. So, the /RDWeb path specified in "all" articles on configuring RDGateway is missing. All there is /rpc/rpcproxy.dll and /remoteDesktopGateway/. This does not use standard GET / POST requests, it uses its own type of request RDG_IN_DATA, RDG_OUT_DATA.

Not much, but at least something.

Let's test.

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

I turn it off, restart it, I see the same four 401 errors in the logs. I enter the wrong username / password and see four 401 errors again. That's what I need. This is what we will capture.

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 (actually not catch, but count) all 4xx errors. Also suitable for solving 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 within the specified time.

Technically, it won't be brute force protection, it will be 4xx error protection. For example, if you frequently request a non-existent url (404), then the protection will also work.

The easiest and most effective way is to count and beat back on the backend, if something extra 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 the best option, complicate. We will count on the backend, and block on the frontend.

We will act rudely with the attacker, we will throw off a 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, but politely, we will return an 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
    ...

I check: I run mstsc and start typing passwords randomly. After the third attempt, it kicks me in 10 seconds, and mstsc gives an error. As you can see in the logs.

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

I'm confused about the numbering of counters. Haproxy masters, I will be glad if you add me, correct me, make it better.

In the comments, you can throw in other ways to protect the 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 ciphers, 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 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 two servers on the backend? Because this is how you can make fault tolerance. Haproxy can also make 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:

Setting up rdp-gateway from HAProxy
The only article I found where they bothered to brute force the password

Source: habr.com

Add a comment