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 of april 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:
[sourcecode language=”text”]
log=admin&pwd=admin&wp-submit=Log+In&redirect_to=http%3A%2F%www.domain.tld%2Fwp-admin%2F&testcookie=1
[/sourcecode]
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.
[sourcecode language=”text”]
[…]
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
[…]
[/sourcecode]

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:
[sourcecode language=”text”]
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
[…]
[/sourcecode]

  * sandbox backend configuration:
[sourcecode language=”text”]
[…]
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
[…]
[/sourcecode]

  * 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:
[sourcecode language=”text”]
[…]
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"
[…]
[/sourcecode]
5 attempts before being redirected to the sendbox, and still attempting 😉

Let’s have a look at the stick table:
[sourcecode language=”text”]
# 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
[/sourcecode]

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

Appendice


  * 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
[sourcecode language=”text”]
[…]
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 &rsaquo; 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?">&larr; Back to Bedis</a></p>

</div>

<div class="clear"></div>
</body>
</html>

[…]
[/sourcecode]