How to Stop Malicious Vulnerability Scanners in Laravel with Fail2ban

Every website is bombarded with dozens of unwanted requests every day. The talk here is of so-called vulnerability scanners. In this article you will learn what they are all about and how you can fight the uninvited guests.

What is a Vulnerability Scanner?

Before any successful burglary, the object of desire must be scouted. Of course, you don't do this yourself. A self-respecting burglar has his henchmen for that. Imagine a small, inconspicuous guy. An everyman. Someone who disappears in the crowd. He walks past your house and casually looks under the doormat in front of your door. Is there a spare key lying there? No. Then he strolls relaxed along the wall of your house and rattles a window from time to time. Quietly, of course. Locked. If he's even braver, he might make a little detour into your garden. Maybe he'll have more luck with the patio door. Bingo, only ajar. He immediately informs his client and the latter quickly gets to work.

Burglar looks through door

House photo created by jcomp - www.freepik.com

You can imagine a vulnerability scanner like this or something similar. Of course, this is not a real person. Would be a miserably boring job, after all. No, it is an automatic program. This just pops in and checks if you might have done something stupid when setting up your website. Publicly available Git repository? A database backup lying around? A CMS that hasn't been updated? Anything can be useful. Most bots take a relatively similar approach. That is, they call similar paths. And that's exactly what you can take advantage of. Wouldn't it be great if a man with a big baton would watch over your property, respectively your website? He would then simply hit anyone over the head who shook the window. Well, the man with the stick has a name: fail2ban.

What is fail2ban?

The main purpose of fail2ban is to temporarily block certain IP addresses based on filters. There are several predefined filters. For example, filters that block attackers after so and so many unsuccessful login attempts. For this purpose, log files are usually searched for certain patterns. You can also define these patterns yourself. For example, you can specify a list of paths where you want to kick out an uninvited guest. But then you have to write a long regex pattern. Quite confusing. Why don't we just kick attackers out when they trigger a certain HTTP code? It should be quite unique, of course, so you don't accidentally scare away your desired visitors. I have built a small solution for this in Laravel, which I would like to present to you.

Mmmmh... Hooooneeeey

Winnie Pooh eats Honey

The basic idea behind this solution is that we want to set a trap for the scanner. In technical jargon, this is called a honeypot. As soon as the scanner tampers with the honeypot, the stick swooshes down respectively the scanner is blocked.

But how could we best trigger fail2ban if we don't want to check the path directly? Well for example with a response code. The best thing to do here is to take something that can generally never occur and then block the scanner immediately after the first request. Unfortunately there is no response code for a honeypot. But there is the Teapot. This is actually an April Fool's joke. But this code is really part of the standard. And it has the added benefit of never being used otherwise. But before we can set up a filter, we first need to install fail2ban.

We install fail2ban

Under Ubuntu it can be done like this:

sudo apt update
sudo apt install fail2ban

Then we start and activate the service:

sudo systemctl start fail2ban
sudo systemctl enable fail2ban

Afterwards, we create a new filter. Here we simply check if the status code 418 appears in any string whatsoever. I am assuming here that we are using Nginx as our web server. But the filter will probably work for Apache as well.

/etc/fail2ban/filter.d/nginx-honeypot.conf

[Definition]
failregex = ^<HOST>.*"(GET|POST).*" (418) .*$
ignoreregex =

Accordingly, we then additionally create a jail. We want to block the scanner immediately at the first attempt, so we set maxretry to 1. When using Apache, the path to the Apache log must of course be entered under logpath.

/etc/fail2ban/jail.d/honeypot.conf

[nginx-honeypot]
enabled = true
filter = nginx-honeypot
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 1

After that we have to restart fail2ban.

$ fail2ban-client restart

With the status command we can check if our jail is really activated.

fail2ban-client status

It's All About The Honey

Now we have fail2ban set up. However, there is no way to generate a 418 code in our app yet. That is why we now first create a configuration file. In this we enter all the paths which vulnerability scanners usually call. We can create a list of paths from a log file, for example, and it is also always very individual. In a Laravel installation, for example, we can block all access to wp-admin (Wordpress) across the board. In wordpress, that would be rather inconvenient. But what we can definitely block are requests to .git or .env.

config/honeypot.php

return [
    'paths' => [
        '.env',
        '.git/config',
        '.git/HEAD',
        '.well-known/security.txt',
        // And so on...
    ],
];

Then we create a controller in which we access our configuration. If the user calls a path that starts with one of our registered paths, we return a 418 code.

namespace App\Http\Controllers;

use Illuminate\Http\Response;

class Honeypot extends Controller
{
    public function __invoke(string $path = '')
    {
        // Load the array of honeypot paths from the configuration.
        $honeypot_paths_array = config('honeypot.paths', []);

        // Turn the path array into a regex pattern.
        $honeypot_paths = '/^(' . str_replace(['.', '/'], ['\.', '\/'], implode('|', $honeypot_paths_array)) . ')/i';

        // If the user tries to access a honeypot path, fail with the teapot code.
        if (preg_match($honeypot_paths, $path)) {
            abort(Response::HTTP_I_AM_A_TEAPOT);
        }

        // Otherwise just display our regular 404 page.
        abort(Response::HTTP_NOT_FOUND);
    }
}

We now need to register a route. And in this case it will be a wildcard route. Here it is especially important that the route is registered last. Otherwise we will overwrite all other paths at this point. A positive side effect is that we cannot accidentally mark a known route as a honeypot. Even if we enter it in the configuration, Laravel will always prefer the correct route. Our honeypot controller will only be considered if no other controller is responsible.

use App\Http\Controllers\Honeypot;

Route::get('{path}', Honeypot::class)->where('path', '.*');

That's about it. Our trap is set. All we have to do now is wait and enjoy the fruits of our labor. It is also recommended to regularly check the log files and update the honeypot.php. If you don't want to wait that long, you can also do it yourself. At OWASP you can find a list of scanners that you can unleash on your site. The called paths you can then take over into the configuration.

A Few Words in Conclusion

One thing should be clear to everyone. This measure is not a substitute for a secure CMS/framework or whatever. With this method you can prevent most of the automatic scans on your system. But of course not all of them. That's because if you really have a database dump lying around, for example, our honeypot controller would not take action at all.

What you can achieve is that you get less unwanted traffic. No more and no less. Also, you may be able to prevent vulnerabilities from being found that would otherwise have been discovered. For example, because scanners often call common paths first. So it increases your security level. But it does not provide one hundred percent protection. That should be clear to you at this point.