Blog

Sicherheit für Perl-Anwendungen: fail2ban

03.02.2021 // Renée Bäcker

Ist eine Webanwendung öffentlich erreichbar, wird es nicht lange dauern und irgendwelche Bots versuchen sich anzumelden. Oder es werden wild irgendwelche URLs aufgerufen. Auch wenn die Anmeldeversuche wahrscheinlich scheitern, geht die Bot-Aktivität zu Lasten der Webanwendung. Und mit genügend versuchen klappt es vielleicht doch mal, dass sich jemand Unbefugtes anmeldet.

Aus diesem Grund sollte man IP-Adressen nicht dauerhaft auf die Webanwendung lassen. Sie sollten nach einer bestimmten Anzahl an Fehlversuchen ausgesperrt werden.

Unter Linux kann man das bequem mit iptables machen. Aber wie ist die Syntax um jemanden auszusperren? Und muss man die Fehlversuche selbst raussuchen und dann die IPs sperren? Nein. Für diese Aufgabe gibt es fail2ban.

fail2ban ist ein Tool, das die Firewallregeln von iptables aktualisiert. Also IP-Adressen sperren und die Sperre wieder aufheben kann. Nach der Installation mittels

apt install fail2ban

kann das Tool direkt loslegen und z.B. SSH-Loginversuche auswerten. Dazu nimmt es die SSH-Logs und durchsucht diese nach Fehlversuchen. Wird eine bestimmte Anzahl von Fehlversuchen registriert, werden die Regeln von iptables aktualisiert.

Damit das Tool nicht nur SSH-Logs durchsucht und Angriffsversuche entdeckt, werden sogenannte Filter für verschieden Anwendungen wie Apache, MySQL, Nagios und viele mehr zur Verfügung gestellt.

Man kann aber auch eigene Anwendungen schützen. Als Beispiel nehmen wir eine Mini-Anwendung, die mit Mojolicious umgesetzt ist:

#!/usr/bin/perl

use strict;
use warnings;

use Mojolicious::Lite -signatures;

get '/' => 'index';
post '/' => sub ($c) {
    if ( $c->param('user') ne 'Test' ) {
        return $c->redirect_to('/');
    }

    $c->session( logged_in => 1 );
    $c->redirect_to( '/welcome' );
};

under '/' => sub ($c) {
    if ( !$c->session('logged_in') ) {
        $c->redirect_to('/');
        return;
    }

    return 1;
};

get '/welcome' => sub ($c) {
    $c->render( data => 'Welcome!' );
};

app->start;

__DATA__
@@ index.html.ep
<form action="/" method="post">
<input type="text" name="user">
</form>


Es gibt eine Route, um sich anzumelden und ist die Anmeldung erfolgreich bekommt man eine Willkommensseite. Nichts spezielles.

Um diese Anwendung mit fail2ban zu schützen, müssen wir etwas mehr loggen - nämlich die erfolglosen Loginversuche.

# [...]
    if ( $c->param('user') ne 'Test' ) {
        $c->app->log->warn(
            sprintf "Login failed for host <%s>",
                $c->tx->remote_address
        );
        return $c->redirect_to('/');
    }
# [...]

Wenn wir jetzt die Anwendung aufrufen und ganz häufig die falschen Zugangsdaten eingeben oder nicht-existente Routen aufrufen, passiert nichts. Die Anwendung kann immer wieder aufgerufen werden.

Jetzt richten wir fail2ban ein.

Als erstes benötigen wir einen Filter. Dazu erstellen wir die Datei /etc/fail2ban/filter.d/app.conf mit folgendem Inhalt:

[Definition]
failregex = .* Login failed for host "<HOST>"
ignoreregex =

Bei failregex wird ein Regulärer Ausdruck angegeben, mit dem fail2ban einen Fehlversuch im Logfile erkennt. Das <HOST> ist ein spezieller Platzhalter, mit dem fail2ban IP-Adressen erkennt. Alternativ könnte man auch (?:::f{4,6}:)?(?P<host>\\S+) schreiben.

Diesen Filter können wir direkt testen

$ fail2ban-regex ~/app/log/development.log /etc/fail2ban/filter.d/app.conf 

Running tests
=============

Use   failregex filter file : app, basedir: /etc/fail2ban
Use         log file : /home/dev/app/log/development.log
Use         encoding : UTF-8


Results
=======

Failregex: 3 total
|-  #) [# of hits] regular expression
|   1) [3] .* Login failed from host "<HOST>"
`-

Ignoreregex: 0 total

Date template hits:
|- [# of hits] date format
|  [65] Year(?P<_sep>[-/.])Month(?P=_sep)Day 24hour:Minute:Second(?:,Microseconds)?
`-

Lines: 65 lines, 0 ignored, 3 matched, 62 missed [processed in 0.00 sec] 
Missed line(s): too many to print.  Use --print-all-missed to print all 62 lines

Da wir schon falsche Zugangsdaten eingegeben haben, sollte der Test ein paar Treffer finden.

Da der Filter noch nicht aktiv ist, schauen wir uns mal an, wie die iptables-Regeln aktuell aussehen:

$ sudo iptables -L
[sudo] Passwort für otrsvm: 
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
f2b-sshd   tcp  --  anywhere             anywhere             multiport dports ssh

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain f2b-sshd (1 references)
target     prot opt source               destination         
RETURN     all  --  anywhere             anywhere

Man sieht bei den Regeln für den eingehenden Datenverkert das Ziel f2b-sshd. Und weiter unten genauer aufgeführt was dieses Ziel bedeutet.

Wir müssen fail2ban jetzt so konfigurieren, dass es einen sogenannten Jail für unsere Anwendung gibt. Dazu fügen wir in der Datei /etc/fail2ban/jail.conf folgendes hinzu:

[app-name]
enabled  = true
port     = 3000
filter   = app
logpath  = /path/to/app/log/development.log
action   = iptables[name=AppName, port=3000, protocol=tcp]
maxretry = 5
bantime  = 60

Folgende Angaben werden hier gemacht:

  • enabled = true heißt, dass dieses Jail aktiv ist
  • filter = app bedeutet, dass der Filter app.conf genutzt wird
  • logpath gibt den Pfad zu Logdatei an, die gefiltert werden soll
  • Bei action gibt man an, was beim Erreichen der maximalen Funde gemacht werden soll. Hier wird ein iptables-Eintrag gemacht
  • Unter maxretry gibt man die Anzahl der maximalen Funde an. Hier darf man maximal 5 Fehlversuche machen.
  • bantime gibt die Zeit (in Sekunden) an, die eine IP-Adresse gebannt wird.

Jetzt versuchen wir uns wieder mehrfach anzumelden. Geben wir jetzt 5 mal die falschen Zugangsdaten, ist die Anwendung nicht mehr erreichbar:

dc319a34-5203-46dd-8eb6-cb076787cac9.png

Im fail2ban.log ist jetzt folgendes zu finden:

2020-10-28 12:38:11,757 fail2ban.jail           [472]: INFO    Creating new jail 'app'
2020-10-28 12:38:11,757 fail2ban.jail           [472]: INFO    Jail 'app' uses pyinotify
2020-10-28 12:38:11,758 fail2ban.filter         [472]: INFO    Set jail log file encoding to UTF-8
2020-10-28 12:38:11,763 fail2ban.jail           [472]: INFO    Initiated 'pyinotify' backend
2020-10-28 12:38:11,818 fail2ban.actions        [472]: INFO    Set banTime = 60
2020-10-28 12:38:11,826 fail2ban.filter         [472]: INFO    Set maxRetry = 5
2020-10-28 12:38:11,829 fail2ban.filter         [472]: INFO    Set jail log file encoding to UTF-8
2020-10-28 12:38:11,894 fail2ban.filter         [472]: INFO    Added logfile = /home/dev/app/log/development.log
2020-10-28 12:38:12,173 fail2ban.filter         [472]: INFO    Set findtime = 600
2020-10-28 12:38:12,192 fail2ban.jail           [472]: INFO    Jail 'sshd' started
2020-10-28 12:38:12,203 fail2ban.jail           [472]: INFO    Jail 'app' started
2020-10-28 12:39:07,383 fail2ban.filter         [472]: INFO    [app] Ignore 127.0.0.1 by ip
2020-10-28 12:41:02,691 fail2ban.filter         [472]: INFO    [app] Found 192.168.123.1
2020-10-28 12:41:04,382 fail2ban.filter         [472]: INFO    [app] Found 192.168.123.1
2020-10-28 12:41:06,460 fail2ban.filter         [472]: INFO    [app] Found 192.168.123.1
2020-10-28 12:41:07,980 fail2ban.filter         [472]: INFO    [app] Found 192.168.123.1
2020-10-28 12:41:10,028 fail2ban.filter         [472]: INFO    [app] Found 192.168.123.1
2020-10-28 12:41:10,447 fail2ban.actions        [472]: NOTICE  [app] Ban 192.168.123.1

Wenn wir jetzt in die iptables-Regeln schauen, sehen wir eine Veränderung:

~$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
f2b-AppName  tcp  --  anywhere             anywhere             tcp dpt:3000
f2b-sshd   tcp  --  anywhere             anywhere             multiport dports ssh

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain f2b-AppName (1 references)
target     prot opt source               destination         
REJECT     all  --  192.168.123.1  anywhere             reject-with icmp-port-unreachable
RETURN     all  --  anywhere             anywhere            

Chain f2b-sshd (1 references)
target     prot opt source               destination         
RETURN     all  --  anywhere             anywhere 

Beim Ziel f2b-AppName gibt es für die aufrufende IP jetzt einen REJECT-Eintrag. Durch die bantime=60-Angabe im Jail, wird der Eintrag nach 1 Minute aber wieder gelöscht.

Die Anwendung und der Filter sind auch im Gitlab-Repository zu finden.


Permalink: