Sicherheitslücken im Web – Teil 1

Nicht erst seit dem NSA-Skandal ist Security wieder ein Thema. Erst kürzlich wurde vom Ponemon Institute eine Umfrage veröffentlicht in der 73 Prozent der befragten Unternehmen zugaben mindestens einmal in den letzten zwei Jahren gehackt worden zu sein. Da ein Großteil der Sicherheitslücken auf dem Web Application Layer zu finden ist, wollen wir in den nächsten Ausgaben einige der Hauptprobleme aufzeigen und Lösungen dafür anbieten.

Die zehn größten Sicherheitslücken im Web – Teil 1

Doch was sind denn die wichtigsten Sicherheitsprobleme? Dieser Frage hat sich das Open Web Application Security Project (kurz OWASP, sie dazu auch den gleichnamigen Kasten) gewidmet, die bisher alle drei Jahre eine Top-10-Liste der riskantesten Sicherheitslücken erstellt hat.

Die aktuelle Version wurde 2013 veröffentlicht und um nicht nur aufzuzeigen wie groß denn der technische Impact ist, wurde eine Einstufung nach Risiken vorgenommen. Hier ist unter anderem dann auch die wirtschaftliche Sicht berücksichtigt:

OWASP OWASP ist eine Non-Profit-Vereinigung mit verschiedenen Chapters, meist nach Ländern organisiert und über den ganzen Globus verteilt. Zudem hat zum Beispiel der Germany Chapter weitere Unterteilungen in diverse lokale Gruppen, wie z. B. im Rhein-Main-Gebiet oder Hamburg. Diese treffen sich dann in regelmäßigen Abständen und tauschen sich über aktuelle Sicherheitsthemen aus. Die meisten Mitglieder kommen bisher aus der Java-Welt, aber auch PHP-Entwickler sind herzlich willkommen. Wir werden dann zunächst zwar erst belächelt, aber es ist unsere Aufgabe, die Fahne hochzuhalten und aufzuzeigen, dass uns das Thema ernst ist! Neben dem riesigen Wiki auf der OWASP-Website mit über 1 000 Mitgliedern gibt es auch zahlreiche Mailing-Listen zu diversen Themen, professionell produzierte Videos von Talks und veranschaulichte Erklärungen der Sicherheitslücken sowie Events, die gemeinnützig veranstaltet werden. Die bekannteste europäische Veranstaltung ist dabei sicherlich die AppSecEU, die 2013 in Hamburg stattfand und im Juni 2014 in Cambridge zu Gast sein wird. Die Liste der Speaker ist dabei international und sehr hochkarätig besetzt. Die OWASP arbeitet an einer ganzen Liste von Projekten, wie z. B. einem XSS-Tool, dem Zed Attack Proxy oder Webgoat, eine Webanwendung, die beabsichtigt unsicher programmiert wurde, um so eine praktische Anleitung zu bieten, sichereren Code zu schreiben.

Neben der globalen Top-10-Liste gibt es auch noch einen Ableger für Mobile Development, der sich speziell an Produzenten für mobile Apps richtet und aktuell ist eine OWASP Top 10 für Entwickler in Arbeit, die sich speziell an uns richtet und u. a. auch mit mehr technischem Background und Codebeispielen in möglichst vielen Sprachen aufwarten soll. Den ersten Sicherheitslücken der OWASP Top 10 wollen wir uns nun in diesem ersten Beitrag einer Serie über mehrere Ausgaben widmen.

Injection

SQL Injections sind inzwischen fast jedem Entwickler ein Begriff und die Statistiken zu erfolgreichen Angriffen auf diesem Gebiet sind zum Glück rückläufig, aber leider immer noch hoch. Deswegen soll das Thema dennoch angeschnitten werden, bevor wir uns weiteren (Nicht-SQL-)Injections widmen.

Ein großes Problem bei SQL Injections ist, dass der SQL-Interpreter zunächst nur alle Strings bis zum Auftreten eines Semikolons verarbeitet und diese dann direkt ausführt, ohne den nachfolgenden String zu überprüfen. Nehmen wir folgendes Beispiel an, in dem die E-Mail-Adresse eines Users aktualisiert werden soll:

$sql = 'UPDATE users
SET email = "'.$email.'"
WHERE id=123';

Wird nun der Parameter $email nicht weiter validiert, kann über diesen beliebiger Code an den SQL Server geschickt werden und damit Daten manipuliert, gelöscht oder ausspioniert werden. Die doppelten Anführungszeichen sollen eigentlich den Werteteil der E-Mail-Adresse abtrennen, aber natürlich kann auch dieses Anführungszeichen entsprechend in der Variable stehen und somit seinen vorbestimmten Scope verlassen.

Angenommen in der Variable $email stehen nur zwei Zeichen: Ein Anführungszeichen und ein Semikolon. So sieht der String, der an den SQL Server gesendet wird, folgendermaßen aus:

    UPDATE users
SET email="";"
WHERE id=123

Und dies ist zumindest bis zum Semikolon vollkommen valider SQL-Code, der aber leider nicht die E-Mail-Adresse des Users 123 aktualisiert, sondern sämtliche E-Mail-Adressen aus der Tabelle users entfernt, da das Statement nach dem Semikolon zu Ende ist. Dass danach invalider SQL-Code steht, interessiert den Interpreter in dem Moment noch gar nicht. Zudem kann natürlich auch dies durch eine größere Manipulation der $email-Variable gelöst werden, sodass offensichtlich nicht mal ein Fehler auftritt und auch ein entsprechender Logging-Mechanismus nichts von dieser Manipulation mitbekommt. Denken Sie z. B. an folgende Manipulation:

        
$email = ‚";UPDATE users SET email="' . $email;

Dies würde sogar alle E-Mail-Adressen aus der Tabelle löschen und keinerlei SQL-Fehler erzeugen, aber die ursprüngliche Änderung der einen E-Mail-Adresse dennoch ausführen, sodass der Angriff noch weniger auffällt. Abbildung 1 zeigt, wie der aktuelle Response-Header von reddit.com versucht, eine SQL Injection bei entsprechenden Crawlern auszulösen.

Abb. 1: Der aktuelle Response-Header von reddit.com versucht, eine SQL Injection bei entsprechenden Crawlern auszulösen

Abb. 1: Der aktuelle Response-Header von reddit.com versucht, eine SQL Injection bei entsprechenden Crawlern auszulösen

Neben dem Löschen von Daten können diese natürlich auch einfach nur manipuliert werden. So können z. B. sensitive E-Mails an einen anderen Empfänger geschickt oder Passwörter überschrieben werden. Die gefährlichste Attacke ist dabei immer diejenige, die zunächst gar nicht entdeckt wird.

Aber nicht nur die Datenbank selbst ist bei einer SQL Injection in Gefahr, so können über die MySQL-nativen Funktionen LOAD DATA INFILE und INTO OUTFILE sogar Dateien auf dem Dateisystem gelesen und manipuliert werden. Dies kann den Angriffsvektor dann extrem vergrößern und die ganze Website, im schlimmsten Fall der ganze Server, könnte heimlich Daten an Dritte senden.

Usermanagement

Erster Schutzmechanismus ist dabei natürlich, die Rechte der SQL-User entsprechend einzuschränken. Es ist zwar ungemein praktisch, mit einem gut ausgestatteten Superuser Datenbanken zu verwalten, der nahezu überall schreiben kann. Aber genauso gefährlich ist dieses Vorgehen auch. Insbesondere, wenn die Webanwendung nur auf eine Datenbank Zugriff benötigt, evtl. sogar nur auf einen Teil der Tabellen und hier auch nur Daten liest, statt schreibt. Je nach Anwendungsszenario kann dies natürlich in einer Vielzahl an diversen Datenbankusern enden, aber Ihre Endanwender werden es Ihnen danken. Es sollte also mindestens ein User pro Datenbank und Webanwendung existieren. Und hier muss genau abgewägt werden, in welchen Tabellen überhaupt Schreibrechte existieren müssen.

Selbstverständlich sollte auch jeweils im ACL der Webanwendung selbst geprüft werden, ob der aktuelle Client überhaupt den entsprechenden Datensatz lesen oder vielleicht sogar manipulieren darf. Hier gibt es in den diversen Frameworks schon sehr gute Implementierungen, die einem helfen, sehr schnell, einfach und auch sicher ein entsprechendes Rechte-/Rollensystem zu etablieren.

Aufmacherbild: <a href=“http://www.istockphoto.com/vector/staircase-16556481? title=“Staircase – Illustration von Shutterstock / Uhrheberrecht: gavni “ class=“elf-external elf-icon elf-external elf-icon“ rel=“nofollow nofollow“>Staircase – Illustration von Shutterstock / Urheberrecht: gavni [ header = Seite 2: Time-base SQL Injection ]

Time-base SQL Injection

Doch auch wenn die Datenbankrechte entsprechend eingeschränkt wurden und das ACL die Ausgabe von sensitiven Daten sogar verhindert, gibt es für die Angreifer eine inzwischen weit verbreitete Möglichkeit, Daten über einen anderen Weg auszulesen. Nehmen wir einmal das SQL-Statement aus Listing 1 zur Verdeutlichung.

    SELECT IF(
         SUBSTRING(
           user_password, 1, 1
         ) = CHAR(68),
         BENCHMARK(
           5000000,
           ENCODE('foo', 'bar')
         ),
         null
  )
  FROM users
  WHERE id = 123;

Im äußeren Block wird eine SELECT-Abfrage auf den User mit der ID 123 ausgeführt. Der spannende Teil passiert in den selektierten Spalten. Hier wird zunächst eine IF-Bedingung gestartet. Im ersten Abschnitt wird via SUBSTRING(user_password, 1, 1) das erste Zeichen des Userpassworts ausgelesen. Wie erwähnt gehen wir davon aus, dass diese Daten nicht in der Response ausgegeben werden. Bis hierhin ist die Selektion des ersten Zeichens also nur im Speicher des Servers zu finden und, sofern keine weiteren Sicherheitslücken existieren, nicht auslesbar. Aber in der IF-Bedingung wird nun dieses Zeichen mit dem ASCII-Wert 68 (D) verglichen. Trifft diese Bedingung zu, wird mit der nativen Funktion BENCHMARK für fünf Sekunden lang ein ENCODE der Strings foo und bar durchgeführt. Trifft diese Bedingung nicht zu, wird direkt ein NULL zurückgeliefert. Man kann nun also hergehen, sich die normale Responsezeit der Webseite anschauen und dann eine Query entsprechend so manipulieren, dass diese IF-Bedingung Bestandteil ist. Verlängert sich die Responsezeit um fünf Sekunden, ist sofort klar, dass das erste Zeichen des Passworts ein D ist. Sollte die Responsezeit weiterhin die selbe sein, hat man hier zwar keinen Treffer, kann aber direkt den nächsten Buchstaben ausprobieren und dies so lange wiederholen, bis man das richtige Zeichen gefunden hat. Danach kann man mit dem zweiten Zeichen des Passworts fortfahren und so in relativ kurzer Zeit auch längere Strings ohne aufwändiges Brute Forcing und ohne direkte Ausgabe auslesen.

Noch interessanter wird dies in Kombination mit entsprechenden XSS-Lücken, da die Same-Origin-Policy der Browser nur den Inhalt der Response unterbindet, über readyState aber dennoch die Zeit ermittelt werden kann, wann die Response zur Verfügung stehen würde. Somit ist es auch möglich, z. B. Daten aus einem geschlossenen Intranet auszulesen. Hierzu gehen wir in der nächsten Ausgabe genauer ein, wenn wir uns den verschiedenen XSS-Attacken und -Risiken widmen.

Escaping

Eine übliche Variante, um das Ausführen von SQL-Funktionen zu verhindern und den Scope des eigentlich definierten Wertebereichs nicht zu verlassen, ist das Escapen von Parametern. Die wohl bekannteste Variante im PHP-Umfeld ist hierbei sicherlich mysql_real_escape_string, bzw. mysqli_real_escape_string. Die Funktionen werden hierzu über jede Variable, die an den SQL Server gesendet werden, aufgerufen und escapen die darin enthaltenen Werte entsprechend. Dies funktioniert prinzipiell auch sehr gut, birgt aber das Risiko, dass die komplette Anwendung wieder angreifbar ist, wenn nur eine einzige Variable vergessen wurde. Dementsprechend kann dieses Vorgehen nicht empfohlen werden.

Prepared Statements

Die wohl sicherste Variante in PHP ist die Nutzung von Prepared Statements. Hierbei werden die Syntax der SQL-Query und die zu benutzenden Werte separat an den Server gesendet. Dieser nimmt die Ersetzung selbst vor und bleibt dabei definitiv im Scope des Wertebereichs. Das hat zudem den netten Nebeneffekt, dass der SQL Server die Queries besser optimieren und bei wiederholenden Statements, die aber unterschiedliche Werte enthalten, auf die bereits durchgeführte Optimierung zurückgreifen kann und diese nicht erneut durchführen muss. Die SQL-Queries werden also nicht nur sicherer, sondern auch noch schneller. Prepared Statements werden sowohl von PHP selbst, als auch von allen aktuellen Frameworks unterstützt.

Bei der Nutzung der MySQLi-Klassen von PHP würde die Vorbereitung der Query also wie folgt aussehen:

    $stmt = $mysqli->prepare(
  'UPDATE users
   SET email = ?
   WHERE id = 123'
);

Das Fragezeichen markiert dabei den Platzhalter für den variablen Wert. Hier sind natürlich auch mehrere Platzhalter möglich. So könnten auch die ID als Parameter übergeben und komplexere Statements durchgeführt werden.

Die Parameter können dann entsprechend mit der bind_param-Methode befüllt und sogar der Datentyp bestimmt werden:

    $stmt->bind_param(
  's',
  $email
);

Das s steht hierbei für einen String. Dies kann aber bei Bedarf auch weiter eingeschränkt werden und z. B. ein Integer-Casting forciert werden, sodass eine weitere Validierung des Wertebereichs stattfindet.

Wildcards

Vorsicht ist auch bei der Parameterübergabe geboten, wenn Wildcards, z. B. bei LIKE-Vergleichen, erlaubt sind. Dies betrifft dann auch Datenbankabstraktionen wie Propel oder Doctrine. Angenommen anhand eines Usernamens soll eine E-Mail-Adresse aktualisiert werden, könnte der Aufbau der entsprechenden Doctrine-Query wie folgt aussehen:

   q = Doctrine_Query::create()
     ->update('User')
     ->set('email', 'foo @bar.de')
     ->where(
       'username LIKE ?',
       $username
     ); 

Gefahr besteht nun, wenn $username ein mögliches Wildcard-Zeichen für den LIKE-Vergleich enthält, wie ein Prozent- oder Fragezeichen. Je nach Zielgruppe muss dies nicht mal durch einen bewussten Angriff passieren, denn Sonderzeichen können sogar ein gewolltes Mittel sein, um seinen Nickname „aufzuhübschen“. Angenommen ein User nennt sich nun A%, führt dies dazu, dass bei dieser Query alle E-Mail-Adressen der User überschrieben werden, deren Username mit einem A beginnt, da das Prozentzeichen eine Wildcard ist und somit auf mehr als nur eine Datenbankzeile zutrifft. Sicherlich ist es sowieso besser, anhand einer ID ein Update auszuführen. Aber in der Praxis kann nicht allzu selten auch solch ein Code gefunden werden. Die Zeichen, die entsprechend escapt werden müssen, hängen von der verwendeten Datenbank und der Abstraktion ab. Häufig sind aber spezielle Funktionen oder Wildcards Zeichen wie einem Stern, Klammern oder dem Bindestrich zugeordnet.

Interpreter

Wie bereits erwähnt, sind Injections aber nicht nur bei Datenbanken zu finden, sondern bei jeder Art von Interpreter, der mit entsprechenden Eingabedaten umgeht. So muss auch z. B. bei der Integration eines externen API oder einer Search Engine wie Apache Solr oder Elasticsearch darauf geachtet werden, dass sowohl die Query, als auch der Indexing-Prozess entsprechend escapt werden. Leider gibt es hierfür keine allgemeingültige Lösung, sodass jeweils ein Blick in die Dokumentation geworfen werden muss, um entsprechende Angriffe zu verhindern.

Dateisystem

Gerne übersehen werden auch Angriffe auf Dateiebene, wenn eine CSV-, JSON- oder XML-Datei generiert werden soll. So kann ein manipulierter Eintrag in einer CSV-Datei gleich mehrere Zeilen auf einmal zurückliefern, um z. B. die Chancen bei einem Gewinnspiel zu erhöhen oder sogar das vorzeitige Ende der Datei durch das Einfügen eines erzwungen werden. Damit würden alle weiteren Datensätze in dieser Datei komplett ignoriert.

Sehr offensichtlich, aber auch entsprechend gefährlich, und nur kurz erwähnt werden, sollen hierbei auch die Verwendung von eval– und system-Kommandos, die besondere Aufmerksamkeit verlangen.

Mail-Header

Häufig zu finden ist auch die unvalidierte Übergabe von Mail-Headern an die Standard-Mail-Funktion von PHP. Unabhängig davon, dass die Weiterempfehlenformulare in Deutschland inzwischen als illegal eingestuft wurden, möchte man doch, sofern man sie versendet, dass die Absender-E-Mail-Adresse dem empfehlenden User entspricht und nicht der Website, von der die E-Mail versendet wurde. Der Quellcode sieht dafür typischerweise folgendermaßen aus:

    mail(
  'foo @bar.com',
  'subject',
  'text',
  'From: ' . $email
);

Der vierte Parameter setzt dabei optionale Header der E-Mail und sollte in diesem Fall durch den entsprechenden Key den From-Header auf die übergebene E-Mail-Adresse setzen. Das Trennzeichen der E-Mail-Header ist dabei allerdings nur ein Zeilenumbruch und somit können weitere Header injected werden, die dazu führen, dass die E-Mail evtl. einen weiteren Empfänger bekommt. Oder es können sogar bestehende Header überschrieben werden, die dann je nach Mail-Client den originalen Text, Empfänger oder Überschrift überschreiben. Somit reicht solch eine Lücke schon aus, um beliebig Spam über ein Weiterempfehlenformular zu versenden. Das Prüfen auf eine gültige E-Mail-Adresse ist leider nicht so einfach, wie es auf den ersten Blick aussieht, und endet in der Regel in einem mehrzeiligen, unwartbaren regulären Ausdruck. Hier helfen aber zum Glück die Libraries (z. B. Zend_Validate) und sogar PHP nativ (filter_var) mit entsprechenden Funktionen.

Das Thema Injection beginnt also leider nur bei den bekannten SQL Injections, ist aber viel umfassender und nicht umsonst weiterhin Top 1 der Liste. Leider gibt es kein Universalrezept, sodass individuell darüber nachgedacht werden muss, welche Parameter valide sind und diese möglichst strikt, im Idealfall sogar über ein Whitelisting, zu filtern.

Broken Authentication

Die Nummer zwei auf der Liste befasst sich mit Problemen in der Authentifizierung und dem Sessionmanagement. Hier soll noch kurz auf die Authentifizierung eingegangen werden, während wir uns dann in der nächsten Ausgabe dem umfangreicheren Thema des Sessionmanagements widmen.

Der gerade erst stattgefundene Hack auf Adobe hat gezeigt, dass dieser Punkt nicht umsonst sehr weit oben in der Liste zu finden ist. Millionen von Userdaten wurden geklaut und wenige Tage später waren die Listen mit allen E-Mail-Adressen, den Sicherheitsfragen, die leider unverschlüsselt waren und sogar teilweise die Passwörter im Netz zu finden. Besonders interessant war hierbei u. a. die Top-100-Liste der meistgenutzten Passwörter. So haben fast 2 Millionen User den String 123123 als ihr Passwort ausgesucht. Auch die darauf folgenden Lieblinge waren nicht weiter kreativ und enthalten dann wie üblich gerne den Namen der Website oder den Lieblingsfußballverein. So kommt es nicht selten vor, dass bei einer Community 80 Prozent der User dieselben Top-30-Passwörter nutzen.

Starke Passwörter

Der wichtigste Punkt ist deswegen, die Nutzer dazu zu zwingen, ein sicheres Passwort zu wählen. So sollte eine entsprechende Mindestlänge überprüft werden und auch das Einfügen von Zahlen und Sonderzeichen verpflichtend sein. Empfehlenswert ist auch, Teile des Usernamens, der Domain, des Firmen- oder Projektnamens und eben die erwähnten Fußballvereine als Passwörter zu unterbinden.

Auch gibt es kaum einen Grund, die maximale Länge eines Passworts zu beschränken. Sofern dieses dann nicht mehrere MB groß ist, dadurch beim Hashing enorme Ressourcen benötigt und es so zum DoS führen kann. Im Gegenteil: Wir sollten eben froh sein, wenn die User selbst ein möglichst langes und komplexes Passwort wählen. Das Hashing eines Passworts ist obligatorisch, sodass der benötigte Speicherplatz für diesen Hash unabhängig von der ursprünglichen Länge des Passworts immer derselbe ist.

Fehlermeldungen

Eine gute Möglichkeit, um herauszubekommen, ob ein bestimmter User oder eine E-Mail-Adresse in einem System einen Account hat, ist die Nutzung des „Passwort vergessen“ oder Registrierungsformulars. Hier sollten in den Fehlermeldungen keine Informationen veröffentlicht werden, ob der User existiert, sondern eine generische Meldung wie „Eine E-Mail wurde an die angegebene E-Mail-Adresse versendet, sofern diese in der Datenbank vorhanden ist“ ist ausreichend – egal, ob die E-Mail-Adresse wirklich existiert oder nicht. Denn habe ich erst mal herausgefunden, ob ein User oder eine E-Mail-Adresse in der Datenbank existiert, ist schon die erste Hürde genommen, und es muss nur noch das entsprechende Passwort ermittelt werden. Sofern die Information, dass z. B. der Username schon belegt ist, in einem Registrierungsprozess veröffentlicht werden muss, könnte diese Ausgabe auch verzögert oder über ein Captcha eingeschänkt werden. Denn dies erschwert wiederum das Auslesen dieser Daten. Die meisten Angreifer suchen eher weiträumig nach Sicherheitslücken, um die Server mit den wenigsten Sicherheitsmechanismen zu finden, statt stundenlang an einer Website zu arbeiten.

Auch beim Login sind Fehlermeldungen, wie „Der Username existiert nicht“ oder „Ja super der User war richtig, aber das Passwort leider falsch“ tabu. Ein simples „Login fehlgeschlagen“ ist hier ausreichend.

Brute Force

Wenn dann nun doch ein Username öffentlich ist und zum Glück nicht durch andere Sicherheitslücken das Passwort bekannt oder manipuliert werden kann, dann ist Brute Forcing oft das einzige Mittel, um an einen entsprechenden authentifizierten Account zu kommen. Hierbei werden einfach durch die pure Masse an Anfragen in der Regel zunächst die häufigsten Passwörter aus einer Passwortliste (vgl. Adobe-Top-100) ausprobiert und wenn dies nicht ausreichend ist, entsprechend alle möglichen Kombinationen ausprobiert. Dies führt über kurz oder lang zu einem gültigen Passwort für den Account. Unser Ziel ist es hierbei, den Weg möglichst lang zu machen. Sollten also mehrere Login-Versuche fehlschlagen, sollten einige Mittel in Erwägung gezogen werden. So kann der Login z. B. verlangsamt und die Ausgabe der Erfolgs- und Fehlermeldungen entsprechend über ein sleep verzögert werden. Eine weitere Möglichkeit ist z. B., ab dem dritten fehlgeschlagenen Versuch ein Captcha einzublenden, das entweder nicht von Robots gelöst werden kann oder zumindest auch wieder mehr Zeit braucht, um die Abfrage auszuführen. Last but not least ist das Sperren des Accounts ab einer gewissen Anzahl von fehlgeschlagenen Login-Versuchen natürlich auch ein probates Mittel. Aber auch hier muss abgewägt werden, ob dies nicht evtl. wieder ausgenutzt werden kann und sich eines Morgens plötzlich niemand mehr auf der Website einloggen kann, was auch nicht gerade zu guter PR führt.

Fazit

Trotz ständiger Progression im Entwicklungsbereich haben wir leider immer noch mit massiven Sicherheitsproblemen zu kämpfen. Und egal, welches neue Framework eingesetzt wird, man muss sich definitiv bewusst Gedanken über das Sicherheitskonzept seiner Applikation machen, dies aber gleichzeitig auch mit Anforderungen aus der User Experience in Einklang bringen. Das nächste Mal werden wir uns deswegen mit den Themen Sessionmanagement, bzw. der Übernahme von Sessions und der Absicherung auf dem Webserver und XSS beschäftigen.

Error: No site found with the domain 'test.basti1012.bplaced.net' (Learn more)