Eine Einführung ins Thema SQL-Injections findet ihr in unserem vorherigen Beitrag.
SQL-Injections sind vermutlich genauso alt wie die ersten Datenbanken. Trotz ihrer Bekanntheit und bewährten Gegenmaßnahmen stoßen wir aber immer wieder auf diese Schwachstelle.
SQL-Injections entstehen, wenn Benutzereingaben direkt in Datenbankabfragen verwendet werden. Dies kann ausgenutzt werden, um die Abfrage (fast) beliebig anzupassen, um so beispielsweise an Daten zu gelangen, die eigentlich nicht zugreifbar sein sollten.
Vergleichen kann man das mit einem Blankoscheck. Der Aussteller gibt die Kontrolle über die eingetragene Summe ab. Der Empfänger kann einen (fast) beliebigen Geldbetrag eintragen und diesen vom Aussteller-Konto abheben. In vertrauensvollen Hände ist das bequem – in den falschen Händen kann das aber fatale Folgen haben.
Die Ausgangslage
- Das Thema wurde nicht bedacht, d.h. es gibt keinen Schutz. Dann ist die Ausnutzung in der Regel einfach.
- Es werden vorgefertigte parametrisierten Abfragen korrekt verwendet, die vor SQL-Injection-Schwachstellen schützen, mehr dazu unten.
- Vor der Verwendung von Benutzereingaben werden eigene Versuche unternommen, die im Wesentlichen auf 2. abzielen, aber leider meist Lücken enthalten.
Im Folgenden geht es um den 3. Fall und den kreativen Prozess, an den eingebauten Hürden vorbei zur Ausnutzung der Schwachstelle zu kommen.
In Zeile 7 werden in den empfangenen Benutzereingaben, in diesem Fall username und password, die definierten Wörter entfernt. Die resultierenden Werte werden dann in Zeile 10 in die Datenbankabfrage eingesetzt und ausgeführt.
Typische Schwächen in implementierten Filtern
Der obige implementierte Filter ist nicht so effektiv, wie er auf den ersten Blick scheinen mag. Mehrere „Tricks“ können kombiniert werden, um schlussendlich eine erfolgreiche SQL-Injection durchzuführen.
Groß- und Kleinschreibung
Groß- und Kleinschreibung ist bei Datenbankabfragen in der Regel egal. SELECT kann also einfach durch select ersetzt werden. Der implementierte Filter beachtet aber die unterschiedlichen Schreibweisen nicht. Übrigens akzeptiert eine Datenbank auch eine gemischte Schreibweise, wie etwa SeLEct.
In manchen Fällen wird die Groß- und Kleinschreibung korrekt beachtet. Oft wird das Ersetzen der gefährlichen Wörter und Zeichen aber nur genau einmal angewendet. Was passiert also, wenn die Eingabe beispielsweise SELSELECTECT enthält? Nach dem Entfernen des Inneren SELECT bleibt selbiges übrig und passiert den Filter ungehindert, da dieser nicht erneut auf das Ergebnis angewendet wird.
Anführungszeichen escapen
Eine weitere Hürde im implementierten Filter ist das Ausschließen des einfachen Anführungszeichens (‚). Dieses wird in der Regel benötigt, um aus einem String-Kontext in der Datenbankabfrage auszubrechen, um so die eingeschleusten SQL-Schlüsselwörter zur Ausführung zu bringen. Die beiden vorherigen Tricks lassen sich auf das Anführungszeichen nicht anwenden.
Trotzdem lässt sich der Filter umgehen, indem man einen Backslash (\) nutzt, um das Anführungszeichen zu escapen. Der Backslash wird vom Filter nicht beachtet und bleibt in der Benutzereingabe deshalb erhalten. Escapen bedeutet, dass ein Zeichen als tatsächlich dieses Zeichen betrachtet wird und nicht als Teil der SQL-Abfrage interpretiert wird, um beispielsweise einen String-Kontext zu beenden. Der Text 'You\'re welcome.'
wird von der Datenbank also als String You're welcome.
verwendet.
Der Trick
Die Datenbankabfrage aus dem Beispiel hat die folgende Struktur:
SELECT * FROM users WHERE name = '<username>' AND password = '<password>'
Beim Aufruf des obigen Codes wählen wir die Parameter wie folgt:
username: MindBytes\ password: or 1=1 -- comment
Der implementierte Filter erkennt den Backslash (\) und das or
nicht und fügt beides in die Datenbankabfrage ein:
SELECT * FROM users WHERE name = 'MindBytes\' AND password = ' or 1=1 -- comment'
Für die Datenbank hat diese Abfrage jedoch die folgende Struktur:
SELECT * FROM users WHERE name = 'MindBytes\' AND password = ' or 1=1 -- comment'
Da das Anführungszeichen hinter MindBytes escapt wird, bekommt die WHERE-Bedingung eine unerwartete Form. Der Parameter password wird dann genutzt, um die Datenbankabfrage zu manipulieren. Hier muss man den SQL-Injection-Filter bedenken und beispielsweise die fehlende Erkennung von Groß- und Kleinschreibung zum eigenen Vorteil nutzen. Weiterhin wird das Ende der vordefinierten Datenbankabfrage durch einen Kommentar (–) unwirksam gemacht.
Die Datenbankabfrage gibt so alle Einträge aus der Tabelle users aus, da wir eine OR-Bedingung einfügen konnten, welche dazu führt, dass die WHERE-Bedingung immer wahr ist.
Über die gleichzeitige Manipulation der beiden Parameter username und password sind wir also am Filter vorbeigekommen.
Die Auswirkung
- Manipulation von Abläufen, bspw. Login ohne gültige Zugangsdaten.
- Unbefugter Zugriff auf Daten innerhalb derselben Datenbank oder weiteren verfügbaren Datenbanken.
- Je nach Art der verwundbaren Datenbankabfrage können Daten verändert oder gelöscht werden.
- Überlasten der Datenbank, beispielsweise durch Ressourcen-hungrige Abfragen, um die Verfügbarkeit zu stören.
- Je nach Konfiguration und Berechtigungen des Datenbankbenutzers können Datenbank-Funktionen missbraucht werden, welche das Lesen oder Schreiben von Dateien auf dem Dateisystem, das Ausführen von Betriebssystem-Befehlen oder das Stellen von Netzwerk-Anfragen, beispielsweise an interne Systeme, ermöglichen.
Die Lösung: Parametrisierte Abfragen
Parametrisierte Abfragen sind für die gängigen Datenbanken verfügbar und deren Verwendung werden von den geläufigen Programmiersprachen unterstützt. Dabei wird es der Datenbank selbst überlassen, die Benutzereingaben richtig zu verarbeiten.
Zunächst wird die Struktur der Datenbankabfrage definiert. An Stellen, an denen Benutzereingaben verwendet werden sollen, müssen Platzhalter eingefügt werden. Diese werden typischerweise durch Fragezeichen in der Datenbankabfrage dargestellt (Zeile 6). Eine solche Form der Abfrage wird auch parametrisierte Abfrage genannt.
Die parametrisierte Abfrage wird von der Datenbank intern zu einem sogenannten Prepared Statement umgewandelt und für die anschließende (mehrfache) Ausführung bereitgestellt. Die Struktur der Abfrage kann nun nicht mehr verändert werden. Bei der Ausführung des Prepared Statements werden die Benutzereingaben als Parameter für die definierten Platzhalter an die Datenbank übergeben (Zeile 7).