Brute force attacks
Brute force is a pretty simple type of attacks: it consists of massively send requests to a URL with different parameter each time. The main purpose is to try to find the right parameter combination.
Usually, brute force is used to discover login/password credentials to enter into a web application.
Fortunately, brute force are easy to detect, and latest HAProxy version have everything they need to protect any web application web form from brute forcing.
In the current article, we’ll apply the configuration on a WordPress CMS (Content Management System), which was brute forced around mid ofAapril 2013.
Note that the ALOHA Load-Balancer firmware 5.5 and above also includes the features introduced here.
WordPress login
WordPress provide the login form through the URL: /wp-login.php. Of course, it’s a regular GET.
The user just fill up the form, then the browser sends a POST on the URL /wp-login.php with form data in the body.
So basically, a brute force attacker will forge POST request on /wp-login.php and trying many different form data combination.
Below, an example of a forged request to try the credential admin/admin:
log=admin&pwd=admin&wp-submit=Log+In&redirect_to=http%3A%2F%www.domain.tld%2Fwp-admin%2F&testcookie=1
We can clearly see the log and pwd fields.
Blocking a brute force? Better sandboxing it smartly!
It would be easy with HAProxy to drop the TCP connection, or to answer HTTP deny (403) status codes when we see somebody is abusing.
Actually, the attacker could use these information to know the maximum request rate he can achieve without being blacklisted.
The current article proposes to send abusers into a sandbox which keeps on delivering a static version of the login form, letting the abuser trying to hack your site, but actually hacking a static page 🙂
Furthermore, HAProxy will also slowdown the abusers request rate by tarpitting the request during 1s.
Brute force protection with HAProxy / ALOHA Load-Balancer
The configration is split in 2 parts:
1. in the frontend, we store the list of blocked users
2. in the backend, we do the brute force detection and we notify the frontend when an abuser is detected
Configuration for Brute force detection in the backend
This configuration stores a hash of 3 elements: HTTP Host header, URL path and source IP.
We’ll enable tracking only when the requests occur on the wordpress login URL (/wp-login.php) and if the method is POST.
Based on this, we can track the number of HTTP request the source IP did over a period of 20s and we can decide if the source IP did more than 5 login tentative during this period of time, then we want to flag this user as an abuser.
Basically 5 tries are allowed per 20s, over this limit, then the 6th try will make the user blocked.
[...] tcp-request inspect-delay 10s tcp-request content accept if HTTP # brute force protection acl wp_login path_beg -i /wp-login.php stick-table type binary len 20 size 500 store http_req_rate(20s) peers local tcp-request content track-sc2 base32+src if METH_POST wp_login stick store-request base32+src if METH_POST wp_login acl bruteforce_detection sc2_http_req_rate gt 5 acl flag_bruteforce sc1_inc_gpc0 gt 0 http-request deny if bruteforce_detection flag_bruteforce [...]
Configuration for blocking abusers in the frontend
The configuration below detects that a user has abused the login page and then redirect him into a sandbox where HAProxy has been configured to serve a wordpress login page.
Which means the attacker will still think he is trying to brute force wordpress, but actually, he will brute force a static page !!!!! It will be impossible for him to know he has been sandboxed…
* Frontend configuration:
tcp-request inspect-delay 10s tcp-request accept if HTTP [...] acl wp_login path_beg -i /wp-login.php acl flagged_as_abuser sc1_get_gpc0 gt 0 stick-table type binary len 20 size 500 store gpc0 peers local tcp-request content track-sc1 base32+src if METH_POST wp_login use_backend bk_login_abusers if flagged_as_abuser [...]
* sandbox backend configuration:
[...] backend bk_login_abusers mode http log global option httplog timeout tarpit 1s http-request tarpit errorfile 500 /etc/haproxy/pages/wp_fake_login.http errorfile 503 /etc/haproxy/pages/wp_fake_login.http [...]
* Errorfile content example is provided at the bottom of this article, in the Apendice section
The protection in action
Below, an extract of HAProxy logs (anonymized) which show the blocking capacity of the configuration above:
[...] ft_www bk_wordpress/w1 "POST /wp-login.php HTTP/1.1" ft_www bk_wordpress/w1 "POST /wp-login.php HTTP/1.1" ft_www bk_wordpress/w1 "POST /wp-login.php HTTP/1.1" ft_www bk_wordpress/w1 "POST /wp-login.php HTTP/1.1" ft_www bk_wordpress/w1 "POST /wp-login.php HTTP/1.1" ft_www bk_login_abusers/<NOSRV> "POST /wp-login.php HTTP/1.1" ft_www bk_login_abusers/<NOSRV> "POST /wp-login.php HTTP/1.1" ft_www bk_login_abusers/<NOSRV> "POST /wp-login.php HTTP/1.1" [...]
5 attempts before being redirected to the sendbox, and still attempting 😉
Let’s have a look at the stick table:
# table: ft_www, type: binary, size:500, used:1 0x24f81e4: key=57FD750958BE12B3000000000000000000000000 use=0 exp=0 gpc0=1 # table: bk_wordpress, type: binary, size:500, used:1 0x24f8740: key=57FD750958BE12B3000000000000000000000000 use=0 exp=0 server_id=1 http_req_rate(20000)=6
Even if the http_req_rate decrease, as long as gpc0 is greater than 0 in the ft_www frontend stick-table, the user will be redirected to the sandbox.
Links
Appendix
* Errorfile content, which is the wordpress login page content:
Don’t forget to change the wwww.domain.tld by your own domain, and don’t forget to update the Content-Length header using the following script from our github: errorfile_content_length
[...] HTTP/1.0 200 OK Server: webserver Date: Fri, 26 Apr 2013 08:17:37 GMT Content-Type: text/html; charset=UTF-8 Expires: Wed, 11 Jan 1984 05:00:00 GMT Cache-Control: no-cache, must-revalidate, max-age=0 Pragma: no-cache Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/ X-Frame-Options: SAMEORIGIN Connection: close Content-Length: 3253 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en-US"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Bedis › Log In</title> <link rel='stylesheet' id='wp-admin-css' href='http://www.domain.tld/wp-admin/css/wp-admin.min.css?ver=3.5.1' type='text/css' media='all' /> <link rel='stylesheet' id='buttons-css' href='http://www.domain.tld/wp-includes/css/buttons.min.css?ver=3.5.1' type='text/css' media='all' /> <link rel='stylesheet' id='colors-fresh-css' href='http://www.domain.tld/wp-admin/css/colors-fresh.min.css?ver=3.5.1' type='text/css' media='all' /> <link rel="stylesheet" href="http://www.domain.tld/wp-content/themes/notes-blog-core-theme/custom/login.css" type="text/css" media="screen" /><meta name='robots' content='noindex,nofollow' /> <script type="text/javascript"> addLoadEvent = function(func){if(typeof jQuery!="undefined")jQuery(document).ready(func);else if(typeof wpOnload!='function'){wpOnload=func;}else{var oldonload=wpOnload;wpOnload=function(){oldonload();func();}}}; function s(id,pos){g(id).left=pos+'px';} function g(id){return document.getElementById(id).style;} function shake(id,a,d){c=a.shift();s(id,c);if(a.length>0){setTimeout(function(){shake(id,a,d);},d);}else{try{g(id).position='static';wp_attempt_focus();}catch(e){}}} addLoadEvent(function(){ var p=new Array(15,30,15,0,-15,-30,-15,0);p=p.concat(p.concat(p));var i=document.forms[0].id;g(i).position='relative';shake(i,p,20);}); </script> </head> <body class="login login-action-login wp-core-ui"> <div id="login"> <h1><a href="http://www.domain.tld/" title="Bedis Sites">Bedis</a></h1> <div id="login_error"> <strong>ERROR</strong>: Invalid username. <a href="http://www.domain.tld/wp-login.php?action=lostpassword" title="Password Lost and Found">Lost your password</a>?<br /> </div> <form name="loginform" id="loginform" action="http://www.domain.tld/wp-login.php" method="post"> <p> <label for="user_login">Username<br /> <input type="text" name="log" id="user_login" class="input" value="" size="20" /></label> </p> <p> <label for="user_pass">Password<br /> <input type="password" name="pwd" id="user_pass" class="input" value="" size="20" /></label> </p> <p class="forgetmenot"><label for="rememberme"><input name="rememberme" type="checkbox" id="rememberme" value="forever" /> Remember Me</label></p> <p class="submit"> <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="Log In" /> <input type="hidden" name="redirect_to" value="http://www.domain.tld/wp-admin/" /> <input type="hidden" name="testcookie" value="1" /> </p> </form> <p id="nav"> <a href="http://www.domain.tld/wp-login.php?action=lostpassword" title="Password Lost and Found">Lost your password?</a> </p> <script type="text/javascript"> function wp_attempt_focus(){ setTimeout( function(){ try{ d = document.getElementById('user_login'); if( d.value != '' ) d.value = ''; d.focus(); d.select(); } catch(e){} }, 200); } if(typeof wpOnload=='function')wpOnload(); </script> <p id="backtoblog"><a href="http://www.domain.tld/" title="Are you lost?">← Back to Bedis</a></p> </div> <div class="clear"></div> </body> </html> [...]
Hello,
Sorry for my english.
I have a problem with your code in backend
peers local don’t work :
ALERT] 045/175615 (26440) : Proxy ‘SSLOFFLOADING_BK’: unable to find sync peers ‘local’.
Do you have any idea for this problem ?
Thanks
Hi,
you may have a configuration typo in your stick-table definition which may point to a peer name which does not exist.
Baptiste
Hey Baptiste,
Are you able to explain the “Configuration for blocking abusers in the frontend” section in detail? I am not sure which line (if any) in this example specifies the 5req in 20sec rule?
Also – would url32+src be better for matching url-substring and ip-src?
Thanks!
Andrew
Hi Andrew,
The block action occurs in the frontend, but the tracking is done in the backend, with the following statement:
– 20s is the time you want to watch URLs in the stick table (http_req_rate):
stick-table type binary len 20 size 500 store http_req_rate(20s) peers local
– number of occurence to detect an abuser (over the period above):
acl bruteforce_detection sc2_http_req_rate gt 5
There are no url32+src for now in HAProxy.
Baptiste
Why does the detection need to take place in the back end? I have a number of wordpress servers, each with their own backend… and I think it would be wise to watch for abusers across all of the sites served.