Tag Archives: naxsi

wordpress CMS brute force protection with HAProxy

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:


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.



  * 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">
        <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);});
        <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 />

<form name="loginform" id="loginform" action="http://www.domain.tld/wp-login.php" method="post">
                <label for="user_login">Username<br />
                <input type="text" name="log" id="user_login" class="input" value="" size="20" /></label>
                <label for="user_pass">Password<br />
                <input type="password" name="pwd" id="user_pass" class="input" value="" size="20" /></label>
        <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 id="nav">
<a href="http://www.domain.tld/wp-login.php?action=lostpassword" title="Password Lost and Found">Lost your password?</a>

<script type="text/javascript">
function wp_attempt_focus(){
setTimeout( function(){ try{
d = document.getElementById('user_login');
if( d.value != '' )
d.value = '';
} catch(e){}
}, 200);

if(typeof wpOnload=='function')wpOnload();

        <p id="backtoblog"><a href="http://www.domain.tld/" title="Are you lost?">&larr; Back to Bedis</a></p>


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


high performance WAF platform with Naxsi and HAProxy


I’ve already described WAF in a previous article, where I spoke about WAF scalability with apache and modsecurity.
One of the main issue with Apache and modsecurity is the performance. To address this issue, an alternative exists: naxsi, a Web Application Firewall module for nginx.

So using Naxsi and HAProxy as a load-balancer, we’re able to build a platform which meets the following requirements:

  • Web Application Firewall: achieved by Apache and modsecurity
  • High-availability: application server and WAF monitoring, achieved by HAProxy
  • Scalability: ability to adapt capacity to the upcoming volume of traffic, achieved by HAProxy
  • DDOS protection: blind and brutal attacks protection, slowloris protection, achieved by HAProxy
  • Content-Switching: ability to route only dynamic requests to the WAF, achieved by HAProxy
  • Reliability: ability to detect capacity overusage, this is achieved by HAProxy
  • Performance: deliver response as fast as possible, achieved by the whole platform

The picture below provides a better overview:

The LAB platform is composed by 6 boxes:

  • 2 ALOHA Load-Balancers (could be replaced by HAProxy 1.5-dev)
  • 2 WAF servers: CentOS 6.0, nginx and Naxsi
  • 2 Web servers: Debian + apache + PHP + dokuwiki

Nginx and Naxsi installation on CentOS 6

Purpose of this article is not to provide such procedue. So please read this wiki article which summarizes how to install nginx and naxsi on CentOS 6.0.


The diagram below shows the platform with HAProxy frontends (prefixed by ft_) and backends (prefixed by bk_). Each farm is composed by 2 servers.


Nginx and Naxsi

Configure nginx as a reverse-proxy which listen in bk_waf and forward traffic to ft_web. In the mean time, naxsi is there to analyze the requests.

server {
 proxy_set_header Proxy-Connection "";
 access_log  /var/log/nginx/naxsi_access.log;
 error_log  /var/log/nginx/naxsi_error.log debug;

 location / {
  include    /etc/nginx/test.rules;

 error_page 403 /403.html;
 location = /403.html {
  root /opt/nginx/html;

 location /RequestDenied {
  return 403;

HAProxy Load-Balancer configuration

The configuration below allows the following advanced features:

  • DDOS protection on the frontend
  • abuser or attacker detection in bk_waf and blocking on the public interface (ft_waf)
  • Bypassing WAF when overusage or unavailable
######## Default values for all entries till next defaults section
  option  http-server-close
  option  dontlognull
  option  redispatch
  option  contstats
  retries 3
  timeout connect 5s
  timeout http-keep-alive 1s
  # Slowloris protection
  timeout http-request 15s
  timeout queue 30s
  timeout tarpit 1m          # tarpit hold tim
  backlog 10000

# public frontend where users get connected to
frontend ft_waf
  bind name http
  mode http
  log global
  option httplog
  timeout client 25s
  maxconn 10000

  # DDOS protection
  # Use General Purpose Couter (gpc) 0 in SC1 as a global abuse counter
  # Monitors the number of request sent by an IP over a period of 10 seconds
  stick-table type ip size 1m expire 1m store gpc0,http_req_rate(10s),http_err_rate(10s)
  tcp-request connection track-sc1 src
  tcp-request connection reject if { sc1_get_gpc0 gt 0 }
  # Abuser means more than 100reqs/10s
  acl abuse sc1_http_req_rate(ft_web) ge 100
  acl flag_abuser sc1_inc_gpc0(ft_web)
  tcp-request content reject if abuse flag_abuser

  acl static path_beg /static/ /dokuwiki/images/
  acl no_waf nbsrv(bk_waf) eq 0
  acl waf_max_capacity queue(bk_waf) ge 1
  # bypass WAF farm if no WAF available
  use_backend bk_web if no_waf
  # bypass WAF farm if it reaches its capacity
  use_backend bk_web if static waf_max_capacity
  default_backend bk_waf

# WAF farm where users' traffic is routed first
backend bk_waf
  balance roundrobin
  mode http
  log global
  option httplog
  option forwardfor header X-Client-IP
  option httpchk HEAD /waf_health_check HTTP/1.0

  # If the source IP generated 10 or more http request over the defined period,
  # flag the IP as abuser on the frontend
  acl abuse sc1_http_err_rate(ft_waf) ge 10
  acl flag_abuser sc1_inc_gpc0(ft_waf)
  tcp-request content reject if abuse flag_abuser

  # Specific WAF checking: a DENY means everything is OK
  http-check expect status 403
  timeout server 25s
  default-server inter 3s rise 2 fall 3
  server waf1 maxconn 100 weight 10 check
  server waf2 maxconn 100 weight 10 check

# Traffic secured by the WAF arrives here
frontend ft_web
  bind name http
  mode http
  log global
  option httplog
  timeout client 25s
  maxconn 1000
  # route health check requests to a specific backend to avoid graph pollution in ALOHA GUI
  use_backend bk_waf_health_check if { path /waf_health_check }
  default_backend bk_web

# application server farm
backend bk_web
  balance roundrobin
  mode http
  log global
  option httplog
  option forwardfor
  cookie SERVERID insert indirect nocache
  default-server inter 3s rise 2 fall 3
  option httpchk HEAD /
  # get connected on the application server using the user ip
  # provided in the X-Client-IP header setup by ft_waf frontend
  source usesrc hdr_ip(X-Client-IP)
  timeout server 25s
  server server1 maxconn 100 weight 10 cookie server1 check
  server server2 maxconn 100 weight 10 cookie server2 check

# backend dedicated to WAF checking (to avoid graph pollution)
backend bk_waf_health_check
  balance roundrobin
  mode http
  log global
  option httplog
  option forwardfor
  default-server inter 3s rise 2 fall 3
  timeout server 25s
  server server1 maxconn 100 weight 10 check
  server server2 maxconn 100 weight 10 check

Detecting attacks

On the load-balancer

The ft_waf frontend stick table tracks two information: http_req_rate and http_err_rate which are respectively the http request rate and the http error rate generated by a single IP address.
HAProxy would automatically block an IP which has generated more than 100 requests over a period of 10s or 10 errors (WAF detection 403 responses included) in 10s. The user is blocked for 1 minute as long as he keeps on abusing.
Of course, you can setup above values to whatever you need: it is fully flexible.

To know the status of IPs in your load-balancer, just run the command below:

echo show table ft_waf | socat /var/run/haproxy.stat - 
# table: ft_waf, type: ip, size:1048576, used:1
0xc33304: key= use=0 exp=4555 gpc0=0 http_req_rate(10000)=1 http_err_rate(10000)=1

Note: The ALOHA Load-balancer does not provide watch tool, but you can monitor the content of the table in live with the command below:

while true ; do echo show table ft_waf | socat /var/run/haproxy.stat - ; sleep 2 ; clear ; done

On the Waf

Every Naxsi error log appears in /var/log/nginx/naxsi_error.log. IE:

2012/10/16 13:40:13 [error] 10556#0: *10293 NAXSI_FMT: ip=, client:, server: , request: "GET /testphp.vulnweb.com/artists.php?artist=0+div+1+union%23foo*%2F*bar%0D%0Aselect%23foo%0D%0A1%2C2%2Ccurrent_user HTTP/1.1", host: ""

Naxsi log line is less obvious than modsecurity one. The rule which matched os provided by the argument idX=abcde.
No false positive during the test, I had to build a request to make Naxsi match it 🙂 .


Today, we saw it’s easy to build a scalable and performing WAF platform in front of any web application.
The WAF is able to communicate to HAProxy which IPs to automatically blacklist (throuth error rate monitoring), which is convenient since the attacker won’t bother the WAF for a certain amount of time 😉
The platform allows to detect WAF farm availability and to bypass it in case of total failure, we even saw it is possible to bypass the WAF for static content if the farm is running out of capacity. Purpose is to deliver a good end-user experience without dropping too much the security.
Note that it is possible to route all the static content to the web servers (or a static farm) directly, whatever the status of the WAF farm.
This make me say that the platform is fully scallable and flexible.
Thanks to HAProxy, the architecture is very flexible: I could switch my apache + modexurity to nginx + naxsi with no issues at all 🙂 This could be done as well for any third party waf appliances.
Note that I did not try any naxsi advanced features like learning mode and the UI as well.

Related links