Heutzutage gibt es kaum noch jemanden, der nicht aus tiefer Frustration auf die Schaltfläche „Passwort wiederherstellen“ geklickt hat. Selbst wenn das Passwort zweifellos richtig zu sein scheint, verläuft der nächste Schritt zur Wiederherstellung meist reibungslos, indem man einen Link aus einer E-Mail besucht und das neue Passwort eingibt (machen wir uns nichts vor; es ist kaum neu, da Sie es in Schritt 1 bereits dreimal eingegeben haben, bevor Sie auf die lästige Schaltfläche geklickt haben).
Die Logik hinter E-Mail-Links muss jedoch sehr genau unter die Lupe genommen werden, da eine unsichere Generierung eine Flut von Schwachstellen in Bezug auf unbefugten Zugriff auf Benutzerkonten öffnet. Leider ist hier ein Beispiel für eine UUID-basierte Wiederherstellungs-URL-Struktur, die viele wahrscheinlich schon einmal gesehen haben, die aber dennoch nicht den Sicherheitsrichtlinien entspricht:
https://.../recover/d17ff6da-f5bf-11ee-9ce2-35a784c01695
Wenn ein solcher Link verwendet wird, bedeutet das im Allgemeinen, dass jeder Ihr Passwort erhalten kann, und so einfach ist das. Ziel dieses Artikels ist es, tief in die Methoden zur UUID-Generierung einzutauchen und unsichere Ansätze für ihre Anwendung auszuwählen.
UUID ist ein 128-Bit-Label, das häufig zum Generieren pseudozufälliger Kennungen mit zwei wertvollen Eigenschaften verwendet wird: Es ist komplex genug und eindeutig genug. Dies sind in der Regel wichtige Anforderungen, damit die ID das Backend verlässt und dem Benutzer explizit im Frontend angezeigt oder allgemein über die API gesendet wird und beobachtet werden kann. Im Vergleich zu id = 123 ist es schwer zu erraten oder mit Brute-Force-Methoden zu ermitteln (Komplexität) und verhindert Kollisionen, wenn die generierte ID mit einer zuvor verwendeten dupliziert wird, z. B. eine Zufallszahl zwischen 0 und 1000 (Eindeutigkeit).
Die „genug“-Teile stammen eigentlich erstens aus einigen Versionen des Universally Unique IDentifier, wodurch es zu geringfügigen Duplizierungsmöglichkeiten kommt, die jedoch durch zusätzliche Vergleichslogik leicht gemildert werden können und aufgrund der kaum kontrollierten Bedingungen für ihr Auftreten keine Bedrohung darstellen. Und zweitens wird der Umgang mit der Komplexität verschiedener UUID-Versionen im Artikel beschrieben, im Allgemeinen wird davon ausgegangen, dass er bis auf weitere Sonderfälle recht gut ist.
Primärschlüssel in Datenbanktabellen scheinen auf denselben Prinzipien der Komplexität und Eindeutigkeit zu beruhen wie UUIDs. Da in vielen Programmiersprachen und Datenbankverwaltungssystemen integrierte Methoden zur Generierung weit verbreitet sind, sind UUIDs häufig die erste Wahl zur Identifizierung gespeicherter Dateneinträge und als Feld zum Verbinden von Tabellen im Allgemeinen und durch Normalisierung aufgeteilten Untertabellen. Das Senden von Benutzer-IDs, die als Reaktion auf bestimmte Aktionen aus einer Datenbank über eine API stammen, ist ebenfalls gängige Praxis, um den Prozess der Vereinheitlichung von Datenflüssen zu vereinfachen, ohne zusätzliche temporäre IDs generieren und sie mit denen im Produktionsdatenspeicher verknüpfen zu müssen.
In Bezug auf Beispiele für das Zurücksetzen von Passwörtern enthält die Architektur wahrscheinlich eher eine Tabelle, die für einen solchen Vorgang verantwortlich ist und bei jedem Klicken eines Benutzers auf die Schaltfläche Datenzeilen mit generierter UUID einfügt. Sie leitet den Wiederherstellungsprozess ein, indem sie eine E-Mail an die mit dem Benutzer über seine Benutzer-ID verknüpfte Adresse sendet und anhand der Kennung, die der Benutzer hat, sobald der Reset-Link geöffnet wird, prüft, für welchen Benutzer das Passwort zurückgesetzt werden soll. Es gibt jedoch Sicherheitsrichtlinien für solche für Benutzer sichtbaren Kennungen, und bestimmte Implementierungen von UUID erfüllen diese mit unterschiedlichem Erfolg.
Version 1 der UUID-Generierung teilt ihre 128 Bits auf und verwendet eine 48-Bit-MAC-Adresse des Gerätes, das die Kennung generiert, einen 60-Bit-Zeitstempel, 14 Bit, die für die Werterhöhung gespeichert werden, und 6 Bit für die Versionierung. Die Garantie der Eindeutigkeit wird somit von den Regeln der Codelogik auf die Hardwarehersteller übertragen, die für jede neue Maschine in der Produktion korrekte Werte zuweisen müssen. Wenn nur 60+14 Bits übrig bleiben, um nützliche, veränderbare Nutzdaten darzustellen, verschlechtert sich die Integrität der Kennung, insbesondere wenn eine derart transparente Logik dahinter steckt. Sehen wir uns eine Folge der infolgedessen generierten Nummern von UUID v1 an:
from uuid import uuid1 for _ in range(8): print(uuid1())
d17ff6da-f5bf-11ee-9ce2-35a784c01695 d17ff6db-f5bf-11ee-9ce2-35a784c01695 d17ff6dc-f5bf-11ee-9ce2-35a784c01695 d17ff6dd-f5bf-11ee-9ce2-35a784c01695 d17ff6de-f5bf-11ee-9ce2-35a784c01695 d17ff6df-f5bf-11ee-9ce2-35a784c01695 d17ff6e0-f5bf-11ee-9ce2-35a784c01695 d17ff6e1-f5bf-11ee-9ce2-35a784c01695
Wie man sehen kann, bleibt der Teil „-f5bf-11ee-9ce2-35a784c01695“ immer gleich. Der veränderbare Teil ist einfach eine 16-Bit-Hexadezimaldarstellung der Sequenz 3514824410 – 3514824417. Dies ist ein oberflächliches Beispiel, da Produktionswerte normalerweise mit größeren Zeitlücken dazwischen generiert werden, sodass der zeitstempelbezogene Teil ebenfalls geändert wird. Der 60-Bit-Zeitstempelteil bedeutet auch, dass ein bedeutenderer Teil der Kennung über eine größere Stichprobe von Kennungen hinweg visuell geändert wird. Der Kernpunkt bleibt derselbe: UUIDv1 lässt sich leicht erraten, egal wie zufällig es zunächst aussehen mag.
Nehmen Sie nur den ersten und letzten Wert aus der angegebenen Liste mit 8 IDs. Da die Kennungen streng generiert werden, ist klar, dass zwischen den beiden angegebenen IDs nur 6 generiert werden (durch Subtraktion der hexadezimalen veränderbaren Teile) und deren Werte ebenfalls eindeutig gefunden werden können. Die Extrapolation dieser Logik ist der zugrunde liegende Teil des sogenannten Sandwich-Angriffs, der darauf abzielt, die UUID aus der Kenntnis dieser beiden Grenzwerte mit Brute-Force-Methoden zu ermitteln. Der Angriffsablauf ist unkompliziert: Der Benutzer generiert UUID A, bevor die Ziel-UUID generiert wird, und UUID B direkt danach. Unter der Annahme, dass dasselbe Gerät mit einem statischen 48-Bit-MAC-Teil für alle drei Generierungen verantwortlich ist, wird einem Benutzer eine Folge potenzieller IDs zwischen A und B zugewiesen, wo sich die Ziel-UUID befindet. Abhängig von der zeitlichen Nähe zwischen den generierten IDs zum Ziel kann der Bereich in für Brute-Force-Methoden zugänglichen Bereichen liegen: Überprüfen Sie jede mögliche UUID, um vorhandene unter den leeren zu finden.
Bei API-Anfragen mit dem zuvor beschriebenen Endpunkt zur Kennwortwiederherstellung bedeutet dies, dass Hunderte oder Tausende von Anfragen mit den entsprechenden UUIDs gesendet werden, bis eine Antwort mit der vorhandenen URL gefunden wird. Beim Zurücksetzen des Kennworts führt dies zu einem Setup, bei dem der Benutzer Wiederherstellungslinks auf zwei Konten generieren kann, die er so nah wie möglich kontrolliert, um die Wiederherstellungstaste auf dem Zielkonto zu drücken, auf das er keinen Zugriff hat, aber nur E-Mail/Login kennt. Briefe an kontrollierte Konten mit den Wiederherstellungs-UUIDs A und B sind dann bekannt, und der Ziellink zum Wiederherstellen des Kennworts für das Zielkonto kann mit Brute-Force geknackt werden, ohne Zugriff auf die eigentliche Reset-E-Mail zu haben.
Die Schwachstelle rührt von dem Konzept her, sich ausschließlich auf UUIDv1 zur Benutzerauthentifizierung zu verlassen. Durch das Senden eines Wiederherstellungslinks, der Zugriff auf das Zurücksetzen von Passwörtern gewährt, wird angenommen, dass ein Benutzer durch Klicken auf den Link als derjenige authentifiziert wird, der den Link erhalten sollte. Dies ist der Teil, bei dem die Authentifizierungsregel fehlschlägt, da UUIDv1 direkter Brute-Force-Attacke ausgesetzt ist, so als ob jemandes Tür geöffnet werden könnte, wenn er wüsste, wie die Schlüssel der beiden Nachbartüren aussehen.
Die erste Version von UUID wird hauptsächlich als veraltet angesehen, da die Generierungslogik nur einen kleineren Teil der Kennungsgröße als zufälligen Wert verwendet. Andere Versionen, wie v4, versuchen dieses Problem zu lösen, indem sie so wenig Platz wie möglich für die Versionierung lassen und bis zu 122 Bits für die zufällige Nutzlast übrig lassen. Im Allgemeinen bringt es die Gesamtzahl möglicher Variationen auf satte 2^122
, was im Moment als ausreichend für die Anforderung der Kennungseindeutigkeit angesehen wird und somit Sicherheitsstandards erfüllt. Eine Brute-Force-Anfälligkeit könnte auftreten, wenn die Generierungsimplementierung die für den zufälligen Teil übrig gebliebenen Bits irgendwie erheblich verringert. Aber sollte das ohne Produktionstools oder Bibliotheken der Fall sein?
Lassen Sie uns ein wenig in die Kryptographie eintauchen und einen genauen Blick auf die gängige Implementierung der UUID-Generierung in JavaScript werfen. Hier ist die Funktion randomUUID()
die auf dem Modul math.random
zur Generierung von Pseudozufallszahlen basiert:
Math.floor(Math.random()*0x10);
Und die Zufallsfunktion selbst, kurz gesagt, sie ist nur der Teil, der für das Thema dieses Artikels von Interesse ist:
hi = 36969 * (hi & 0xFFFF) + (hi >> 16); lo = 18273 * (lo & 0xFFFF) + (lo >> 16); return ((hi << 16) + (lo & 0xFFFF)) / Math.pow(2, 32);
Die pseudozufällige Generierung erfordert einen Startwert als Basis, auf dem mathematische Operationen ausgeführt werden, um Sequenzen von ausreichend zufälligen Zahlen zu erzeugen. Solche Funktionen basieren ausschließlich darauf, d. h. wenn sie mit demselben Startwert wie zuvor neu initialisiert werden, stimmt die Ausgabesequenz überein. Der Startwert in der betreffenden JavaScript-Funktion besteht aus den Variablen hi und lo, jeweils eine 32-Bit-Ganzzahl ohne Vorzeichen (0 bis 4294967295 Dezimalzahl). Für kryptografische Zwecke ist eine Kombination aus beiden erforderlich, wodurch es nahezu unmöglich ist, die beiden Anfangswerte durch Kenntnis ihres Vielfachen definitiv umzukehren, da dies auf der Komplexität der Ganzzahlfaktorisierung bei großen Zahlen beruht.
Zwei 32-Bit-Ganzzahlen ergeben zusammen 2^64
mögliche Fälle zum Erraten von Hi- und Lo-Variablen hinter der initialisierten Funktion, die UUIDs erzeugt. Wenn Hi- und Lo-Werte irgendwie bekannt sind, ist es kein Aufwand, die Generierungsfunktion zu duplizieren und alle Werte zu kennen, die sie erzeugt und aufgrund der Offenlegung des Startwerts in Zukunft erzeugen wird. 64 Bits können in Sicherheitsstandards jedoch als intolerant gegenüber Brute-Force-Angriffen in einem messbaren Zeitraum angesehen werden, damit dies sinnvoll ist. Wie immer liegt das Problem in der spezifischen Implementierung. Math.random()
nimmt verschiedene 16 Bits von jedem von Hi und Lo in 32-Bit-Ergebnisse; randomUUID()
darüber verschiebt den Wert jedoch aufgrund der .floor()
Operation erneut, und der einzige sinnvolle Teil kommt jetzt plötzlich ausschließlich von Hi. Es hat keinerlei Einfluss auf die Generierung, führt jedoch dazu, dass kryptografische Ansätze scheitern, da nur 2^32
mögliche Kombinationen für den gesamten Funktions-Seed der Generierung übrig bleiben (es besteht keine Notwendigkeit, sowohl hi als auch lo mit roher Gewalt zu erzwingen, da lo auf jeden beliebigen Wert gesetzt werden kann und die Ausgabe nicht beeinflusst).
Der Brute-Force-Flow besteht darin, eine einzelne ID zu erhalten und mögliche hohe Werte zu testen, die diese generiert haben könnten. Mit etwas Optimierung und durchschnittlicher Laptop-Hardware kann dies nur ein paar Minuten dauern und erfordert nicht das Senden vieler Anfragen an den Server wie beim Sandwich-Angriff, sondern führt alle Vorgänge offline aus. Das Ergebnis eines solchen Ansatzes bewirkt eine Replikation des Status der Generierungsfunktion, der im Backend verwendet wird, um alle erstellten und zukünftigen Reset-Links im Beispiel der Kennwortwiederherstellung abzurufen. Schritte zum Verhindern der Entstehung von Sicherheitslücken sind unkompliziert und erfordern die Verwendung kryptografisch sicherer Funktionen, z. B. crypto.randomUUID()
.
UUID ist ein großartiges Konzept und erleichtert Dateningenieuren in vielen Anwendungsbereichen das Leben erheblich. Es sollte jedoch niemals im Zusammenhang mit der Authentifizierung verwendet werden, da in diesem Artikel Mängel in bestimmten Fällen seiner Generierungstechniken ans Licht gebracht werden. Dies bedeutet natürlich nicht, dass alle UUIDs unsicher sind. Der grundlegende Ansatz besteht jedoch darin, die Leute davon zu überzeugen, sie überhaupt nicht aus Sicherheitsgründen zu verwenden. Dies ist effizienter und, nun ja, sicherer, als in der Dokumentation komplexe Beschränkungen festzulegen, welche UUIDs für diesen Zweck verwendet werden sollen oder wie sie nicht generiert werden sollen.