whateverthing.com

Retrofitted Login Throttling

Last month, Nintendo announced that one of its sites (Nintendo Club of Japan) had been compromised by a brute force login attack. The attackers made 15,000,000 authentication attempts, and successfully took control of 24,000 accounts.

The attack would have failed if Nintendo had implemented login throttling.

Fail2Ban is a Python-based utility that hooks directly into the system's firewall to ban malicious IP addresses, and I'm going to show a few easy steps to make your site safer by blacklisting brute-force attackers. If you maintain a web application that doesn't have built-in authentication throttling, this might be the how-to you're looking for - alternatively, this would work as an additional way to punish pesky rogue connections.

Step 1: The Log

Any time an authorization fails, we need to log it to a file on disk. While you might be able to deduce a failed authentication attempt from your existing Apache or Application logs, let's do a quick demonstration of how you can add failure logging to your codebase in a usable format. The log output required for fail2ban is just the time and the source IP (in that order), but for your own records you might also want to capture the account name, user agent, or other data.

The output string will be fairly simple, just the current timestamp and the IP of the failure:

<?php

$log_message = sprintf(
    '[%s] Authorization failure: %s', 
    strftime('%F %T'), 
    $_SERVER['REMOTE_ADDR']
);

If you think other information might be useful, you can go ahead and add things like the User Agent ($_SERVER['HTTP_USER_AGENT']) or X-Forwarded For header ($_SERVER['HTTP_X_FORWARDED_FOR']) to the logged data.

Pro Tip: DO NOT LOG PASSWORDS OR CREDIT CARD NUMBERS. If you ever find yourself serializing an object/array to a log file, be certain that it does not contain passwords. It goes without saying that if you actually decide to log credit card numbers, you should probably just pack up your keyboard and go work on a farm. It's healthier than a cubicle, anyway.

I recommend using a Psr\Log compliant logging service for writing the data, but because I'm writing this at nearly midnight, I'm going to do it the bad way (you'll have to set $log_file to a writeable file):

file_put_contents($log_file, $log_message . "\n", FILE_APPEND);

You'll end up with a file containing lines like so:

[2013-07-23 23:34:56] Authorization failure: 10.0.2.2
[2013-07-23 23:35:05] Authorization failure: 10.0.2.2

Make sure you put the logging code in the actual "failure" portion of your code. It would make sense to make it a function or a method, so you could do something like this:

$log_file = '/path/to/app/logs/auth-failure.log';

function logFailure($log_file, $ip)
{
    $log_message = sprintf(
        '[%s] Authorization failure: %s', 
        strftime('%F %T'), 
        $ip
    );

    file_put_contents($log_file, $log_message . "\n", FILE_APPEND);
}

// ...

if ( true === $authorized ) {
    // User is authenticated
    // Insert magical goodness here
} else {
    // Failed to authorize!
    logFailure($log_file, $_SERVER['REMOTE_ADDR']);
}

... I can see that I've made the classic mistake of dismissing things as "easy". Uh. Sorry. Anyway, moving along ...

Step 2: The Banhammer

A ban issued by fail2ban is quite brutal: the source IP is prohibited from all access to the host for a period of time (5 minutes, 24 hours, a week, permanently, etc). Regular Expressions are matched against each line in the log file, and decisions are made based on the frequency of the failures.

Installing fail2ban should be easy on most Linux systems, usually just a simple yum install fail2ban or apt-get install fail2ban will get you going.

Because we've gone with a simple log format, we can use a simple matching pattern. In the filters folder (usually /etc/fail2ban/filter.d/) create a file called "webapp.conf" like so:

[Definition]
failregex      = Authorization failure: <HOST>
ignoreregex    =

This will match on lines containing Authorization failure. Fail2Ban includes several internal checks that attempt to find timestamps in matching lines, and the format we've gone with for our timestamp is one of them - this means that we can just rely on Fail2Ban to detect the timestamp automatically.

Now we can set the rules for our jail in the "jail.conf" file. Add this to the bottom:

[webapp]
enabled = true
port = http
protocol = tcp
filter = webapp
logpath = /path/to/app/logs/auth-failure.log
maxretry = 5
findtime = 600
bantime = 300

This tells fail2ban: Enable the webapp filter for HTTP, and if 5 failures are detected in a 10-minute period, issue a five-minute ban to the IP. If you want to make the ban permanent, use a negative value for "bantime".

One thing to keep in mind - and this caused a few hours of confusion on my part - is that fail2ban uses the system time, whereas the PHP date command might use the timezone set by PHP. This can result in fail2ban ignoring new entries because the timestamp is several hours older than what fail2ban thinks is the current time. So, make sure that the PHP timezone and the system timezone configuration are in agreement (or adjust the written timestamp to match the system timezone).

Pro Tip: When testing firewall rules, it's wise not to permanently ban yourself from your own machine. Be careful. :-)

Once this configuration is in place, restart fail2ban and then try to trigger the rule on yourself. This ought to get you banned from your own server for five minutes :) The command to restart fail2ban is usually /etc/init.d/fail2ban restart.

You should be able to use the command fail2ban-client status webapp to see how many failure lines there are in the log, as well as the current list of banned IPs and the total that have been banned since the service was started.

If you want to play along at home some more, you should also look into fail2ban's notification services: it can alert you to suspicious activity.

Step 3: Profit!

This handy tool can save your butt in a brute-force situation. If Nintendo's login system had detected and punished high-volume failures, the attacker wouldn't have been able to keep pounding the system for months.

While an in-app system for detecting brute force attacks is the best solution (because you can message to the user about what is happening and why they are being punished, as well as performing advanced detection of distributed attacks), the fail2ban solution is a drop-in tool that can get you most of the way there.

I hope you found this informative and helpful. Thanks for checking in!

Published: July 24, 2013

Categories: coding, howto

Tags: coding, dev, development, howto, utilities, maintenance, security