Blacklistfilter sind unsicher

Wieso uns WAFs und Input-Filter manchmal in falscher Sicherheit wiegen

 

Eine sichere, moderne Webanwendung verfügt mittlerweile über unterschiedlichste Sicherheitsmechanismen. Zum einen wären da HTTP-Header wie Content-Security-Policy (CSP), X-Frame-Options und X-XSS-Protection oder auch andere, Browserbasierte Sicherheitsmechanismen wie die Same Origin Policy (SOP), die Benutzer vor clientseitigen Angriffen schützen können. Diese sind sowohl für Benutzer als auch Angreifer ersichtlich und bieten ein hohes Maß an Schutz, sofern sie korrekt konfiguriert wurden.

 

Zum anderen gibt es da aber auch weniger offensichtliche Schutzmechanismen, für Benutzer und Angreifer nicht auf den ersten Blick erkennbar und dennoch allgegenwärtig. Die Rede ist hierbei von Input-Filtern und Web Application Firewalls (WAFs). Gewöhnliche Input-Filter werden in der Regel auf dem Application Level eingesetzt, beispielsweise um zu überprüfen ob Nutzereingaben gefährliche Sonderzeichen enthalten. Ein Beispiel hierfür wären die beiden Sonderzeichen < und >. Diese beiden harmlos aussehenden Satzzeichen werden in HTML Code benutzt um Tags zu starten und zu beenden, wie hier zu sehen.

 

<a href = "https://example.com">example</a>

 

Falls solche Sonderzeichen unverändert ausgegeben werden, kann ein Angreifer möglicherweise einen Cross-Site-Scripting (XSS) Angriff ausführen. Solch ein Input-Filter kann sowohl gefährliche Satzzeichen blockieren, als auch ganze Stichworte, wie "javascript" oder "eval". Entweder werden sie entfernt, oder der Benutzer erhält eine Fehlermeldung.

 

Diese Schutzfunktion ist direkt im Quellcode der Anwendung zu finden, es gibt allerdings auch die Möglichkeit eine Web Application Firewall (WAF) zu verwenden um solche gefährlichen Eingaben zu verhindern. Diese befindet sich meist auf einem anderen Server und funktioniert wie ein gewöhnlicher Reverse Proxy. Allerdings werden die Anfragen an den eigentlichen Zielserver hier zuerst auf verbotene Zeichen und Keywords überprüft und nur weitergeleitet, wenn keine für die Anwendung gefährlichen Eingaben erkannt wurden.

 

All dies klingt nach einem perfekten Schutz vor Schwachstellen. In der Realität werden solche auf Blacklists basierenden Filter jedoch oftmals überwunden.

Wieso ist es oft so schwer eine vollständige Blacklist zu erstellen

Es gibt verschiedene Gründe wieso Blacklists in der Praxis oft unsicher sind. Nehmen wir einmal HTML und XSS als Beispiel. HTML, die meistverbreitete Auszeichnungssprache im Internet ist fast ausnahmslos in jeder Web Applikation zu finden, insofern sie nicht ausschließlich aus Textdateien besteht. Die Anforderungen an diese Sprache, aus der Sicht von Browserentwicklern sind daher recht eindeutig. Um Einsteigern das Entwickeln eigener Webseiten zu ermöglichen, sollte die Sprache nicht auf einer zu strikten Syntax beharren. So können beispielsweise Anführungszeichen weggelassen werden, es können beliebig viele Leerzeichen verwendet werden und auch die Groß- und Kleinschreibung ist bei HTML nicht so wichtig.

 

HTML richtet sich an Entwicklern von Webanwendungen und zwingt sie daher nicht dazu, sich an feste Formatierungen zu halten, wie dies beispielsweise bei Python der fall wäre. Das wäre bei dynamisch generiertem Content, schnell sehr umständlich.

Kurzum, HTML vergibt viel, ist daher allerdings auch anfälliger für das Umgehen von Filtermechanismen.

Beispiel 1: Das Umgehen eines XSS Filters

Bisher sind diese Beschreibungen relativ abstrakt, daher schauen wir uns einfach einmal an wie ein Angreifer in der Praxis solch einen Filter umgehen würde.

 

Verbotene Keywords:     <script>, alert, document.cookie

Output:            <div>*OUTPUT*</div>

Ziel:            Das anzeigen der Browser-Cookies in einem Popupfenster

 

Wäre kein Input-Filter oder WAF zugegen, wäre die einfachste Art das Ziel zu erreichen, die folgende Payload:

 

<script>alert(document.cookie)</script>

 

Natürlich würde solch eine Eingabe verhindert werden, daher greifen wir zu mehreren Tricks. Ich werde erst die Payload zeigen und dann Schritt für Schritt erklären wieso sie funktioniert.

 

<img src = x onerror = "confirm(document['cookie'])">

 

Beginnen wir mit document['cookie']. Die obige Vorgabe verbietet uns lediglich den String document.cookie in unserem Input. Der Punkt zwischen den beiden Worten bedeutet, dass cookie eine Eigenschaft des document Objekts ist. Alternativ lässt sich dies mit den eckigen Klammern in unserer Payload darstellen. Hierfür muss lediglich die Eigenschaft als String (mit Anführungszeichen) in die Klammern geschrieben werden. Dieses Konstrukt wird wie oben gezeigt einfach an das document Objekt angehängt.

 

Als nächstes haben wir confirm statt alert. Auch confirm zeigt ein Popup und ist daher fast identisch mit alert. Mit dem oben gelernten Trick hätten wir allerdings auch window['al'+'ert'] oder window['\x61l\u0065rt'] schreiben können. Auch die String.fromCharCode Methode wäre eine Möglichkeit gewesen, oder das Verwenden von Teilen anderer Strings, wie hier zu sehen: window[(![]+'')[1]+'lert']. Dies würde zuerst den String "false" erzeugen und dann mit Hilfe der eckigen Klammer, den zweiten Buchstaben, also ein a zurückgeben. Es werden zwar noch immer die buchstaben 'l','e','r' und 't' benötigt und sowohl "t" als auch "r" kommen in "false" nicht vor, wenn man aber noch ein weiteres "!" benutzt, wird der string "true" zurück gegeben, womit alle Buchstaben für alert komplett sind.

 

Dies ist ein sehr spezifisches Beispiel und es ist im Prinzip möglich alle Buchstaben mit ähnlichen Tricks zu erhalten, es zeigt aber, dass, sobald es einem Angreifer möglich ist JavaScript auszuführen, eine Keyword-Basierte Blacklist zum Scheitern verurteilt ist.

 

Ähnlich, jedoch nicht ganz so verheerend sieht es mit dem <script> Keyword aus. Wie zu sehen ist wurde es durch das img tag im Zusammenspiel mit dem onerror Eventhandler ersetzt. Diese Eventhandler führen den in ihnen befindlichen JavaScriptcode aus, wenn ein bestimmtes Event eintrifft, beispielsweise wie hier ein Fehler bei dem Laden eines Bildes. Da kein Bild mit dem Pfad /x auf dem Server existiert, gibt der Browser einen Fehler zurück und der Code wird ausgeführt. Es ist hierbei zu beachten, dass es dutzende dieser Eventhandler gibt, die mit jeweils mehreren HTML Tags kompatibel sind.

 

Die verfügbaren Eventhandler werden häufig geändert und es werden neue hinzugefügt, außerdem unterscheiden sie sich stark von Browser zu Browser und sind sogar abhängig von der Plattform. Eine komplette up-to-date liste aller verfügbaren Eventhandler ist mir nicht bekannt und wäre relativ schwer zu realisieren. Daher ist es oft nicht effektiv einzelne Eventhandler zu blocken, außer man erstellt solch eine Liste und hält sie aktuell.

Beispiel 2: System Command Injections

System Command Injections sind nicht häufig in normalen Webanwendungen anzutreffen. Shopsoftware, Blogs und die meisten CMS' sind selten darauf angewiesen ein externes Programm aufzurufen. Anders sieht es da aus bei IoT Geräten und Admin-Interfaces von Routern, Firewalls und ähnlichen Geräten. Selbst Internetradios und Fernseher bleiben von Angriffen dieser Art nicht immer verschont.

Nicht selten ziehen die Hersteller dieser Geräte diesen Umstand in Betracht und versuchen mit Hilfe von Input-Filtern und einer Blacklist solche Angriffe im Keim zu ersticken. Dennoch sind diese Filter für gewöhnlich unzureichend, wie ich an folgendem Beispiel demonstrieren werde.

 

Verbotene Keywords und Sonderzeichen:    ; | & " " (Leerzeichen)

System: Linux

System Command: ping *input*

Ziel: Lesen der /etc/passwd Datei

 

Erwartet wird offenbar eine IP Adresse oder ein Hostname. Der ping Befehl sendet dann ICMP Pakete an den Zielrechner und wartet auf eine Antwort. Bei erfolgreicher Antwort, gibt es Statistiken zu den gesendeten und empfangenen Paketen zurück. Die Blacklist lässt sich folgendermaßen erklären:

 

    ;    Beginnt einen neuen Befehl

    |    Sendet den output des vorherigen Befehls als input zu dem nächsten Befehl

    &    Führt einen neuen Befehl aus, falls der return code des vorherigen 0 ist

Leerzeichen werden benötigt um parameter zu übergeben

 

Wie es aussieht ist das ein ziemlich effektiver Schutz vor einer System Command Injection. Wären da nicht Subshells. Mit Hilfe von ``, den sogenannten Backticks, oder $() ist es möglich system commands auszuführen. Beispielsweise würde echo "Hallo, $(whoami)" den aktuellen Benutzernamen ausgeben.

 

 

Hierfür wird zuerst der Befehl in den Klammern ausgeführt. Beispielsweise gäbe dieser "root" zurück. Dann wird $(whoami) ersetzt durch "root" und folglich wird "Hallo, root" ausgegeben. Dieser Trick lässt sich auch hier nutzen um einen eigenen Befehl auszugeben, beispielsweise um $(cat /etc/passwd) auszuführen. Das Problem ist hierbei allerdings, dass wir ein Leerzeichen zwischen cat und dem ersten Parameter benötigen. Dies können wir umgehen, indem wir entweder ein "    " (tab) nutzen oder die IFS-Variable. IFS, oder auch "Internal Field Separator" genannt, ist eine Variable die Sonderzeichen enthält, welche genutzt werden, um Daten in einzelne Token zu zerlegen. So enthält IFS standardmäßig "Leerzeichen", "Tab" und "Newline". Geben wir nun also $(cat${IFS}/etc/passwd) ein, so wird der Befehl erfolgreich ausgeführt und die /etc/passwd Datei wird ausgelesen. Erst danach wird das Ergebnis and den Ping-Befehl übergeben. Dann kann man den Inhalt entweder über eine Fehlermeldung erfahren, oder man muss eine Payload senden, die das Ergebnis beispielsweise über wget oder curl and den eigenen HTTP Server sendet.

 

Natürlich könnte man sich jetzt auf dieses Katz-und-Maus-Spielchen einlassen und wget, curl, cat, passwd, IFS, etc. blocken, dies kann aber in der Regel umgangen werden, je nachdem welche Tools auf dem verwundbaren Rechner installiert sind. Außerdem kann es schnell zu Falsch-Positiven Treffern führen, wenn beispielsweise das Keyword "cat" geblockt wird, der Nutzer aber gerne seinen cats-are-awesome.xy Server anpingen möchte. Wie sich solche Nachteile effektiv verhindern lassen, werde ich am Ende dieses Blogbeitrages erläutern.

Beispiel 3: Gemischtes

Nicht nur solch bekannte Technologien und Schwachstellen, wie die eben erwähnten sind anfällig für solche Tricks. Am Beispiel einer PHP Object Injection wird das deutlich. Die Genaue Funktionsweise dieses Angriffes würde den Rahmen dieses Beitrages sprengen, weil wir erst über PHP-Klassen, Magic Methods, Gadgets und ähnliches sprechen müssten.

 

Was hier aber wichtig zu wissen ist, ist ausschließlich, dass der Input in einem gewissen Format erfolgen muss, dem eines serialisierten PHP Objektes. Dieses beginnt mit einem O, der Anzahl der Buchstaben des Klassennamens, dem Klassennamen in Anführungszeichen, und dann der Anzahl seiner Eigenschaften. Aussehen würde es etwa folgendermaßen:

 

O:2:"xy":0:{}

 

Würde man verhindern wollen, dass ein serialisiertes PHP Objekt übergeben wird, könnte man dementsprechend versuchen, jeden String der mit O: beginnt, zu blocken. Das Problem ist, falls sich das Objekt in einem Array befindet, wird dennoch ein Objekt deserialisiert, allerdings beginnt der String dann mit einen a: statt einem O:. Man könnte auch versuchen /O:\d+/ zu matchen, also ein O: gefolgt von einer oder mehreren Zahlen. Dazu muss man aber auch wissen, dass O:2: ebenso gültig ist wie O:+2:, wohingegen letzteres nicht vom oben erwähnten Regulären Ausdruck erfasst würde.

 

Auch zu erwähnen sind hierbei NoSQL-Injections, welche im Falle von MongoDB auch leicht zu erkennen sind. Der $ne Operator, beispielsweise wird hier häufig genutzt. Will man aber verhindern dass ein Angreifer ihn verwendet, muss man sich im Klaren darüber sein, dass man ihn auch oft durch \u0024ne ersetzen kann. Dies dürfte den meisten nicht klar sein.

 

Diese Liste ließe sich ewig fortführen. Das Problem ist auch, dass neue Versionen der verwendeten Software möglicherweise neue Wege für Angreifer bieten, Filter zu umgehen. Selbst Chrome's eigener XSS-Filter war hiervon nicht verschont, da er neuere HTML Entities, wie &sol; nicht erkennen konnte, weil er sie schlicht und ergreifend noch nicht kannte. Chrome's HTML-Parser hatte sie aber bereits unterstützt, weswegen hier ein Angriff möglich war und der XSS Filter umgangen werden konnte.

Wie kann ich meine Anwendung nun schützen?

Das kommt sehr darauf an, welchen Input man erwartet. In den meisten Fällen ist eine Whitelist die richtige Wahl, in anderen Fällen ist es sinnvoller den Input in ein geeignetes Format umzuwandeln, beispielsweise durch ein entsprechendes Encoding. Gehen wir die vorherigen Beispiele nun noch einmal durch.

1. Cross Site Scripting

Es ist wichtig, den Input vor der Ausgabe korrekt zu kodieren. Allerdings gilt es dabei zu beachten, dass nicht jedes Encoding in jedem Kontext angebracht ist. Befindet sich die Ausgabe direkt im HTML-Code, ist es nötig, bestimmte Zeichen in HTML-Sonderzeichen wie &lt; umzuwandeln. Dies ist beispielsweise nicht effektiv in einem Scriptblock, der sich in einem <svg> Tag befindet oder in HTML-Parametern oder Eventhandlern. Hier ist in manchen Fällen URL-Encoding, wie %22 mitunter sinnvoller. Da JavaScript diese beiden Encodings nicht versteht, sollten Nutzereingaben, welche direkt in den String eines Scriptblocks geschrieben werden, in JavaScript's Hex Encoding, beispielsweise \x27 ausgegeben werden.

2. System Command Injections

Hier sieht es wieder etwas anders aus. Es wäre theoretisch möglich auch hier den Input zu Encoden, beispielsweise mit Hilfe des echo-Befehls, welcher über eine subshell an den Ping-Befehl übergeben wird. Die einfachere Variante ist eine Whitelist, welche die erlaubten Zeichen enthält. In unserem Beispiel wird das besonders deutlich, da Hostnamen und IP Adressen nur wenige Sonderzeichen enthalten können. So sind nur Punkte und Bindestriche in Hostnamen und ausschließlich Punkte in IP-Addressen erlaubt (jedenfalls in IPv4).

3. Gemischtes

Auch hier wird deutlich, dass es keinen allgemein gültigen Ansatz gibt, um Nutzereingaben sicher zu machen. Laut dem PHP-Manual wird beispielsweise empfohlen, überhaupt keine Nutzerangaben zu deserialisieren. Dies ist zwar eine gute Möglichkeit Object Injections zu verhindern, in der Praxis macht es jedoch einige Dinge komplizierter und es ist schwer existierende Schwachstellen zu beheben ohne große Teile der Anwendung neu zu entwickeln.

 

In diesen Fällen ist es sinnvoll eine HMAC einzusetzen, also einen Hash, der mithilfe des serialisierten Objektes und einem geheimen Schlüssel auf dem Webserver generiert wird. Sollte die HMAC fehlen, oder in dem Fall dass ein Angreifer sie, dank des geheimen Schlüssels, nicht berechnen kann, wird eine Fehlermeldung ausgegeben und das Objekt wird nicht deserialisiert.

 

Im Fall von NoSQL-Injections ist es oft ausreichend, einem Angreifer nicht zu erlauben Objekte oder Arrays zu übergeben. Hierbei sollte stets der Typ der Usereingabe überprüft werden. Falls es sich nicht um einen String handelt, sollte sie nicht akzeptiert werden.

Die Schlussfolgerung

Um eine sichere Webanwendung zu programmieren, reicht es in der Regel nicht aus, eine Blacklist einzusetzen. Vielmehr müssen Nutzereingaben entsprechend überprüft und umgewandelt werden. In einigen Fällen ist auch eine Whitelist sinnvoll, welche einer Blacklist auf jeden Fall vorzuziehen ist. Es ist theoretisch möglich Blacklists zu erstellen, die in einigen Anwendungsgebieten funktionieren, jedoch ist hierfür eine Menge Know-How nötig und sie müssen ständig neu angepasst werden, um dem neuesten Stand der zu schützenden Technologie zu entsprechen.

 

Jedoch haben auch WAFs ihre Daseinsberechtigung, beispielsweise in Fällen in denen es darum geht, möglichst schnell, ein System vor neuen Gefahren zu schützen, wie beispielsweise eines kürzlich veröffentlichten Zero-Day-Exploits. In diesem Fall bieten Blacklist einen gewissen Schutz gegen Angriffe, vor allem wenn diese automatisiert erfolgen. Dies kann kurzfristig helfen, bis die Anwendung das entsprechende Update erhält.

 

 

 

 

 

Ein Artikel von Sven Morgenroth.