Yeti: Passwörter unlesbar machen

Hallo liebe Forengemeinde,

zur Zeit stehe ich vor der Aufgabe, die bisher mit MD5 gehashten Passwörter einer Datenbank sicherer zu machen. MD5 und SHA-1 sind ja bereits geknackt und zu MD5 habe ich auch schon Internetseiten gefunden, wo ich die halbe Datenbank sofort knacken und den Rest in Auftrag geben konnte.
Ich persönlich mag die PHP-Funktion crypt, allerdings soll hier der MySQL-Server aus Sicherheitsgründen die Funktionen ausführen.
ENCRPYT() sieht toll aus, allerdings ist das nicht unter Windows verfügbar, unsere Entwicklung läuft aber unter Windows (produktiv natürlich nicht).
Wie kann ich das Dilemma lösen?

Der Yeti

  1. Hallo Yeti,

    zur Zeit stehe ich vor der Aufgabe, die bisher mit MD5 gehashten Passwörter einer Datenbank sicherer zu machen.

    Sicherer wovor?

    MD5 und SHA-1 sind ja bereits geknackt

    Nein, bitte keine Panikmache!

    MD5 ist insofern "geknackt", als dass es möglich ist, zwei Nachrichten zu erzeugen, die den gleichen Hash haben. Es gibt allerdings bis heute noch keinen Algorithmus außer Brute-Force, der Dir eine Klartextnachricht zu einem bereits vorhandenem Hash liefert. Es ist zwar möglich (und vmtl. auch wahrscheinlich), dass sich dies in den nächsten Jahren ändern wird, aber noch kannst Du MD5 nicht einfach ohne weiteres "rückgängig machen".

    Dennoch ist es natürlich sinnvoll, für neu entworfene Anwendungen nicht mehr auf MD5 zu setzen, da die Sicherheit des Algorithmus durchaus kompromittiert ist.

    SHA-1 ist nicht geknackt. Es gibt zwar aus gutem Grund einige Zweifel an der Sicherheit von SHA-1, da es bereits ein Verfahren gibt, der Brute-Force auf eine deutlich geringere Anzahl an Versuchen reduziert, allerdings ist diese Anzahl immer noch groß genug, um Brute Force nicht praktikabel zu machen.

    Daher wäre für mich SHA-1 der Algorithmus meiner Wahl, wenn ich Passwörter in Datenbanken hashen wollte. Er wird sich vielleicht in Zukunft als unsicher herausstellen, da Passwörter aber eine begrenzte Lebensdauer haben (oder zumindest haben sollten!), stellt das in meinen Augen kein wirkliches Problem dar.

    Wobei ich selbst mit MD5 kein Problem hätte, wenn es sich um Passwörter handelt, die sowieso unverschlüsselt über das Internet übertragen werden, denn da ist es viel Wahrscheinlicher, jemand fängt die auf dem Weg ab, als dass jemand in Deinen Server einbricht, sich ein komplettes DB-Dump zieht und dann alle Passwörter knackt.

    Wenn ich eine Anwendung entwerfen wollte, die Hashes für andere Zwecke, z.B. für digitale Signaturen, benötigt, würde ich natürlich nicht mehr SHA-1 nehmen. SHA-256 oder SHA-512 wären zum Beispiel denkbar. Oder RIPMED-160. Diese sollten auf absehbare Zeit ausreichende Sicherheit bieten können.

    und zu MD5 habe ich auch schon Internetseiten gefunden, wo ich die halbe Datenbank sofort knacken und den Rest in Auftrag geben konnte.

    Diese Seiten nutzen vermutlich sogenannte Rainbow-Tabellen, d.h. vorberechnete MD5-Hashes zu allen möglichen Passwörtern bis zu einer gewissen Länge.

    Gegen so ein Verfahren hilft das sogenannte "Salting": Ein Passwort wird vor dem Hashing mit einem Zufallsschlüssel konkateniert und der Zufallsschlüssel wird auch mit abgespeichert, d.h. so:

    Gehashtes Passwort = Salt + Hash(Salt + Passwort)

    Wenn der Salt z.B. aus 6 Zeichen bestünde, dann kann könnte man das Passwort über folgenden Algorithmus überprüfen:

    Salt = Erste 6 Zeichen von gespeichertem Passwort
    TestPasswort = Salt + Hash(Salt + Eingegebenes Passwort)
    Vergleiche TestPasswort mit gespeichertem Passwort

    Wenn für jedes neu festgelegte Passwort eine andere Zufallszeichenkette als Salt genutzt wird, dürfte man Rainbow-Tabellen so ziemlich gut aushebeln können - da dann für genau dieses Salt jemand selbst nochmal Rainbow-Tabellen berechnen müsste, und damit wären wir wieder bei Brute Force.

    Das Verfahren hilft auch prima gegen diese MD5-Knackdienste, selbst wenn man weiterhin MD5 verwendet.

    Ich persönlich mag die PHP-Funktion crypt,

    Crypt nutzt entweder einen 3DES-basierten Hash, der maximal 8 Zeichen schluckt (dafür aber gar keine Kollisionen besitzt, da die Eingabedaten begrenzt sind) oder selbst MD5 oder SHA1 (dann fangen die Passwörter mit $1$ oder $2$ an, $1$ für MD5, $2$ für SHA1). Der einzige Vorteil von crypt() ist, dass es von vorne herein Salts verwendet. Da 3DES-crypt unter Windows nicht zur Verfügung stht, kann crypt unter Windows selbst nur MD5 und SHA1.

    allerdings soll hier der MySQL-Server aus Sicherheitsgründen die Funktionen ausführen.

    Was für Sicherheitsgründe? Wäre es nicht sinnvoller, das Passwort nicht nochmal unverschlüsselt über die Leitung zu jagen zwischen MySQL und PHP? Mir ist nicht klar, warum das die Sicherheit erhöhen sollte...

    Wie kann ich das Dilemma lösen?

    Baue Salting ein und nutze als Algorithmus weiterhin MD5 oder SHA1. Du musst Dir halt nur überlegen, wie an einen möglichst geeigneten zufälligen Salt herankommst.

    Viele Grüße,
    Christian

    1. Hi,

      Sicherer wovor?

      Vor den unten beschriebenen Rainbow-Attacken und Zufallsfunden, da sie bisher nicht gesaltet sind.

      Nein, bitte keine Panikmache!

      Sorry. Hatte nur den Hinweis in der MySQL-Doku gelesen. "Sind Exploits bekannt" ist natürlich nicht dasselbe wie geknackt, aber es ist "unsicherer" als "sicher". ;)

      SHA-1 ist nicht geknackt. Es gibt zwar aus gutem Grund einige Zweifel an der Sicherheit von SHA-1, da es bereits ein Verfahren gibt, der Brute-Force auf eine deutlich geringere Anzahl an Versuchen reduziert, allerdings ist diese Anzahl immer noch groß genug, um Brute Force nicht praktikabel zu machen.

      Okay, dann werde ich wohl auch darauf setzen.

      Wobei ich selbst mit MD5 kein Problem hätte, wenn es sich um Passwörter handelt, die sowieso unverschlüsselt über das Internet übertragen werden, denn da ist es viel Wahrscheinlicher, jemand fängt die auf dem Weg ab, als dass jemand in Deinen Server einbricht, sich ein komplettes DB-Dump zieht und dann alle Passwörter knackt.

      Das SSL-Zertifikat kommt noch, ich wollte allerdings alle möglichen Lücken schließen und nicht darauf hoffen, dass der Angreifer nur unbedingt den leichtesten Weg geht. ;)
      Immerhin hätte der bei einem DB-Dump _alle_ Passwörter und nicht nur die paar, die er durch Sniffen abfangen kann, weil sich zufällig ein paar einloggen.

      Diese Seiten nutzen vermutlich sogenannte Rainbow-Tabellen, d.h. vorberechnete MD5-Hashes zu allen möglichen Passwörtern bis zu einer gewissen Länge.

      Genau. Allerdings gibt es dabei wohl auch Verfahrensschwächen, die ausgenutzt werden. Jedenfalls, Salting wollte ich sowieso anwenden, dabei aber dann direkt auf einen "sichereren" Algorithmus wechseln.

      Was für Sicherheitsgründe? Wäre es nicht sinnvoller, das Passwort nicht nochmal unverschlüsselt über die Leitung zu jagen zwischen MySQL und PHP? Mir ist nicht klar, warum das die Sicherheit erhöhen sollte...

      Die "Leitung" zwischen MySQL und PHP nennt sich (zur Zeit noch) localhost. Auch später wird niemand von außen an die Verbindung kommen.
      Der Zugriff auf die User-Tabelle ist beschränkt, d.h. nur über Stored Procedures möglich. Damit wird verhindert, dass eine kompromittierte PHP-Anwendung ein komplettes Auslesen der Tabelle ermöglicht.

      Baue Salting ein und nutze als Algorithmus weiterhin MD5 oder SHA1. Du musst Dir halt nur überlegen, wie an einen möglichst geeigneten zufälligen Salt herankommst.

      Mach ich (SHA1)! Danke!

      Der Yeti

      1. Yerf!

        Immerhin hätte der bei einem DB-Dump _alle_ Passwörter und nicht nur die paar, die er durch Sniffen abfangen kann, weil sich zufällig ein paar einloggen.

        Was ich hier noch anmerken würde: wenn ein Angreifer einen DB-Dump hat, dann hat er mehr als nur die Passwörter und je nach Anwendung wären die vielleicht das letzte worum ich mir dann sorgen machen müsste...

        Was ich damit sagen will: nicht soviel in die Passwortverschlüsselung stecken, sondern lieber in die Sicherheit des DB-Servers selbst.

        Gruß,

        Harlequin

        1. Hi,

          Was ich hier noch anmerken würde: wenn ein Angreifer einen DB-Dump hat, dann hat er mehr als nur die Passwörter und je nach Anwendung wären die vielleicht das letzte worum ich mir dann sorgen machen müsste...

          Ja. Username, Rolle, CustomerID, LastIp, LastHost, Logincount, FailedloginAt, Failures, Disabled, CreatedAt und UpdatedAt. Die Adresse, E-Mail und Kontoinformationen liegen in einer anderen Anwendung. ;)

          Was ich damit sagen will: nicht soviel in die Passwortverschlüsselung stecken, sondern lieber in die Sicherheit des DB-Servers selbst.

          Per SSH mit Public-Key auf dem DB-Server anmelden, dann an die nochmals passwortgeschützte Datenbank. Reicht das? ;) Patches etc. sind natürlich auch drauf.
          Nur wollen wir gerne alle Löcher stopfen, deswegen machen wir jedes Glied so sicher wie möglich, auch wenn es noch schwächere Glieder geben sollte.

          Der Yeti

    2. Hallo Christian,

      Salt = Erste 6 Zeichen von gespeichertem Passwort

      Nur zur Verdeutlichung: Als Salt keinesfalls die ersten 6 Zeichen des Passwortes nehmen! Hier in dem Beispiel wird nur das Salt sozusagen vor den Passwort abgespeichert. Der Salt sollte zufällig generiert werden und möglichst lang sein und möglichst auch alle möglichen Sonderzeichen enthalten, um das Erstellen von passenden vorberechneten Tabellen zu verhindern/erschweren.

      Wenn für jedes neu festgelegte Passwort eine andere Zufallszeichenkette als Salt genutzt wird, dürfte man Rainbow-Tabellen so ziemlich gut aushebeln können - da dann für genau dieses Salt jemand selbst nochmal Rainbow-Tabellen berechnen müsste, und damit wären wir wieder bei Brute Force.

      Exakt.

      Was für Sicherheitsgründe? Wäre es nicht sinnvoller, das Passwort nicht nochmal unverschlüsselt über die Leitung zu jagen zwischen MySQL und PHP? Mir ist nicht klar, warum das die Sicherheit erhöhen sollte...

      Das "Problem" ist wohl, dass wenn der Browser nur einen Hash senden muss, es genauso ausreicht, wenn jemand den Hash mithört und diesen dann versendet.

      Eine relativ sichere Lösung ist es, dass der Server dem Client einen zufälligen String schickt und von diesem verlangt, dass er diesen zusammen mit dem Passwort hashed (z.B. MD5) und dann zurücksendet. Jemand der den Datenverkehr mithört kriegt dann natürlich den Hash mit, der hilft ihm aber wenig, da der Server das nächste mal einen anderen zufälligen String schicken wird, der gehasht werden muss.

      Baue Salting ein und nutze als Algorithmus weiterhin MD5 oder SHA1. Du musst Dir halt nur überlegen, wie an einen möglichst geeigneten zufälligen Salt herankommst.

      Die Zufälligkeit ist IMHO nicht so wichtig. Er sollte wie gesagt nur für jedes Passwort einzigartig sein und eben möglichst lang und möglichst viele verschiedene Zeichen besitzen. Wenn man den Hash auslesen kann, kommt man normalerweise auch ans Salt dran, und dann ist es relativ egal, ob das Salt jetzt "-----1" oder "$6J},*s" ist, da man für jedes Salt sowieso ne eigene Tabelle erstellen muss, bzw. ein eigenes Bruteforce ausführen muss, (sofern das Salt+Passwort eben nicht so kurz/primitiv ist, dass vorhandene Tabellen ausreichen).

      Jonathan

      1. Hallo Jonathan,

        Was für Sicherheitsgründe? Wäre es nicht sinnvoller, das Passwort nicht nochmal unverschlüsselt über die Leitung zu jagen zwischen MySQL und PHP? Mir ist nicht klar, warum das die Sicherheit erhöhen sollte...

        Das "Problem" ist wohl, dass wenn der Browser nur einen Hash senden muss, es genauso ausreicht, wenn jemand den Hash mithört und diesen dann versendet.

        Mein Einwand hier bezog sich auf die Strecke zwischen PHP und MySQL und nicht zwischen Browser und Webserver.

        Ferner:

        Eine relativ sichere Lösung ist es, dass der Server dem Client einen zufälligen String schickt und von diesem verlangt, dass er diesen zusammen mit dem Passwort hashed (z.B. MD5) und dann zurücksendet. Jemand der den Datenverkehr mithört kriegt dann natürlich den Hash mit, der hilft ihm aber wenig, da der Server das nächste mal einen anderen zufälligen String schicken wird, der gehasht werden muss.

        Was Du hier vorschlägst, nennt sich Challange-Response - und dafür muss das Passwort auf dem Server im Klartext vorliegen, also genau das, was der OP nicht will. ;-)

        Viele Grüße,
        Christian

        1. Hallo Christian,

          Mein Einwand hier bezog sich auf die Strecke zwischen PHP und MySQL und nicht zwischen Browser und Webserver.

          Das hab ich auch gerade gemerkt.

          Was Du hier vorschlägst, nennt sich Challange-Response - und dafür muss das Passwort auf dem Server im Klartext vorliegen, also genau das, was der OP nicht will. ;-)

          Stimmt. Hab ich nicht dran gedacht.

          Man *könnte* es aber so machen, dass der Server dem Client noch zusätzlich das Salt schickt, der dann erstmal damit den (hoffentlich korrekten) Hash auf dem Server zusammenbastelt und diesen dann nochmal mit dem zufälligen String des Servers gehasht zurückschickt. Nachteil ist, dass dadurch das Salt öffentlich wird, was der Sicherheit zwar sicher nicht besonders förderlich ist, aber auch keine großen Probleme verursachen sollte.

          Also, der Server hat in der Datenbank:
          Salt + MD5(Salt + Server_PW)

          der schickt dem Client
          Salt + Zufallstring.

          Der Client macht daraus
          MD5(Zufallstring + MD5(Salt + Client_PW))
          und schickt das dem Server.

          Der Server berechnet sich
          MD5(Zufallstring + MD5(Salt + Server_PW))
          und vergleicht das mit den Daten vom Client.

          Da das aber eh nicht gefragt war, ist es wohl egal.

          Jonathan

      2. Hi,

        Nur zur Verdeutlichung: Als Salt keinesfalls die ersten 6 Zeichen des Passwortes nehmen! Hier in dem Beispiel wird nur das Salt sozusagen vor den Passwort abgespeichert. Der Salt sollte zufällig generiert werden und möglichst lang sein und möglichst auch alle möglichen Sonderzeichen enthalten, um das Erstellen von passenden vorberechneten Tabellen zu verhindern/erschweren.

        Wie zufällig ist generiert ist gut genug? Reicht PHPs rand()?
        Wie lang ist lang genug? 281 Billiarden verschiedene Möglichkeiten?
        Sollte das Salt dem Hash ähnlich sehen oder vielleicht eher doch nicht?

        Der Yeti

        1. Hallo Yeti,

          Wie zufällig ist generiert ist gut genug? Reicht PHPs rand()?

          Wie schon gesagt. Die pure Zufälligkeit ist IMHO nicht so wichtig.

          Wie lang ist lang genug? 281 Billiarden verschiedene Möglichkeiten?

          Das halte ich nicht für besonders viel. Ich denke, du kannst locker 16 Byte nehmen mit allen Zeichen von 0-255 (sofern es keine Probleme mit irgendwelchen Steuerzeichen gibt). Das entspricht dann 2^128 verschiedenen Möglichkeiten, was eine Zahl mit 39 Stellen ist.

          Sollte das Salt dem Hash ähnlich sehen oder vielleicht eher doch nicht?

          IMHO eher unerheblich. Es wäre unsinnig die Zahl der möglichen Salts zu verringern nur um diese besonders ähnlichen/unähnlich gegenüber dem Hash zu machen.

          Jonathan