Wie man bösartige Schwachstellen-Scanner in Laravel mit Fail2ban stoppt

Jede Webseite wird täglich mit dutzenden ungewollten Anfragen bombardiert. Die Rede ist hier von sogenannten Schwachstellen-Scannern. Was es damit auf sich hat und wie man die ungebetenen Gäste bekämpfen kann, erfahrt Ihr in diesem Artikel.

Was ist ein Schwachstellen-Scanner?

Vor jedem gelungenen Einbruch steht das Auskundschaften des Objektes der Begierde. Natürlich macht man das nicht selbst. Ein Einbrecher, der etwas auf sich hält, hat dafür seine Handlanger. Stellt Euch da einen kleinen, unauffälligen Typen vor. Ein Jedermann. Jemand der in der Masse verschwindet. Der läuft nun an Eurem Haus vorbei und schaut ganz beiläufig Mal unter die Fußmatte vor Eurer Tür. Liegt da vielleicht ein Ersatzschlüssel? Fehlanzeige. Dann schlendert er ganz entspannt an der Hauswand entlang und rüttelt ab und zu Mal an einem Fenster. Ganz leise natürlich. Verschlossen. Wenn er noch mutiger ist, macht er vielleicht einen kleinen Abstecher in Euren Garten. Vielleicht hat er bei der Terrassentür mehr Glück. Bingo, nur angelehnt. Sogleich sagt er seinem Auftraggeber Bescheid und dieser macht sich flugs ans Werk.

Burglar looks through door

House photo created by jcomp - www.freepik.com

So oder so ähnlich kann man sich einen Schwachstellen-Scanner vorstellen. Natürlich ist dies keine reale Person. Wäre ja auch eine elendig langweilige Arbeit. Nein, es ist ein automatisches Programm. Das schaut einfach mal vorbei und guckt, ob Ihr bei der Einrichtung Eurer Webseite vielleicht eine Dummheit begangen habt. Öffentlich zugängliches Git-Repository? Ein herumliegendes Datenbank-Backup? Ein nicht aktualisiertes CMS? Alles kann nützlich sein. Die meisten Bots gehen dabei relativ ähnlich vor. Sprich sie rufen ähnliche Pfade auf. Und genau das kann man sich zunutze machen. Wäre es nicht toll, wenn ein Mann mit einem großen Knüppel über Euer Anwesen, respektive Eure Webseite, wachen würde? Der würde dann einfach jedem eins über den Schädel hauen, der am Fenster rüttelt. Nun, der Mann mit dem Knüppel hat einen Namen: fail2ban.

Was ist fail2ban?

Der Hauptzweck von fail2ban ist das temporäre Blockieren bestimmter IP-Adressen auf Basis von Filtern. Es gibt dabei diverse vorgefertigte Filter. Zum Beispiel welche, die Angreifer nach so und so vielen vergeblichen Login-Versuchen blockieren. Dafür werden in der Regel Log-Dateien nach bestimmten Mustern durchforstet. Diese Muster könnt Ihr dabei auch selber festlegen. Ihr könnt zum Beispiel eine Liste von Pfaden angeben, bei dessen Aufruf Ihr einen ungebetenen Gast rauswerfen wollt. Dafür schreibt Ihr dann allerdings eine elendig lange Regex-Pattern. Ziemlich unübersichtlich. Warum schmeißen wir Angreifer nicht einfach raus, wenn sie einen bestimmten HTTP-Code auslösen? Der sollte natürlich ziemlich einzigartig sein, damit Ihr nicht versehentlich Eure erwünschten Besucher vergräzt. Ich habe dafür eine kleine Lösung in Laravel gebaut, die ich Euch einmal vorstellen möchte.

Mmmmh... Hooooniiiig

Winnie Pooh eats Honey

Der Grundgedanke hinter dieser Lösung ist, dass wir dem Scanner eine Falle stellen wollen. Im Fachjargon spricht man in diesem Fall von einem Honigtopf. Sobald der Scanner sich am Honigtopf zu schaffen macht, saust der Knüppel herunter bzw. er wird geblockt.

Aber wie könnten wir fail2ban am besten triggern, wenn wir nicht direkt den Pfad überprüfen wollen? Nun zum Beispiel mit einem Response-Code. Am besten nehmen wir hier etwas, was generell nie vorkommen kann und blocken den Scanner dann sofort nach dem ersten Request. Leider gibt es keinen Response-Code für einen Honeypot. Aber es gibt den Teapot. Dabei handelt es sich zwar eigentlich um einen April-Scherz. Aber dieser Code ist wirklich Teil des Standards. Und er hat den zusätzlich Vorteil, dass er sonst nie verwendet wird. Aber bevor wir einen Filter einrichten wollen, müssen wir erst einmal fail2ban installieren.

Wir installieren fail2ban

Unter Ubuntu geht das so:

sudo apt update
sudo apt install fail2ban

Anschließend starten und aktivieren wir den Service:

sudo systemctl start fail2ban
sudo systemctl enable fail2ban

Anschließend legen wir einen neuen Filter an. Hier wird einfach nur geprüft, ob der Status-Code 418 in einem wie auch immer gearteten String auftaucht. Ich gehe hier davon aus, dass wir Nginx als Webserver verwenden. Der Filter wird aber wahrscheinlich auch für Apache funktionieren.

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

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

Entsprechend legen wir dann zusätzlich noch ein Jail an. Wir wollen hier den Scanner sofort beim ersten Versuch scannen, setzen maxretry also auf 1. Bei der Verwendung von Apache, muss unter logpath natürlich entsprechend der Pfad zum Apache-Log eingetragen werden.

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

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

Anschließend müssen wir fail2ban neu starten.

$ fail2ban-client restart

Über den status-Command können wir dann noch prüfen, ob unser Jail nun auch wirklich aktiviert ist.

fail2ban-client status

Her mit dem Honig

Nun haben wir fail2ban eingerichtet. Allerdings gibt es in unserer App noch keine Möglichkeit einen 418-Code zu erzeugen. Darum erstellen wir nun zunächst einmal eine Konfigurations-Datei. In dieser tragen wir alle Pfade ein, die Schwachstellen-Scanner in der Regel aufrufen. Eine Liste mit Pfaden können wir zum Beispiel aus einer Log-Datei erstellen und sie ist auch immer sehr individuell. In einer Laravel-Installation können wir zum Beispiel pauschal alle Zugriffe auf wp-admin (Wordpress) sperren. In Wordpress wäre das eher ungünstig. Was wir aber auf jeden Fall sperren können, sind Zugriffe auf .git oder .env.

config/honeypot.php

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

Anschließend erstellen wir einen Controller in dem wir auf unsere Konfiguration zugreifen. Ruf der Benutzer einen Pfad auf, der mit einem unserer eingetragenen Pfade beginnt, geben wir einen 418-Code zurück.

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);
    }
}

Wir müssen nun noch eine Route registrieren. Und zwar in diesem Fall als Wildcard-Route. Hier ist vor allem wichtig, dass die Route als letztes registriert wird. Sonst überschreiben wir an dieser Stelle nämlich alle anderen Pfade. Ein positiver Nebeneffekt ist, dass wir nicht versehentlich eine bekannte Route als Honeypot markieren können. Selbst dann, wenn wir sie in der Konfiguration eintragen, wird Laravel immer die richtige Route bevorzugen. Unser Honeypot-Controller wird nur dann berücksichtigt, wenn kein anderer Controller zuständig ist.

use App\Http\Controllers\Honeypot;

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

Das wars dann auch schon. Unsere Falle ist gestellt. Wir müssen nun nur noch abwarten und die Früchte unserer Arbeit genießen. Es empfiehlt sich außerdem regelmäßig die Log-Dateien zu durchforsten und die honeypot.php zu aktualisieren. Wer nicht so lange warten möchte, kann auch selber tätig werden. Bei OWASP könnt Ihr eine Liste mit Scannern finden, die Ihr auf Eure Seite loslassen könnt. Die aufgerufenen Pfade könnt Ihr anschließend in die Konfiguration übernehmen.

Ein paar Worte zum Schluss

Eins sollte allen klar sein. Diese Maßnahme ist kein Ersatz für ein sicheres CMS/Framework oder was auch immer. Mit dieser Methode könnt Ihr einen Großteil der automatischen Scans auf Euer System verhindern. Aber natürlich nicht alle. Das liegt allein schon daran, dass solltet Ihr beispielsweise wirklich einen Datenbank-Dump herumliegen haben, unser Honeypot-Controller gar nicht tätig werden würde.

Was Ihr erreichen könnt, ist dass Ihr weniger ungewollten Traffic bekommt. Nicht mehr und nicht weniger. Außerdem könnt Ihr möglicherweise verhindern, dass Sicherheitslücken gefunden werden, die sonst entdeckt worden wären. Zum Beispiel, weil Scanner häufig zunächst gängige Pfade aufrufen. Es erhöht also Euer Sicherheitslevel. Aber es bietet keinen hundert-prozentigen Schutz. Das sollte Euch an dieser Stelle klar sein.