[PL]

Koktajl z bcrypta i wyłuskiwanie haszy

TL;DR - łączenie bcrypta z innymi, niesolonymi funkcjami skrótu (typu MD5 czy SHA-1), może powodować poważne konsekwencje w postaci "wyłuskania" bcrypta. Nadal jest to jednak lepsze, niż po prostu używanie słabego algorytmu. W niektórych przypadkach, możliwe jest znalezenie czegoś w rodzaju "kolizji".

Wyłuskiwanie haszy aka. password shucking

30ml legacy code

Firmy, które istnieją juz jakiś czas na rynku niejednokrotnie muszą dopasowywać się do nowych okoliczności, wytycznych lub schematów działania. Tak jest również w kwestii przechowywania haseł. Lata temu biznesy radziły sobie z tym na różne sposoby - przechowywały hasła bez jakiegokolwiek zabezpieczenia, zaszyfrowane, czy z użyciem szybkich funkcji skrótu typu MD5 czy SHA-1. Kiedy nadszedł jednak powiew zmian, i chęć lub konieczność dopasowania swojej infrastruktury do obecnych realiów, nastąpił problem. Jak zmienić wszystkie hasze haseł swoich użytkowników w całej bazie danych na nowszy algorytm? Można wyróżnić trzy poniższe pomysły.

Po pierwsze, kazać każdemu nadać nowe hasło. Niesie to za sobą jednak pewne konsekwencje - nie jest to wygodne ani dla nas, ani dla użytkowników. Musimy obsłużyć bardzo dużą, nagłą i natychmistową liczbę operacji oraz narażamy się na podejrzenia o wyciek danych. Klienci mogą chcieć zrezygnować z korzystania z platformy.

Po drugie, można przy logowaniu podmieniać hasze użytkowników w bazie. Pomysł nienajgorszy - nikt się nie dowie, z zewnątrz nic nie widać, ale ile to zajmie czasu? Jakaś część użytkowników jest pewnie naszymi stałymi klientami, ale wymiana wszytstkich to w najlepszym przypadku miesiące, o ile nie lata. Co, jeśli w tym czasie będziemy mieli wyciek? Co z użytkownikami, którzy nigdy się nie zalogują? Opinii publicznej nie będzie interesowało, że 15% użytkowników zdązyliśmy przerzucić na bcrypta przed wyciekiem. Trzeba obsługiwać też wiele mechanizmów logowania na raz.

Po trzecie - a może by tak zahaszować hasza? Weźmy hasza każdego użytkownika i policzmy z niego bcrypta - np. bcrypt(MD5($pass)). Mechanizm również niewidoczny z zewnątrz. Co prawda musimy wspierać dwa algorytmy na raz, ale jesteśmy od razu bezpieczni, nie mamy przecież MD5 w bazie. Ale czy na pewno?

90ml danych z wycieków

Problemem, który zaczyna wychodzić nam naprzeciw jest password shucking. Metoda, która w języku polskim nie ma ani znanego mi tłumaczenia, ani opisu działnia - roboczo nazwijmy ją zatem wyłuskiwaniem haszy. Żeby zrozumieć, dlaczego jest ona problemem, rzućmy okiem na jeden z najpopularniejszych serwisów agregujących dane o wyciekach - haveibeenpwned. Obecnie, znajduje się w nim 12,485,202,808 haseł, z 664 stron na dzień 27 marca 2023. Spójrzmy na kilka ostatnich opublikowanych informacji o wyciekach: TheGradCafe, Shopper+, Eye4Fraud, LBB czy iDTech - o wszystkich z nich HIBP poinformował na Twitterze w marcu 2023.

Screenshot%20from%202023-03-27%2010-09-30

Screenshot%20from%202023-03-27%2010-10-22

Screenshot%20from%202023-03-27%2010-10-35

Screenshot%20from%202023-03-27%2010-10-46

Screenshot%20from%202023-03-27%2010-11-00

Przytłaczająca większość haszy z tych, i wielu innych wycieków o których informuje HIBP była już w ich bazie danych. Co to oznacza? Możnaby pokusić się o stwierdzenie, że przynajmniej część użytkowników używa po prostu tego samego hasła w wielu miejscach.

Inne wycieki ujawnione w tym roku:

3

md51

md52

(Hasła przechowywane w MD5, czy nawet w postaci nieprzetworzonej.)

Czym jest w takim razie właściwie wyłuskiwanie haszy? Weźmy na warsztat (nieco zmodyfikowany) przykład Royce Williamsa, członka zespołu hashcat. Hipotetyczna sytuacja jest następująca:

  1. Atakujący uzyskuje bazę danych z giełdy kryptowalut, w których znajdują się hasze haseł w postaci bcrypta.
  2. Atakujący przeprowadza najprostsze, bezpośrednie ataki na bcrypta - niestety nie dają sukcesu - algorytm bcrypt jest bardzo powolny.
  3. Atakujący konstruuje słownik do ataku na bcrypta, w którym znajdują się hasze MD5 z innego, niepowiązanego wycieku, np. z portalu randkowego. Sprawdza, czy któraś z MD5 jest prawidłowym "hasłem" do bcrypta. Może też poszukiwać hasza konkretnego użytkownika, jeżeli atak skierowany jest na określoną osobę. Jeśli atakujący trafi chociaż jedną MD5, może wyciągnąć następujące wnioski:
  • Po pierwsze, bezpośrednie złamanie bcryptów, których bazą jest wyjście z MD5 jest mało prawdopodobne.
  • Po drugie, złamanie MD5 z innego wycieku w połączeniu z popularnością reużywania haseł przez użytkowników w różnych serwisach daje mu spore szanse na sukces.

Łamanie MD5 w praktyce jest dużo szybsze niż bcrypta. Karta graficzna RTX4080 Founders Edition potrafi łamać MD5 z prędkością około 98000 MH/s, słownie dziewięćdziesięciu ośmiu miliardów haszy na sekundę. Bcrypt? 131 kH/s - słownie sto trzycieści jeden tysięcy haszy na skeundę. Jest różnica.

Tym właśnie jest password shucking - wyłuskaniem słabszego algorytmu z silniejszej obudowy. Takie "opakowywanie" ma miejsce także w innych sytuacjach, np. kiedy ktoś (dosyć nieumiejętnie) chce obejść ograniczenia bcrypta co do maksymalnej długości hasła, albo uznał, że dzięki temu łatwe hasła zostaną "wzmocnione". Nie dotyczy to tylko MD5, a każdego niesolonego algorytmu opakowywanego w bcrypta. Narzędzie hashcat wspiera także bezpośrednie łamanie niesolonych algorytmów opkowywanych w bcrypta:

  • 25600 - bcrypt(md5($pass))
  • 25800 - bcrypt(sha1($pass))
  • 30600 - bcrypt(sha256($pass))
  • 28400 - bcrypt(sha512($pass))

Nie jest to również atak teoretyczny - Royce przyznaje, że:

This is not a theoretical attack. It is used all the time by advanced password crackers to successfully crack bcrypt hashes that would otherwise be totally out of reach for the attacker.

2 sztuki dobrych rad

Jak w takim razie zrobić to dobrze?

  1. Należy użyć pieprzu, czyli zapisanej osobno, długiej i losowej wartości, która będzie dodawana do słabszego algorytmu (np. w postaci bcrypt(md5($pass).$pepper), tak, aby inne wycieki nie dawały przewagi w łamaniu opakowanych haszy. Trzeba jednak mieć na nią oko - utrata pieprzu oznacza powrót do punktu wyjścia.
  2. Można połączyć pomysły opakowywania bieżących haszy i wymiany ich na "zwykłego" bcrypta przy logowaniu użytkownika.

Wstrząśnięte, nie zmieszane - podano koktajl z bcrypta

Innym problemem, który możemy napotkać podczas łączenia bcrypta z innymi funkcjami jest to, w jaki sposób został on zaimplementowany. Weźmy na warsztat język PHP oraz wpis Anthon'ego Ferrara (@ircmaxwell) opiosujący kwestię implementacji bcrypta. Wszystko sprowadza się właściwie do zdania:

Basically, it ignores everything after the first null byte.

Jeżeli w wejściu do funkcji bcrypt znajdzie się null byte, wszystko po nim zostanie zignorowane. Czy to jest problem? Cóż, używanie null byte w haśle z pewnością nie należy do najpopularniejszych zabiegów wśród użytkowników. Czy w takim razie, powoduje to jakiś realny problem?

disperse-lesley-nielsen

Jeżeli połączymy wyjście z funkcji typu MD5, SHA, czy HMAC-SHA z bcryptem to duży. W nieprzetowrzonym wyjściu z tych funkcji, null byte jest standardowo pojawiającym się znakiem. Co więcej, Anthony podaje statystykę wskazującą na fakt, że 1 na 256 (~0,39%) wygenerowanych "pre-haszy" z użyciem HMAC-SHA256 będzie miało null byte jako pierwszy znak. Posłużę się kolejnym jego przykładem, w którym wygenerujemy dwa "hasła" z użyciem HMAC-SHA256, które bedą od siebie różne, a jednak ich weryfikacja jako ten sam bcrypt będzie prawidłowa (dzięki pierwszemu znakowi null byte):

$key = "cwuioshc8934f89ch398h34hdfhd3d3d4d343d"; //losowo wybrany klucz, wybrany poprzez uderzenie czołem w klawiaturę
$hash_function = "sha256";
$i = 0;
$found = [];

while (count($found) < 2) {
    $pw = base64_encode(str_repeat($i, 5));
    $hash = hash_hmac($hash_function, $pw, $key, true); //tworzenie HMAC-SHA256 na podstawie klucza oraz licznika pętli
    if ($hash[0] === "\0") {
        $found[] = $pw;
    }
    $i++;
}

var_dump($i, $found);
$hash = password_hash(hash_hmac("sha256", $found[0], $key, true), PASSWORD_BCRYPT);
var_dump(password_verify(hash_hmac("sha256", $found[1], $key, true), $hash));

Rezultat jest następujący:

result

Dwa różne wyjścia z HMAC-SHA256 dają zbieżny wynik bcrypt. Problemem jest nie tylko znak null byte na pierwszym miejsu, a właściwie na każdym. Przypadek nie jest także typowo PHPowy - sama zależność crypt(3) w języku C posiada ten sam "feature". Nie jest to nawet przypadłość tylko i wyłącznie bcrypta, a różnych rozwiązań z rodziny crypt(). Przykład bcrypta jest natomiast dobry, gdzyż bcrypt jest rozwiązaniem bardzo populanrym i obecnym w wielu miejscach - PHP dostępny zarówno bezpośrednio jako password_hash() jak i we frameworkach (chociażby Hash::make w Laravelu, który pod spodem również używa password_hash()).

Jak nie dać się zmiksować? Najlepiej po prostu używać standardowego bcrypta. Jeżeli jednak jesteśmy zmuszeni do użycia takiej konstrukcji, nie używać bezpośredniego wyjścia z "pre-hasha" (ostatni parametr true w funkcji hash_hmac - tak standardowo łączy się funkcje kryptograficzne, używając wyjścia raw, a nie encoded), a np. dodatkowo je przetowrzyć - wykorzystywać wyjście w formie ciągu heksydecymalnego, czy np. z użyciem base64: password_hash(base64_encode(hash_hmac("sha512", $password, $key, true)), PASSWORD_BCRYPT).

Źródła:

https://www.youtube.com/watch?v=OQD3qDYMyYQ

https://twitter.com/haveibeenpwned

https://security.stackexchange.com/questions/234794/is-bcryptstrtolowerhexmd5pass-ok-for-storing-passwords

https://superuser.com/questions/1561434/how-do-i-crack-a-double-encrypted-hash/1561612#1561612

https://www.scottbrady91.com/authentication/beware-of-password-shucking

https://gist.github.com/bigpick/cfa22947c884f7a3fc1431475e345427

https://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html+

https://tenor.com/view/disperse-lesley-nielsen-explosion-gif-13010485

https://github.com/illuminate/hashing/blob/master/BcryptHasher.php

Previous Post