Cross-Site Scripting (XSS) ist nach wie vor eine der häufigsten Schwachstellen, die wir in unseren Penetrationstests von Webanwendungen vorfinden. In mehr als 80 % der Web-Anwendungen, die wir untersuchen, finden wir Cross-Site Scripting-Schwachstellen. Mit diesem Artikel wollen wir die drei XSS-Typen reflective und stored Cross-Site Scripting sowie DOM-based XSS nochmal mit Beispielen demonstrieren und aufzeigen, wie diese verhindert werden können. Auch ein paar wichtige Fallstricke werden aufgezeigt.
Reflective Cross-Site Scripting (XSS)
Reflective XSS bzw. reflektives XSS ist die einfachste Form von XSS. Der Name kommt daher, dass ein Parameter an die Applikation übertragen und anschließend ungefiltert wieder an den Browser zurückgesendet (zurückgeworfen, deswegen reflected) wird.
Genau das ist auch das zentrale Problem: die ungefilterte Ausgabe von benutzerkontrollierten Daten. Folgender PHP-Code zeigt ein typisches Beispiel:
<html> <body> <h1>Willkommen zurück <?php echo $_GET['username']; ?></h1> </body> </html>
Für die Nicht-PHP-Sprecher eine kurze Erklärung: Mit „echo“ wird eine Variable in die spätere HTML-Seite eingebettet. In diesem Fall der Parameter „username“, der mit Hilfe von HTTP-GET übertragen wurden.
Leider findet überhaupt keine Ausgabefilterung statt. Alles, was über die Variable „username“ übertragen wird, wird somit eins zu eins wieder ausgegeben. Das bedeutet: XSS ist möglich. Wird die Seite mit folgendem Link aufgerufen, wird beispielsweise ein Prompt mit einer Passwortabfrage geöffnet:
http://www.evil.corp/xss.php?username=test%22%3E%3Cscript%3Eprompt(%22Bitte%20Passwort%20eingeben%22)%3C/script%3E
Der böse Code: Cross-Site Scripting-Beispiel
In diesem Link „versteckt“ ist unser bösartiger JavaScript-Code. Dieser ist URL-kodiert, sodass alle Zeichen, die mit dem Verarbeiten der URL Probleme machen könnten, maskiert sind. Ohne Kodierung ist der Schadcode wie folgt:
test"><script>prompt("Bitte Passwort eingeben")</script>
Im Browser sieht das dann wie folgt aus:
Gerade in den Berichten für Webapplikations-Penetrationstests sieht man jedoch meist eine Alert-Box.
Sehen wir uns den Quelltext etwas genauer an:
<html> <body> <h1>Willkommen zurück test"><script>prompt("Bitte Passwort eingeben")</script></h1> </body> </html>
Dort ist der „bösartige“ JavaScript-Code ungefiltert im Quelltext und wird vom Browser natürlich interpretiert.
Zum Angriff blasen
Mit diesen Informationen gerüstet ist nun klar, wie ein tatsächlicher Angriff eines reflektiven Cross-Site Scripting aussieht:
- Der Angreifer präpariert einen Link mit Schadcode.
- Der Link wird per E-Mail an das spätere Opfer gesendet (z.B. per E-Mail).
- Das Opfer klickt auf den Link – selbst, wenn er oder sie die Zieldomain prüft, ist diese ja glaubwürdig.
- Beim Klicken auf den Link wird der Payload, also der bösartige JavaScript-Code an den Server übertragen.
- Der Server bettet das XSS ungefiltert in den HTML-Code ein und liefert die Seite an den Browser.
- Der Browser rendert das HTML und führt den Schadcode aus.
Stored Cross-Site Scripting (XSS)
Stored Cross-Site Scripting bzw. Persistent XSS unterscheidet sich dadurch vom reflective XSS, dass kein spezieller Link aufgerufen werden muss. Der Schadcode wird permanent in der Datenbank, einer Logdatei oder einer anderen Art von permanentem Speicher abgelegt.
Dies ändert den Ablauf des Angriffs entsprechend:
- Der Angreifer baut den Schadcode und übermittelt diesen an den Server.
- Der Server empfängt die Daten und legt diese in einer Persistenz-Schicht (z.B. Datenbank) ab.
- Später: Das Opfer öffnet irgendwann später die Website.
- Der Server liest den Schadcode aus der Datenbank und liefert die Seite an den Browser des Opfers.
- Der Browser rendert das HTML und führt den Schadcode aus.
Schutz vor bzw. Abwehr von reflected und stored XSS
Um sich gegen reflective bzw. persistent Cross-Site Scripting zu schützen, muss man sich klarmachen, dass XSS ein reines Ausgabeproblem ist. An der Stelle, an der die Benutzereingaben in den Quelltext eingebunden werden, muss auch eine Maskierung (Escaping) aller relevanten Steuerzeichen geschehen. Mehr dazu gleich.
Verstehen Sie mich bitte nicht falsch: Eingabefilterung und das absolute Einschränken der zugelassenen Zeichen auf das minimal Mögliche ist nach wie vor sinnvoll. Dennoch gibt es genug Felder, die solch eine Eingabefilterung nicht zulassen, beispielsweise Freitext-Felder.
So nicht: Maskieren bei der Eingabe
Auch das Maskieren bei der Eingabe ist nicht sinnvoll, da das gerade im persistenten Szenario zu Probleme führt. Wird beispielsweise eine Eingabe maskiert in die Datenbank abgelegt, hat das folgende Nachteile zur Folge: Wird das maskierte Feld in ein anderes Dateiformat übertragen, beispielsweise PDF oder XLS, stehen die maskierten HTML-Zeichen in dem Enddokument. Programmierer verstehen zwar noch, was unter " (doppeltes Anführungszeichen) bzw. ' (einfaches Anführungszeichen) zu verstehen ist, das wars dann aber auch schon. Alle anderen Nutzer der Applikation werden sich über einen Fehler beschweren.
Ein weiteres Problem, welches man inzwischen häufiger vorfindet, ist das doppelte Escaping. Dabei findet eine Maskierung sowohl bei der Eingabe als auch bei der Ausgabe statt. Das führt dazu, dass ein " in ein &quot; übersetzt wird, weswegen in der HTML-Oberfläche wieder ein " zu lesen ist.
Der Ort ist wichtig!
Das dritte, in meinen Augen auch das größte Problem ist, dass vorher nicht klar gesagt werden kann, welche Zeichen für ein funktionierendes Escaping relevant sind.
Folgendes Beispiel soll das verdeutlichen. Wir nehmen wieder PHP zur Demonstration. Für die Maskierung verwenden wir die in PHP integrierte Funktion htmlspecialchars, welche die HTML-Steuerzeichen automatisch maskiert:
<html> <head> <script type="text/javascript"> var username = '<?php echo htmlspecialchars($_GET['username']); ?>'; var action = '<?php echo htmlspecialchars($_GET['action']); ?>'; </script> </head> <body> <h1>Willkommen zurück <?php echo htmlspecialchars($_GET['username']); ?></h1> </body> </html>
Wenn der Link aus dem obigen Beispiel mit dem reflective XSS verwendet wird, erhalten wir folgende Ausgabe:
<html> <head> <script type="text/javascript"> var username = 'test"><script>prompt("Bitte Passwort eingeben")</script>'; var action = ''; </script> </head> <body> <h1>Willkommen zurück test"><script>prompt("Bitte Passwort eingeben")</script></h1> </body> </html>
Die Maskierung hat funktioniert, auch auf der Oberfläche sieht man nun JavaScript-Code als harmlosen Text.
Grundsätzlich würde das Escaping also funktionieren. Das Problem ist, dass wir ein neues Stück Code eingefügt haben, nämlich den Skript-Teil:
<script type="text/javascript"> var username = '<?php echo htmlspecialchars($_GET['username']); ?>'; var action = '<?php echo htmlspecialchars($_GET['action']); ?>'; </script>
In der Standardeinstellung maskiert nämlich htmlspecialchars das einfache Anführungszeichen nicht. Ändern wir also den Schadcode folgendermaßen ab, können wir nach wie vor ein XSS ausführen:
test';prompt('Bitte+Passwort+eingeben');//
Der sich ergebende Quelltext sieht wie folgt aus:
<html> <head> <script type="text/javascript"> var username = 'test';prompt('Bitte Passwort eingeben');//'; var action = ''; </script> </head> <body> <h1>Willkommen zurück test';prompt('Bitte Passwort eingeben');//</h1> </body> </html>
Jetzt könnte der geneigte Programmierer natürlich einfach die Option ENT_QUOTES für htmlspecialchars setzen, sodass auch einfache Anführungszeichen maskiert werden:
<html> <head> <script type="text/javascript"> var username = '<?php echo htmlspecialchars($_GET['username'], ENT_QUOTES); ?>'; var action = '<?php echo htmlspecialchars($_GET['action'], ENT_QUOTES); ?>'; </script> </head> <body> <h1>Willkommen zurück <?php echo htmlspecialchars($_GET['username']); ?></h1> </body> </html>
Nun wird auch das Skript entsprechend escaped:
<script type="text/javascript"> var username = 'test';prompt('Bitte Passwort eingeben');//'; var action = ''; </script>
Aber ist dieser Code jetzt sicher?
Die Tücken des XSS
Nein, deswegen möchte ich nochmal darauf hinweisen: XSS kann nur sinnvoll verhindert werden, wenn klar ist, welche Zeichen ein Problem darstellen. Sie können ja eine kurze Pause einlegen und sich Gedanken darüber machen, wie man daraus noch ein funktionierendes XSS bekommt. Kleiner Tipp: Es hängt mit der zweiten Variable zusammen.
Ein wichtiges Steuerzeichen für JavaScript ist noch der Backspace („\“), da damit andere Zeichen maskiert werden. Möchten wir beispielsweise ein einfaches Anführungszeichen direkt in eine JavaScript-Variable schreiben, müssen wir nur ein Backslash davorsetzen:
<script type="text/javascript"> var withQuote = 'Der Name O\'Hara ist im englischsprachigem Raum weit verbreitet'; </script>
Genau das wird uns jetzt zum Verhängnis: htmlspecialchars maskiert nämlich den Backslash nicht. Damit ist folgender Aufruf nach wie vor möglich:
Variable username:
\
Variable action:
;prompt(([]+/Bitte Passwort eingeben/g).substr(1,23));//
Der komplette Link lautet dann:
http://www.evil.corp/xss_fixed_ent.php?username=\&action=;prompt(([]%2B/Bitte%20Passwort%20eingeben/g).substr(1,23));//
Wichtig: Das + für den regulären Ausdruck muss mit %2B übertragen werden!
Der daraus entstehende Quelltext sieht folgendermaßen aus:
<script type="text/javascript"> var username = '\'; var action = ';prompt(([]+/Bitte Passwort eingeben/g).substr(1,23));//'; </script>
Die erste Variable wird dazu benutzt, um das Anführungszeichen zu escapen, weswegen der Inhalt der zweiten Variable dazu verwendet wird, um den Schadcode einzufügen.
Abwehr von stored bzw. reflected XSS: Zusammenfassung
Es sollte nun deutlich geworden sein, dass XSS ein reines Ausgabeproblem ist und nur dann in den Griff bekommen werden kann, wenn man alle relevanten Steuerzeichen kennt und diese maskiert. Wichtig ist dabei vor allem die Unterscheidung zwischen HTML- und JavaScript-Code. Aber Achtung: Es gibt auch die Möglichkeit, XSS innerhalb von Attributen auszuführen (z.B. mit Hilfe von onMouseOver und der anderen Action-Handler). Selbst mit CSS kann ein XSS ausgeführt werden – zumindest in älteren Browsern bzw. Browsern, die in einen Kompatibilitätsmodus gezwungen werden.
Ein letzter Hinweis noch: Es ist dringend sicherzustellen, dass auf allen Ebenen (Datenbank, Programm-Code und späterer Website) immer der gleiche, explizit definierte Zeichensatz verwendet wird. Persönlich würde ich UTF-8 empfehlen. Werden unterschiedliche Zeichensätze verwendet oder ist dieser nicht explizit definiert, können manche XSS-Maskierungen unbrauchbar gemacht werden.
DOM-Based Cross-Site Scripting (XSS)
Eine weitere Kategorie von XSS sind DOM-based XSS. Der zentrale Unterscheid zu beiden vorher genannten XSS-Typen ist, dass sich alles auf dem Client abspielt und der Server im schlimmsten Fall nichts davon mitbekommt.
Kurz zur Erinnerung: Das Document Object Model (DOM) ist die browserinterne Repräsentation einer Website innerhalb des Browsers. Alle Elemente, von Texten über Bildern bis hin zu Formularen, sind im DOM zu finden.
Nehmen wir wieder folgende Seite als Beispiel:
<html> <body> <h1>Willkommen zurück <span id="username"></span></h1> <script type="text/javascript"> document.getElementById("username").innerHTML = unescape(window.location.hash); </script> </body> </html>
Es wird dabei mit Hilfe von JavaScript der Inhalt des Window-Hashes (der Teil der URL, der mit einem # abgetrennt ist) in das span-Element in der Überschrift geschrieben.
Wird die folgende URL aufgerufen, ergibt sich dann das entsprechende Bild:
http://www.evil.corp/dom_xss.php#test
Im Quelltext findet sich davon natürlich nichts, dort ist nur das JavaScript zu sehen. Im DOM ist das Element jedoch deutlich zu sehen:
Dadurch, dass wir die JavaScript-Eigenschaft „innerHTML“ verwenden, kann über den Hash auch JavaScript eingeschleust werden:
Payload:
#test<img src="0" onerror="prompt('Bitte Passwort eingeben')" />
Schon kommt das bekannte Prompt zum Vorschein:
Schön erkennbar ist auch, wo das Element im DOM zu finden ist. Diesmal wurde ein anderer Payload verwendet, da es damit einfacher ist, den JavaScript-Code direkt auszuführen. Hätte hier ein Schadcode mit <script></script> gestanden, könnte dieser nur über Umwege zum Ausführen gebracht werden.
Es ist auch wichtig zu erwähnen, dass alles nach dem Hash-Zeichen („#“) nicht an den Server übertragen wird. Sicherheitssysteme wie Web Application Firewalls würden hier also nichts bringen. Auch eine Analyse der Logfiles oder anderer Debugging-Optionen wäre sinnlos.
Schutz vor DOM-Based Cross-Site Scripting (XSS)
Vor DOM-basierten XSS Angriffen schützen geht ganz einfach: indem man die richtigen DOM-Funktionen verwendet. Im obigen Beispiel hätte statt .innerHTML einfach .innerText verwendet werden müssen. Auf jQuery gemünzt heißt dass, dass $.text() statt $.html() verwendet wird.
Wichtig ist natürlich auch, dass kritische Funktionen wie eval() nicht eingesetzt werden bzw. sichergestellt wird, dass kein bösartiger Code verwendet wird.
Noch ein abschließender Hinweis zum DOM-based XSS: Das obige Beispiel war natürlich nur eine sehr vereinfachte Darstellung. Gerade in den heutzutage sehr weit verbreiteten JavaScript-Frameworks gibt es noch bedeutend mehr Möglichkeiten, XSS zur Ausführung zu bekommen. Allerdings – und das ist das Gute daran – wenn man sich einigermaßen an das Framework hält und nicht darum herum entwickelt, ist man schon recht gut geschützt. Jetzt muss man nur noch sicherstellen, dass Sicherheitspatches für die JavaScript-Frameworks und alle(!) Bibliotheken eingespielt werden, dann ist man schon sehr gut vorbereitet.
Super, eine Pop-Up Box. Na und? Gefahren von Cross-Site Scripting (XSS)
Tja, so eine Pop-Up Box ist natürlich nicht so kritisch, das stimmt. Welche Angriffe ein XSS jedoch ermöglicht, werden wir im zweiten Teil dieser Serie durchsprechen, der nächsten Freitag veröffentlicht wird. Wir werden dann ein paar fiesere XSS-Taktiken ansprechen, beispielsweise wie Zugangsdaten geklaut werden können. Außerdem schauen wir uns BeEF, das Browser Exploitation Framework genau an.
Cross-Site Scripting verhindern – eine Zusammenfassung
Bei reflective und stored XSS ist – wie oben ausgeführt – vor allem die Ausgabe(!)-Maskierung das entscheidende Vorgehen. Beim DOM-basierten XSS ist es vor allem der Einsatz der richtigen Methoden. Bitte geben Sie uns Feedback zu diesem Blog-Artikel und teilen Sie uns mit, ob Sie XSS-Angriffe nun besser verstehen und vor allem verhindern können. Vielleicht finden wir ja in Zukunft weniger XSS-Schwachstellen in unseren Penetrationstests für Webanwendungen 🙂