Worum geht's hier?

In zwei Jahrzehnten als Vollzeitnerd hat sich der ein oder andere Text angesammelt, mit denen ich unseren Azubis versucht habe, Grundlagen über unseren Job (TCP/IP, aber auch bevorzugt Telefonie) zu erklären.

Das stelle ich hier, wann immer ich so einen Text nochmal irgendwo finde, zusammen. Diese Texte haben nicht den Anspruch, zu 100% exakt zu sein (sondern manches ist des besseres Verständnisses wegen vereinfacht worden). 

Heute:

SQL-Injections und ähnliche Angriffe

Eine SQL-Injection ist ein Angriff, bei dem man von außen (z.B. auf einer Webseite) versucht, Schadcode in eine durchgeführte Datenbank-Abfrage (SQL = structured query language, also eine Sprache, in der man Datenbankabfragen formuliert) zu schleusen (zu injizieren).

Weil ein Video mehr sagt als tausend Worte, werfen wir einen Blick auf die Simpsons:

  • Bart Simpson ruft in Moes Taverne an und bittet, darum Herrn Reinsch sprechen zu dürfen
  • Moe legt den Hörer zur Seite und ruft in seine Bar: "Hört mal her: Gibt es hier jemanden, der Reinsch heißt?"
  • Die Gäste verstehen etwas anderes und lachen Moe aus.

Das ist das Grundprinzip einer SQL-Injection: Ein Angreifer bringt die Webanwendung "Moe" dazu, intern eine Frage zu stellen, die er für harmlos hält, die aber vom Empfänger (den Gästen, im realen Angriff: der Datenbank) anders aufgefasst wird. 

Nun erscheint das nicht sofort gefährlich. Aber fragen wir nach dem gleichen Muster mal, ob Herr "Dasfin Anzamtbesch" da ist. Moe wird auch diese Frage an seine Gäste weitergeben, und sie werden hören: "Ist hier jemand, der das FinAnzamt bescheißt?". 

Das gefährliche an diesem Angriff ist, dass die Gäste ja nicht wissen, dass die Frage von draußen von einem Fremden kommt. Sondern Moe stellt sie ihnen - und sie vertrauen Moe, werden ihm also antworten.

Im Unterschied zur Taverne sind IT-Produkte meistens humorlos. Das heißt, wenn Moe fragt, wer gegenüber dem Finanzamt schummelt, werden sie nicht lachen, sondern jemand wird sich ehrlich melden. Und schon hätte ein Angreifer (Bart Simpson) eine Information erhalten, die ihn eigentlich gar nichts anging.

Übrigens könnte Bart Simpson am Telefon auch nach Herrn "Müller heißt? Alle anderen trinken bitte, bis Ihr in die Hosen sch" fragen. Der Moe aus der Serie würde spätestens dann stutzig werden. Eine schlecht programmierte Website aber würde auch diese Frage dann an die Datenbank stellen: "Hört mal her: Gibt es hier jemand, der Müller heißt? Alle anderen trinken bitte, bis Ihr in die Hosen scheißt" – und damit, jetzt wird es richtig gefährlich, kann ein Angreifer auch Moe dazu bringen, seinen Gästen Anweisungen zu erteilen.

Von den Simpsons hin zu Webanwendungen

Nachdem das Prinzip hoffentlich verstanden ist, gucken wir uns das jetzt mal technisch an. Stellen wir uns mal vor, Ihr wollt in mein CMS (also die Software, mit der ich diese Webseite hier verwalte) einbrechen. Dann sucht Ihr Euch ein Eingabefeld, z.B. unten die Volltextsuche.

Wenn man da als Suchbegriff Simpson eintippt, macht das CMS hintenraus folgende Datenbankabfrage (stark vereinfacht):

SELECT * FROM article WHERE body LIKE "%Simpson%" AND hidden=0 AND published<NOW()

Also: Hole alle Infos aus der Artikel-Tabelle, und zwar zu allen Datensätzen, bei denen das Feld "body" dem Suchbegriff %Simpson% entspricht (und % ist ein Platzhalter für alles mögliche), die im Feld hidden eine Null stehen haben (also die nicht versteckt=deaktiviert wurden), und die als Published-Zeitpunkt ein Datum in der Vergangenheit (kleiner als jetzt) stehen haben.

Jetzt geben wir als Suchbegriff mal gedanklich ein: " AND hidden=1#

Und da würde dann was draus passieren:

SELECT * FROM article WHERE body LIKE "%" AND hidden=1#%" AND hidden=0 AND published<NOW()

Durch das erste " in unserem Suchbegriff beenden wir den Suchstring (d.h. der Suchbegriff heiß jetzt: body LIKE "%" und würde jeden Artikel finden). Aber weil der Suchstring jetzt zuende ist, können wir danach SQL-Schlagworte einbauen. Zum Beispiel: AND hidden=1 - also als weitere Bedingung: Nur Artikel, die vesteckt sind und nicht gezeigt werden dürfen.

Und dahinter haben wir noch eine # gepackt. Die ist eigentlich gedacht, um Kommentare einzuleiten, d.h. das, was dahinter kommt, wird ignoriert, weil es ja nur Beschriftung des Programmierers ist. Hinter der Raute landet jetzt das vom CMS-Programmerer eingebaute %" AND hidden=0 AND published<NOW() - dadurch, dass der Datenbankserver das ignoriert, haben wir zwei Fliegen mit einer Klappe geschlagen: Wir müssen uns nicht überlegen, wie wir das %" noch irgendwie in unsere Datenbankabfrage einbauen können (denn einfach so unmotiviert hinten drankleben darf es nicht, sonst gäbe das eine Fehlermeldung). Und wir haben die Bedingungen des Programmierers ("zeige nichts, was noch nicht freigegeben ist") abgeschnitten.

Im Endergebnis zeigt uns unser CMS jetzt ausschließlich genau die Artikel, die gar nicht angezeigt werden dürfen.

Passwort? Ist nur für Anfänger

Was beim Suchbegriff geht, geht natürlich auch bei der Passwort-Abfrage, wenn ich mich als Redakteur an diesem CMS anmelden will.

Im Normalfall gibt man als Username admin und als Passwort geHEiM ein, und das CMS guckt dann in der Datenbank nach, wer ich bin:

SELECT * FROM users WHERE username="admin" AND password="geHEiM"

Bei einem schlecht programmierten CMS kann ich aber als Username auch admin"# eingeben und das Passwort leer lassen. Oder Username admin und als Passwort MirDochEgal" OR admin = "1

Und dann wird daraus:

SELECT * FROM users WHERE username="admin"#" AND password=""
SELECT * FROM users WHERE username="admin" AND password="MirDochEgal" OR admin="1"

Im ersten Query muss nur noch der Benutzername passen. Der Teil, in dem das Passwort verglichen wird, wurde mit der # abgeschnitten und wird ignoriert. Im zweiten Beispiel lautet die Bedingung jetzt "entweder das Passwort lautet "MirDochEgal" oder der user hat im Feld "admin" eine 1 stehen, also Admin-Berechtigung. Solange eines von beiden erfüllt ist, wird der Login erfolgreich sein.

Wer lesen kann, der kann auch schreiben

Nachdem das Prinzip verstanden ist, denken wir das noch einen Schritt weiter. Damit das nicht zu eintönig ist, gucken wir uns zunächst nach Such- und Login-Eingabefeldern noch etwas ganz anderes an, nämlich die URL.

In der Browser-Adressleiste dieses Artikels steht irgendwas vom Typ vollzeitnerd.de/erklaerbaer/sql-injections oder so. Schön und gut, aber wir ITler speichern ja alles in der Datenbank, auch dieses CMS hier macht das so, und so kann ich die Seite auch aufrufen mittels vollzeitnerd.de/index.php?id=52

Und wenn ich das eine oder das andere mache, dann passiert im Hintergrund eines der beiden folgenden:

SELECT * FROM pages WHERE page_uri="erklaerbaer/sql-injections" AND hidden=0 AND pushlished<NOW()
SELECT * FROM pages WHERE id=52 AND hidden=0 AND pushlished<NOW()

Es versteht sich von selbst, dass auch hier der gleiche Angriffsversuch möglich ist, in dem ich z.B. vollzeitnerd.de/index.php?id=1%3BUPDATE%20users%20SET%20password%3D1234%3B%23 aufrufe.

Das sieht etwas komplizierter aus, ist aber normales URL-Encoding (weil Sonderzeichen in einer Browser-URL nichts verloren haben, müssen sie codiert werde). Wenn man das zurück-codiert, dann steht da vollzeitnerd.de/index.php?id=1;UPDATE users SET password=1234;#

Und wenn man sich das als SQL-Query in meinem CMS vorstellt, dann wird daraus:

SELECT * FROM pages WHERE id=1;UPDATE users SET password=1234;# AND hidden=0 AND pushlished<NOW()

Und der SQL-Server versteht jetzt daraus:

  • Zuerst ein SELECT-Kommando mit nach Seiten-Id 1
  • Dann ein Semikolon - es kommt ein zweiter SQL-Befehl
  • Dann ein UPDATE-Befehl (also eine Änderung) auf die Tabelle users, und zwar wird das Passwort auf 1234 gesetzt. Ohne weitere Bedingungen heißt: Für alle User
  • Und dann eine #, d.h. der Teil dahinter wird ignoriert.

Mein CMS wird zwar eine Fehlermeldung anzeigen, weil es die Seite nicht finden kann. Aber die Passwörter aller Redakteure und Admins sind danach auf 1234 gesetzt. 

Vergleichbare Angriffsvektoren - Cross-Site-Scripting (XSS) und Co

Die o.g. Beispiele greifen immer auf Datenbanken ab. Das Grundprinzip lässt sich aber auch an vielen anderen Stellen anwenden, z.B. beim sog. Cross-Site-Scripting (XSS), sozusagen HTML-Injection.

Bleiben wir mal bei unserem Suchformular auf der Webseite und stellen wir uns mal vor, was passiert, wenn man da draufklickt:

Auf der Suchseite:

Auf der Ergebnisseite:

Es wurden leider keine Beiträge zu "Kaninchenfell" gefunden.

Und jetzt stellen wir uns das Suchfeld ein bisschen größer vor (nur Optik, damit man es besser lesen kann) und fügen da HTML- bzw. JavaScript-Code ein. Und stellen uns mal vor, was dann aus der Ergebnisseite wird:

Auf der Suchseite:

Auf der Ergebnisseite:

Es wurden leider keine Beiträge zu " <script>
alert("Diese Seite wurde gehackt");
</script>
" gefunden.

Das Prinzip dürfte klar sein, was ich vorne als Suchbegriff reinkippe, kommt hinten auf der Ergebnisseite wieder raus. Nun habe ich in diesem Beispiel die <script>- und </script>-Tags im Browser bewusst als Text dargestellt, damit man sie sieht. Standard (wenn der Programmierer seinen Job nicht ordentlich macht) wäre aber, dass das so als HTML-Inhalt an den Browser geht (und der es wirklich als JavaScript auffasst und ausführt).

Damit daraus jetzt ein Sicherheitsloch wird, braucht man noch ein bisschen mehr Phantasie:

  1. Man muss das Opfer und seinen Browser dazu bringen, das Formular mit dem Schadcode zu füllen und abzuschicken, ohne dass ihm klar ist, was er da tut. Klingt aufwendig? Nö: Klick einfach den unteren der beiden o.g. Go-Knöpfe an (der mit dem fingierten Suchbegriff-Beispiel) und ich bringe Deinen Browser dazu, in Deinem Namen bei Google irgendwas zu suchen. Einfach genug? 
  2. Man muss als Schadcode dann mehr als nur ein alert("Diese Seite wurde gehackt") übermitteln. Ja okay, das ist nur eine Frage der Phantasie. Übrigens geht auch ein <script src="https://irgend-eine-url"> und Dein Browser lädt sich notfalls megabyte-weise Schadcode aus dem Netz nach.
  3. Dieser Schadcode wird dann im Namen des Opfers in dessen Browser ausgeführt. Und zwar ausgeliefert durch den Server der angegriffenen Webseite (in diesem Beispiel mein CMS, in dem ja die Suchfunktion missbraucht wird), d.h. er genießt innerhalb der Browser-Sitzung das größtmögliche Vertrauen. 

Was kann man damit schönes machen?

  • Session Hijacking: Sagen wir mal, das Opfer ist Redakteur und im CMS eingeloggt. Dann speichert der Browser irgendwelche Zugangstokens (i.d.R. als Cookies), damit das CMS ihn bei jedem Klick erkennt und sich erinnert "ach ja, der ist das". Diese Tokens kann man per JavaScript auslesen und wegtransportieren, so dass sich der Angreifer an einem anderen PC als sein Opfer ausgeben und in dessen Namen im CMS weiterarbeiten kann.
  • Sessions am Leben halten. Das Opfer denkt, es hat sich vor der Mittagspause ausgeloggt. Aber in Wirklichkeit wurde per JavaScript-Schadcode die Logout-Funktion gestört und im Hintergrund weiter Kontakt mit dem CMS gehalten (damit kein automatischer Logout nach Zeit erfolgt).  
  • Andere URLs im gleichen CMS aufrufen, die irgendwas ausführen. Z.B. Klicks auf "Einstellungen -> Benutzerverwaltung -> neuen User anlegen -> Passwort 1234" simulieren, und schon hat das Opfer, ohne es zu merken, einen neuen Redakteur angelegt. 
  • Das Verhalten des so kompromittierten CMSes ändern, z.B. andere Dinge anzeigen als da in Wirklichkeit eingestellt sind. Der "Klassiker" ist z.B., wenn das kein CMS-, sondern vielleicht eine Online-Banking-Anwendung ist, dass das Opfer im Feld Betrag den Wert "123,45 Euro" sieht und im Feld IBAN die von ihm eingetipppte Bankverbindung, aber in Wirklichkeit 123.450 Euro an eine ausländische IBAN zur Bank geschickt wird. JavaScript macht's möglich, und der Browser wird es nicht hinterfragen, denn er bekommt diese Anweisungen ja nicht von einem Angreifer, sondern direkt vom Server der z.B. Online-Banking-Anwendung.  

Noch mehr Einfallstore gefällig?

Überall, wo Daten von Dritten kommen, muss man davon ausgehen, dass die potentiell Ärger machen. Das gilt für Autokennzeichen genauso wie für den Namen des Nachwuchs

Selbst in Papierform ist man davor nicht sicher: Meine Firma digitalisiert ihre Eingangspost samt Texterkennung. Der Brief wird in einer Datenbank gespeichert (=SQL-Injection) und ist via Ticketsystem für Mitarbeiter im Browser sichtbar (=XSS), für beides wäre also theoretisch ein Einfallstor für o.g. Angriffstypen und gehört daher genauso abgesichert wie ein Webformular o.ä.

Injections sind aber auch in Dateinamen möglich: Ein Beispiel:

  • Ich habe eingeschränkten Zugriff auf eine Webseite, z.B. weil ich in einem Forum angemeldet bin, oder weil ich per XSS die Session eines Redakteurs in irgend einem CMS geklaut habe
  • Ich lade dort ein Bild hoch und benenne es um in bild1&rm\ -rf\ .&.jpg - zugegeben, kein besonders schöner Name, aber wenn das CMS unsauber programmiert ist und sowas zulässt, Prost Mahlzeit. 
  • Jetzt führe ich eine Größenänderung durch. Das CMS nutzt dafür z.B. ImageMagick auf dem Server, d.h. führt einen Befehl aus in der Form
    /pfad/zu/im4 bilder/NameDerBilddatei fertig/1234567.jpg --resize=120x80
  • Mit meinem Bild-Dateinamen wird daraus: 
    /pfad/zu/im4 bilder/bild1&rm\ -rf\ .&.jpg fertig/1234567.jpg --resize=120x80
  • Und das Linux des Webservers wird die beiden &-Zeichen erkennen (ein Zeichen für parallel auszuführende Kommandos) und interpretiert das als drei Befehle: Der erste Befehl,
    /pfad/zu/im4 bilder/bild1 wird eine Fehlermeldung liefern, weil ImageMagick nicht versteht, was es tun soll. Der letzte Befehl 
    .jpg fertig/1234567.jpg --resize=120x80 wird fehlschlagen, weil es überhaupt kein Befehl ist. Aber der mittlere Befehl, er lautet 
    rm -rf . (die \ dienten nur zur Maskierung, also dem Transport der Leerzeichen) wird mein komplettes CMS auf der Serverfestplatte löschen.

Zu guter letzt kann es sogar passieren, dass durch ein bloßes Fax (Bildübertragung) innerhalb des Faxgerätes Schadcode eingeschleust werden kann, wie man hier nachlesen kann. (Ganz davon abgesehen, dass bei digitalisierten Faxen mit Texterkennung die gleiche Vorsicht geboten ist wie bei unserer o.g. digitalisierten Eingangspost).

Abhilfe

SQL- und andere Injections entstehen nicht als Naturgewalt. Sondern die treten auf, wenn schlampig gearbeitet wird.

Die erste Regel muss lauten: Traue keiner Information, die von draußen kommt. Erst recht nicht vom Browser (über den Du keine Verfügungsgewalt hast), aber wie man sieht auch keinem z.B. Dateinamen (wenn Du ihn nicht selbst vergeben hast) und keiner Texterkennung (wenn der erkannte Text von draußen kam), einfach niemandem.

Und wenn Du weißt, dass Du der Information nicht vertrauen kannst, dann überprüfe ("validate") und reinige ("sanitize") sie. 

Überprüfen/Validieren heißt, Du überlegst Dir welche Art von Zeichen an dieser Stelle überhaupt nur erlaubt sind. Und wenn Du was anderes findest, brich den Vorgang ab. Zurück zum ersten Beispiel in Moes Taverne - mindestens das "Müller heißt? Alle anderen trinken bitte, bis Ihr in die Hosen sch" wäre selbst bei Moe durch die Validierung durchgefallen, denn bei dem Namen eines Gastes haben weder 11 Leerzeichen, noch ein Fragezeichen, noch ein Komma ewas zu suchen. 

Ein paar Zeilen weiter drunter, als es um die Suche ging: Simpson als Suchbegriff ist ok, aber " AND hidden=1# ist es nicht - Anführungszeichen oder Rauten haben in einem Suchfeld nichts verloren. Aber wichtig: Wir müssen grundsätzlich vom Bösen ausgehen. Die Prüfung darf also nicht lauten "wenn eine Raute oder ein Anführungszeichen im Suchbegriff ist, dann brich ab" - den Kampf wirst Du verlieren, es gib zu viele böse Sonderzeichen.

Sondern die Prüfung muss lauten: "Besteht der Suchbegriff aus maximal z.B. 15 Zeichen, und zwar ausschließlich Groß- und Kleinbuchstaben, Leerzeichen und Ziffern?" Wenn ja, dann darfst Du ausnahmsweise davon ausgehen, dass der Suchbegriff ok ist und weiter mit ihm arbeiten. Das ist lästig - und bereits dieses Wort "lästig" würde zum Problem führen, der Umlaut wird bei einer klassischen A-Za-z0-9-Prüfung rausfliegen, d.h. Du musst ein bisschen mehr Gehirnschmalz reinstecken. Aber es geht um das Überleben Deiner Webanwendung!

Und wenn Du es überprüft hast, dann reinige es zur Sicherheit noch zusätzlich, bevor Du es an ein anderes System schickst. Bei der Arbeit mit PHP/MySQL z.B. gibt es Befehle rund um "mysqli_real_escape_string", mit denen Du die Werte passend zu Deiner Datenbank-Konfiguration umschreiben kannst. Also PHP/MySQL kümmert sich dann darum, dass nicht username="admin"#" an die Datenbank geschickt wird, sondern dass die Benutzereingabe admin"# korrekt maskert wird - in der Regel kommt ein Backslash \ davor, der soviel sagt wie "jetzt komm ein Zeichen, dass Du bitte als Zeichen und nicht als Kommando verstehen sollst". Und dann wird da username="admin\"\#" draus und schon ist das unschädlich - wenn man es richtig macht (darum der Verweis auf die PHP-eigegenen Funktionen dazu).

Reinigen kannst Du auch anders (und zu viel reinigen schadet auch nicht). Wenn Du z.B. mit WHERE id= ohnehin nach einem nummerischen Wert fragst, tut es keinem weh, wenn Du den vorher mit z.B. der PHP-Funktion intval() in eine Zahl konvertiert hast. Wenn dann trotz vorheriger Validierung da irgend ein Unsinn drinstand, macht Dir intval() im Zweifelsfall eine 0 draus.

Und reinigen kannst Du auch, in dem Du den Suchbegriff gar nicht erst transportierst. Mein Beispiel WHERE username="admin" AND password="geHEiMwar in sofern rein zu Erklärungszwecken, weil hoffentlich nirgendwo mehr Klartext-Passwörter in der Datenbank stehen. Man vergleicht Hashwerte. Was spräche dagegen, das auch beim Usernamen zu vergleichen? Einfach in PHP die Benutzereingabe mit dem PHP-Kommando md5() umrechnen in 21232f297a57a5a743894a0e4a801fc3, und dann machst Du im Query einfach WHERE MD5(username) = "21232f297a57a5a743894a0e4a801fc3"

Wenn jetzt jemand meint, admin"# als Username eingeben zu müssen, kommt am Datenbankserver nur an: WHERE MD5(username) = "2464876fd856554a09ce3e05c281988c" und das ist völlig unschädlich. In diesem Fall geht es nicht um die kryptographische Sicherheit (bei Passwörtern wäre unsalted MD5 ja tabu), sondern es geht nur darum, eine Zeichenkette zwecks Vergleich in einen unschädlichen String umzuwandeln. Und da ist ein MD5-Vergleich prima geeignet.

Nächste Empfehlung: Minimiere das Risiko. Du kannst keine Webanwendung bauen, die 100% sicher ist. Wenn Du Dir das verinnerlichst und davon ausgehst, dass es eine Sicherheitslücke gibt, dann halte den Schaden so gering wie möglich:

  • Gib den Nutzern, auch Dir, so wenig Rechte wie nötig. Eine XSS-Attacke mit Session-Hijacking verursacht viel weniger Schaden, wenn der Nutzer zu den Zeitpunkt nur mit einfachen Redakteurs-Rechten eingeloggt war (also auch der Angreifer keinen Admin-Account ergattert)
  • Gib den von Dir benötigten z.B. SQL-Usern so wenig Rechte wie nötig. Wenn ein Angreifer direkt (SQL-Injection) oder auf Dauer (weil er sich per XSS einen Admin-Account ergattert hat) Datenbank-Queries ausführen kann, dann macht er das mit den Rechten des Systems, in das er eingedrungen ist. Wenn Du Glück hast, hatte dessen Datenbank-Nutzer nur Rechte auf das CMS (und auf Datei-Ebene das gleiche). Dann ist das CMS danach Müll (Du musst es als irreparabel kompromittiert betrachten), aber eben nur das CMS, der Angreifer war darin aber eingesperrt. Wenn der gleiche Datenbank-User aber Zugriff auf andere Datenbanken von Dir hatte und/oder auch auf andere Verzeichnisse auf dem Webserver, dann...

Und zuletzt: Nutze auch serverseitige Überwachungen (z.B. gibt es mod_security als Webserver-Modul, das "gefährliche" Formulareingaben, URL-Aufrufe usw. direkt verhindert). Aber verlasse Dich nicht darauf, denn nur Du kannst beurteilen, was wirklich in einem Query erlaubt sein darf und was nicht.

Feedback