Es gibt viele Internetdienste, bei denen man sich mit Benutzernamen und Passwort anmelden muss. Das Formular sieht dann zum Beispiel so aus:
Total einfach: Man trägt seinen Benutzernamen (z. B. hszemi) und sei Passwort (z. B. geheim) ein, klickt auf “Anmelden” und wird auf die Startseite des “internen” Bereichs weitergeleitet.
Basics
Im Hintergrund läuft dabei in der Regel Folgendes ab:
Es gibt einen Datenbankserver mit einer Datenbank, in der sich eine Tabelle befindet, in der alle Benutzerkonten eingetragen sind: In der Spalte “benutzername” steht der jeweilige Benutzername und in der Spalte “passwort” jeweils das dazugehörige Passwort – allerdings nicht im Klartext1, sondern in der Regel wird eine Einwegfunktion verwendet (zum Beispiel MD5), die aus dem Passwort eine Prüfsumme berechnet, und lediglich die wird in der Datenbank gespeichert.
Das hat den Vorteil, dass jemand, der Zugriff auf die Datenbank hat, nicht die Passwörter sieht, sondern theoretisch so lange Wörter durch die Einwegfunktion jagen muss, bis zufällig einmal die richtige Prüfsumme herauskommt.
Die Benutzertabelle in der Datenbank sieht also etwa so aus:
ID | benutzername | passwort |
---|---|---|
1 | Safura | 4fdf7cf087cbf024d2e50a14256016f7 |
2 | Erna | 908fc4942e1f629d4c6629808da70165 |
3 | Hans | ccd5cc2741cd6347ac791e76e3062528 |
4 | Jean | b195d8b5aee9b903334bc8360099a90f |
Zurück zum Login-Formular. Wird auf den “Anmelden”-Knopf geklickt, so werden an den Webserver die Inhalte der beiden Textfelder gesendet. Dort wird dann geprüft, ob es eine Zeile in der Benutzertabelle gibt, in der in der Spalte “benutzername” der Text aus dem Formularfeld “Benutzername” steht UND in deren Passwortfeld der Wert steht, der herauskommt, wenn man die Einwegfunktion auf den Inhalt des Formularfelds “Passwort” anwendet.
Solche Datenbankabfragen werden gemeinhin in einer eigenen Sprache formuliert, der “Structured Query Language” oder kurz “SQL”. Das ist eine Art sehr einfaches Englisch. Eine einfache SQL-Anfrage hat folgende Struktur:
SELECT (Liste von Tabellenspalten oder einfach * für alle Spalten)
FROM (Liste von Tabellen, aus denen die Spalten kommen)
WHERE (Bedingungen, um die Zeilen auszuwählen)
Wenn sich nun also Hans mit dem Passwort hanspasswort anmelden will, müsste folgende Anfrage ausgeführt werden:
SELECT *
FROM benutzer
WHERE benutzername = ‘Hans’ AND passwort = MD5(‘hanspasswort’)2
Wenn die Anfrage eine Zeile als Ergebnis zurückliefert, hat Hans das richtige Passwort eingegeben und darf in den “internen” Bereich. Falls es keine Zeile in der Tabelle gibt, die beide Bedingungen erfüllt, hat Hans ein falsches Passwort eingegeben und muss draußen bleiben.
Der Angriff
Die SQL-Abfrage muss nun allerdings für jeden Anmeldevorgang dynamisch zusammengebastelt werden. Nehmen wir an, der eingegebene Benutzername steckt immer in der Variable $BENUTZER und das eingegebene Passwort in der Variable $PASSWORT. Dann könnte man folgende SQL-Abfrage ausführen:
SELECT *
FROM benutzer
WHERE benutzername = ‘$BENUTZER‘ AND passwort = MD5(‘$PASSWORT‘)
Das blöde dabei: So eine SQL-Abfrage ist erstmal nur ein gewöhnlicher String (heißt eine Zeichenkette). Man könnte jetzt als Benutzernamen folgendes eingeben:
Safura’ OR ‘1’ = ‘1
Dann wird aus dem WHERE-Teil (der die Auswahl der Zeilen bestimmt) folgendes:
WHERE benutzername = ‘Safura’ OR ‘1’ = ‘1’ AND passwort = MD5(‘…’)
Da in SQL eine UND-Verknüpfung (AND) vor einer ODER-Verknüpfung (OR) ausgewertet wird und ‘1’ = ‘1’ logischerweise immer den Wert WAHR hat, werden plötzlich alle Zeilen zurückgeliefert, in denen entweder der Benutzername gleich Safura oder das eingegebene Passwort hinterlegt ist, oder beides.
Das heißt, mit diesem Trick kann man sich als jeder beliebige Nutzer anmelden, ohne das Passwort zu kennen. Alternativ kann man auch einen Quatsch-Benutzernamen angeben und als Passwort häufig genutzte Passwörter durchraten und wird bei einem Treffer als der Benutzer angemeldet, dessen Passwort man erraten hat – ohne zuvor den Benutzernamen zu kennen.
Warum funktioniert das?
Beim Zusammenbasteln der Anfrage oben wurden die Benutzereingaben direkt verwendet. Der eingegebene Benutzername wurde dann so gewählt, dass er zur Grundstuktur der SQL-Anfrage passt, aber ein anderes (vom Angreifer gewünschtes) Ergebnis zurückgeliefert wird. Durch die Anführungszeichen wird der eigentliche Benutzername beendet und das folgende OR wird als SQL-Schlüsselwort (statt als Inhalt des Benutzernamens) behandelt – mit fatalen Folgen.
Was tut man dagegen?
Eine der wichtigsten Regeln beim Programmieren: Vertraue gar niemals nicht irgendwelchen Benutzereingaben. Dass im obigen Beispiel der eingegebene Benutzername ohne vorherige Prüfung einfach so in die SQL-Abfrage eingebaut wird, ist grob fahrlässig – was leider nicht heißt, dass so etwas in der freien Wildbahn nicht vorkommt.
Für Datenbankabfragen die Variablen enthalten sollte man wo möglich Prepared Statements verwenden, die sich darum kümmern, dass eine Zahl wirklich eine Zahl und ein String wirklich ein String bleibt, und entsprechende problematische Zeichen zuverlässig herausfiltern.
Falls dafür ein Umbau der gesamten Webanwendung nötig und dieser zu aufwändig wäre, sollte man als Sofortmaßnahme auf jeden Fall die Benutzereingaben vorfiltern. PHP bietet hier zum Beispiel die Funktion mysqli_real_escape_string() an, die das übernimmt.
Die “abgehärtete” Datenbankabfrage sähe dann so aus:
SELECT *
FROM benutzer
WHERE benutzername = ‘mysqli_real_escape_string($BENUTZER)‘
AND passwort = MD5(‘mysqli_real_escape_string($PASSWORT)‘)
Fazit
Nun gut, diese Sicherheitslücke ist also bei uns drin.
Die Reparatur würde aber Geld und/oder Zeit kosten, deshalb machen wir da nix. Das weiß schließlich keiner dass die Lücke da ist und wie man sie ausnutzt weiß außer Ihnen auch niemand.
Ääh ja. SQL-Injections gehören zum kleinen 1×1 eines jeden Skriptkiddies. Gutes Gelingen 🙂
- Wobei es das auch gibt. Sollte man das machen? ↩
- MD5(‘hanspasswort’) berechnet in diesem Fall den Wert der Einwegfunktion für die Eingabe ‘hanspasswort’ ↩