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".
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?
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.
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:
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:
Ł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.
Jak w takim razie zrobić to dobrze?
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.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?
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:
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