To main content

WordPress: Bruteforce-Defence using .htaccess

Published by Benjamin Marwell on
Everyone owning a wordpress installation will know plugins like LoginSecuritySolution, which will send emails in case of ongoing brute force attacks. The problem is that your login names are not being hidden from attackers. But with a simple .htaccess modification you can protect your blog from these kind of attacks easily. The following recommendations are neither complete nor bullet-proof, but will allow to block about 98% of the most common attacks. All you need to do is to modify your .htaccess file slightly. [caption id="attachment_5941" align="aligncenter" width="1600"]Polizeiabsperrung - Karl-Heinz Laube / pixelio.dePolizeiabsperrung - Karl-Heinz Laube /[/caption]

Problematic author names

The attacker can just grab all the author names of your wordpress blog, which is the main reason that brute force login attemps might start at all. All he needs to do is to try different internal author IDs, which he doesn't know yet. By just incrementing the numbers starting from zero, the attacker can grab all the author names from a standard wordpress installation in no time. It's an easy and fast procedure.

Using curl to query author names

The following steps will show you how to create a script which will grab well known author names. The first thing you will need is a shell script containing a for-loop, which will count to e.g. 100 and this tries to receive the first 100 user names from any wordpress blog. The author ID is just a simple number and can be queried like this:

Now, if you have a standard wordpress installation, your client will receive a redirect containing the correct author name (and this login name). "Redirection" means that you will receive an HTTP header containing a location directive, again containing the author archive URL you were looking for. You know you got a hit if you received a 301 redirect. So if you parse the URL you just received you will have the author name - and thus the login name of this user.
user@host:~$ curl -I
HTTP/1.1 301 Moved Permanently
Date: Thu, 15 Oct 2015 11:49:37 GMT
Server: HTTPd
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Vary: User-Agent
Content-Type: text/html; charset=UTF-8
user@host:~$ curl -sI | grep "Location:" | sed  -e 's#Location:.*/author/\(.*\)/#\1#g'
Explanation: The first command (which is curl) will load the given URL. The first argument -I (an uppercase i -- this is important) will give you the HTTP header only, which is all we need (i.e. dropping the body).  You will need to make your script count onwards until you can find a status code 301 (moved permanently). If you hit an unknown author id, you will instead see a 404 (not found). So, hit found? Let's do the request again, grepping for the Location: line and thus giving us a prefix and the URL only. Now sed (stream editor) will look for the part after the fixed /author/ string. As this part is fixed and user names cannot contain forward slashes, you can just store anything until you hit the next forward slash. The result will just print the expression captured by the round braces. As you can see, it is very easy to grab author names via brute force. Usually you will hit a few user names using the first 10 IDs or so. An attacker will therefore have an easy job with your blog and will now start guessing your user's passwords happily. So now that we know how attackers get your user names, let's take look at our defence options.

Supress redirects using the .htaccess file

The most simple solution I came up with is another http redirect. The reason for this is that for an http redirect, your webserver won't need to query a php script and thus the response is a lot faster which again will save precious cpu time. Lucky there are also no internal wordpress links to the author's id, only the name will be linked by default. Thus nothing will prevent us from removing the author id redirect. To supress the redirect just insert the following directive just before your # BEGIN WORDPRESS section.
    RewriteEngine On
    RewriteBase /
    RewriteCond %{QUERY_STRING} ^author=(.*)$
    RewriteRule ^/$ /author/? [L,R=303]
The shown directive will redirect the attacker to the non-existent page /author/. He will also get a new status code 303 (see other) to emphasize that this is different content than originally expected, not a (temporarily) moved page (which would use a 301 or 302). The final destination will be a 404 (not found) nevertheless, but this is just what we want anyway. So... this is it! If you will try to query one of those URLs again, you will see a new message which does not show the author name anymore:
user@host:~$ curl -sI
HTTP/1.1 303 See Other
Date: Thu, 15 Oct 2015 12:05:28 GMT
Server: Apache/2.2.15 (CentOS)
Cache-Control: max-age=3600
Expires: Thu, 15 Oct 2015 13:05:28 GMT
Vary: Accept-Encoding
Connection: close
Content-Type: text/html; charset=iso-8859-1
The most important thing about this response is a fast response. The .htaccess redirect will satisfy this condition.

Eyewash which works [for now]

About the quality of this solution: Yes, it is eyewash, it is Security by Obscurity. Attackers can get the author name by other means. Firstly, there is the sitemap.xml file which will provide links to the author archives. And then there is a link at the end of every blog post. It happens to work anyway, because attackers won't want to parse html or xml for performance reasons. Even if some do -- most won't. That alone makes this workaround worth a lot. This also means it is probably a very good idea to use plugins like Login Security Solution.

Secure your xmlrpc.php

There is another chapter of brute force attacks. The file xmlrpc.php. This file contains all the functions which other tools will need to access the blog's functions. Besides trackbacks, pingbacks and comments there are functions for logging in and publishing a blog post. Everything you would expect your android app to do. So here is another eyewash solution.

Defend your xmlrpc.php via .htaccess

So here is my "Defence light". The following code will attackers make to supply a correct browser agent. If they don't there is no access to pings, trackbacks, etc. -- of course this will also suppress other non-wordpress-blogs to leave pingbacks or trackbacks.
<IfModule mod_setenvif.c>
    <Files xmlrpc.php>
        BrowserMatch "Poster" allowed
        BrowserMatch "Wordpress" allowed
        BrowserMatch "Windows Live Writer" allowed
        BrowserMatch "wp-iphone" allowed
        BrowserMatch "wp-android" allowed
        BrowserMatch "wp-windowsphone" allowed

        Order Deny,Allow
        Allow from env=allowed
Now the only valid browser agent identifiers are "Poster", "Wordpress", "Windows Live Writer", "wp-android", "wp-iphone" and "wp-windowsphone", which are written to an environment variable called "allowed." Of course the attacker can chose any browser agent name he wants to -- but usually they won't bother. For this reason, this solution is simple and will probably be reliable for 90% of all attacks.

More counter measures

Wordpress-Plugins which will defend brute force

So there is more to it then just fooling around with your .htacccess. You can use a time-based one time password (TOTP). It will create to levels of security: Knowing a secret (knowledge) and having an item (posession). This is why I'd like to recommend the plugin Google Authenticator. Sadly it is not being actively developed anymore. So please also take a look at alternatives. I will probably take a look at miniOrange 2factor Authentication soon. Don't forget there are other plugins. Once I used WP Limit Login Attemps, before I switched to Login Security Solution (which has similar functionality). The combination with Google Authenticator will probably provide one secure and easy-to-use barrier for end users.

What defence am I missing?

So is there anything else you put into your .htaccess file? If so, what is a "must have", what is a "no go"? Did I miss your favourite plugin? Please reply below, I'll be happy to read your opinions!