Brute force attacks
Brute force is a pretty simple type of attack: it consists of massively send requests to a URL with different parameters 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 is easy to detect, and the latest HAProxy version has 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-April 2013.
Note that the ALOHA Load-Balancer firmware 5.5 and above also includes the features introduced here.
WordPress login
WordPress provides 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 a POST request on /wp-login.php and try many different form data combinations.
Below, is 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 this information to know the maximum request rate he can achieve without being blacklisted.
The current article proposes to send abusers into a sandbox that keeps on delivering a static version of the login form, letting the abuser try to hack your site, but actually hacking a static page 🙂
Furthermore, HAProxy will also slow down the abusers request rate by tar pitting the request during 1s.
Brute force protection with HAProxy / ALOHA Load-Balancer
The configuration is split into 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 requests 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 the 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.
This 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 decreases, 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 www.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> [...]