Quinn's Blog

Preventing brute-force attacks on Wordpress's wp-login.php

August 30, 2014

The solution here uses ModSecurity and is based mostly on this article by Todd Garrison. There are many ways that one could handle this issue, for example a plugin in wordpress, an upstream proxy server that handles security, or a modifacation to the web server (in this case apache). I chose to handle this on the webserver in part because there were many well documented solutions for this and that it seemed like the best way to conserve system resources (by not adding bloat to wordpress) without adding too much infrastructure.

Installation

The first thing you’ll need to do is install ModSecurity which will probably be available in your package manager (I’m using yum / RHEL in this example):

sudo yum install mod_security

This will most likely create some config files for you within your apache root directory (e.g. /etc/httpd). These settings will enable ModSecurity as well as setup logging and some sensible defaults. These are in mod_security.conf. You may want to check the default settings for uploads in case they will affect your users.

Configure for /wp-login.php

Borrowing (heavily) from the config provided here, create a config for wordpress and place in the ModSecurity config directory:

# /etc/httpd/modsecurity.d/wordpress.conf

# This has to be global, cannot exist within a directory or location clause . . .
SecAction phase:1,log,pass,initcol:ip=%{REMOTE_ADDR},initcol:user=%{REMOTE_ADDR},id:1

<Location /wp-login.php>
    # Setup brute force detection.

    # React if block flag has been set.
    SecRule user:bf_block "@gt 0" "deny,status:401,log,msg:'ip address blocked for 5 minutes, more than 15 login attempts in 3 minutes.',id:2"

    # Setup Tracking.  On a successful login, a 302 redirect is performed, a 200 indicates login failed.
    SecRule RESPONSE_STATUS "^302" "phase:5,t:none,log,pass,setvar:ip.bf_counter=0,id:3"
    SecRule RESPONSE_STATUS "^200" "phase:5,chain,t:none,log,pass,setvar:ip.bf_counter=+1,deprecatevar:ip.bf_counter=1/180,id:4"
    SecRule ip:bf_counter "@gt 15" "t:none,setvar:user.bf_block=1,expirevar:user.bf_block=300,setvar:ip.bf_counter=0"
</location>

It is almost identical to the example provided in the link save a few changes possibly due to version differences. For example, the id parameter has been added to all of the rules except the last one.

Once you’re ready, reload the apache config:

sudo /etc/init.d/httpd reload

Explanation

The ModSecurity syntax can be a bit cryptic, so I’ll try to break it down as best I can. The first assumption is that a 302 response on wp-login.php means a successful login and a 200 means a failed one. Each time a request is made (that returns with a 200 status code) it increments a counter for the ip address by one setvar:ip.bf_counter=+1 and then checks to see if the counter is over 15 SecRule ip:bf_counter "@gt 15". If it is, it says a variable to block the user that expires in 300 seconds (5 minutes) setvar:user.bf_block=1,expirevar:user.bf_block=300 and resets the counter on the IP address setvar:ip.bf_counter=0. As long as the blocking variable is set the user will be denied access SecRule user:bf_block "@gt 0" "deny,status:401".

While all of this is happening the ip address counter is being decremented by 1 every 180 seconds (3 minutes) deprecatevar:ip.bf_counter=1/180. This means that as long as the user waits at least 3 minutes in between each request they will never get denied access for 5 minutes.

Testing

The easiest way to test this is to just hit the login screen a bunch with incorrect credentials. You should see a screen telling you that you are denied access pretty quickly.

I hope this helps with how to deal with this common brute-force attack and also how ModSecurity works.