Ponad dwa lata temu opisaliśmy wyciek danych kilkudziesięciu tysięcy pracowników polskiego wymiaru sprawiedliwości. Ujawniona wówczas baza zawierała hasze haseł, które wydawały się nie do złamania. Słowo „wydawały” zostało użyte celowo.
Autorem artykułu jest Sławek Rozbicki, ekspert w zespole ds. wykrywania cyberzagrożeń banku Standard Chartered. Czasami psuje też oprogramowanie, a śledzić go można na Twitterze. Sławek artykuł napisał ponad rok temu, ale w porozumieniu z nim celowo opóźniliśmy publikację tych treści, by nie zaszkodzić ofiarom wycieku.
Kwiecień 2020 roku zdominowały doniesienia o wyciekach z polskich serwisów, firm i instytucji. W przypadku Krajowej Szkoły Sędziów i Prokuratorów doszło do ujawnienia kopii zapasowej zawierającej bazę danych systemu Moodle, w której znajdowały się informacje dotyczące około 50 tys. użytkowników. Szczegóły tego wycieku zostały opisane w wielu publikacjach, jednak nikt nie wyjaśnił, jak dokładnie doszło do ujawnienia haseł użytkowników.
Przypomnę, że baza nie zawierała haseł, lecz ich skróty MD5 (ang. hash) wykorzystujące tzw. pieprz w celu utrudnienia ich odwrócenia (odgadnięcia).
Czytając informacje prasowe o wycieku, które poruszały kwestię tworzenia haszy, postanowiłem przeanalizować kod źródłowy systemu Moodle, aby znaleźć ewentualne podatności narażające bezpieczeństwo haseł. Kilka dni później opublikowano w serwisie Zaufana Trzecia Strona artykuł, w którym autor poinformował, że wie o złamaniu co najmniej połowy haszy z wycieku z KSSiP.
Kryptografia według Moodle
Według dokumentacji Moodle w celu tworzenia i weryfikacji haszy haseł w wersjach do 2.4 włącznie wykorzystuje się jedną „współdzieloną” sól (ang. salt) dla wszystkich użytkowników, a w nowszych wersjach jest to sól unikatowa (tworzona dynamicznie podczas rejestracji nowego konta). Dokumentacja mylnie nazywa wspomnianą współdzieloną wartość solą, chociaż faktycznie jest to pieprz i pozostanę przy tym terminie w dalszej części tekstu.
Wykorzystanie pieprzu w procesie tworzenia i weryfikacji hasza jest dobrą praktyką, ponieważ utrudnia lub wręcz uniemożliwia odgadnięcie hasła przy użyciu ataku siłowego. Atakujący, nie znając wartości pieprzu, musi bowiem zgadywać jednocześnie hasło i pieprz, co podnosi koszt przedsięwzięcia, czyniąc je niepraktycznym. W przypadku Moodle, który do tworzenia haszy wykorzystuje funkcję skrótu MD5 (absolutnie niezalecaną do zastosowań kryptograficznych), pieprz jest ostatnią deską ratunku w razie ujawnienia haszy.
Aby skutecznie chronić hasła, pieprz musi:
- spełniać wymagania długości i losowości (wysoka entropia),
- być trudnym do przewidzenia (atakujący nie powinien mieć żadnych wskazówek, jak oszacować jego wartość),
- być bezpiecznie przechowywanym (aby ewentualny wyciek bazy nie kompromitował pieprzu).
W celu wygenerowania wartości pieprzu Moodle w wersji do 2.4 wykorzystuje popularny generator liczb pseudolosowych Mersenne Twister, który nie jest bezpieczny kryptograficznie, a dokumentacja języka PHP odradza jego wykorzystanie w zastosowaniach kryptograficznych. Mechanizm generowania pieprzu wykorzystuje funkcję complex_random_string()
zaimplementowaną w sposób następujący:
function complex_random_string($length=null) {
$pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
$poollen = strlen($pool);
mt_srand ((double) microtime() * 1000000);
if ($length===null) {
$length = floor(rand(24,32));
}
$string = '';
for ($i = 0; $i < $length; $i++) {
$string .= $pool[(mt_rand()%$poollen)];
}
return $string;
}
Zakładając, że funkcja wywołana zostanie bez parametru $length
, wykonane zostaną następujące czynności:
- generowanie alfabetu
$pool
i przypisanie jego wielkości zmiennej$poollen
, - zainicjalizowanie generatora liczb pseudolosowych przy użyciu aktualnego czasu w formacie unix przy pomocy funkcji
microtime()
, - wylosowanie liczby z przedziału 24 – 32 w celu określenia długości sekretu,
- utworzenie sekretu
$string
poprzez doklejanie losowanych znaków z alfabetu do czasu osiągnięcia żądanej długości.
O jedną linijkę za dużo
Jeśli interesujesz się kryptografią, to prawdopodobnie złapałeś się właśnie za głowę. W przeciwnym razie pozwolę sobie przytoczyć kilka oczywistości. Podstawową zasadą bezpiecznego używania kryptografii jest wykorzystanie istniejących już algorytmów, które przeszły próbę czasu i wciąż zalecane są w konkretnych aplikacjach.
Szczególnie trudnym problemem jest wygenerowanie przez komputer losowych, niedających się przewidzieć wartości. W tym celu system operacyjny próbuje pozyskać losowość ze źródeł zewnętrznych, takich jak ruch myszy czy też parametry pracy niektórych urządzeń, w których taka losowość może się przejawiać (talerzowy dysk twardy, wartość napięcia prądu zasilającego CPU itp.). W ten sposób jądro systemu dysponuje pewną pulą entropii, która może zaspokoić niewygórowane potrzeby, ale w przypadku kiedy wymagane jest wytworzenie dużej ilości (jak gdyby) losowych danych, wykorzystuje się generator liczb pseudolosowych (ang. pseudorandom number generator). Potrafi on „rozciągnąć” stosunkowo niewielką losowość liczoną w bajtach do gigabajtów trudnych do przewidzenia wartości. Taka niewielka, prawdziwie losowa wartość, wykorzystywana przez generator liczb pseudolosowych, nazywana jest ziarnem (ang. seed), a jej jakość jest ściśle powiązana z jakością wygenerowanych następnie danych pseudolosowych. Upraszczając, możemy przyjąć, że ujawnienie wartości ziarna lub zastosowanie łatwego do odgadnięcia ziarna naraża bezpieczeństwo danych wygenerowanych przez PRNG.
Uzbrojeni w tę wiedzę, możemy odnieść się do sposobu wytworzenia wartości pieprzu w Moodle. Autorzy tego systemu zdecydowali się na implementację kryptografii „po swojemu”, zamiast podążać utartymi ścieżkami, a do tego wykorzystali funkcje, które dokumentacja języka PHP odradza.
Inicjalizacja stanu początkowego generatora liczb pseudolosowych to majstersztyk:
mt_srand ((double) microtime() * 1000000);
Funkcja microtime()
zwraca czas w formacie unix z dokładnością do mikrosekund, a typem zwracanej wartości jest string, np. 0.18150100 1588015096
(mikrosekundy\sekundy). W kolejnym kroku następuje rzutowanie tej wartości do liczby rzeczywistej, którego konsekwencją jest utracenie wartości po spacji. Ostatecznie po wykonaniu mnożenia wynikiem powyższych operacji jest liczba całkowita 181501
. Łatwo zauważyć, że generowana wartość zawsze będzie zawierała się w przedziale 0 ÷ 999 999, a więc generator liczb pseudolosowych zostanie zainicjalizowany (tylko) na jeden z miliona sposobów.
W dokumentacji funkcji mt_srand()
znajduje się informacja o tym, że PRNG jest inicjalizowany automatycznie i nie ma potrzeby robić tego w sposób jawny przed jego użyciem. W takim przypadku wygenerowane zostanie losowe ziarno o entropii 32 bitów, chyba że interpreter zostanie uruchomiony w architekturze 64-bitowej, wtedy ziarno osiągnie entropię 64 bitów… chyba że na systemie Windows, wtedy entropia osiągnie 32 bity… chyba że PHP w wersji co najmniej 7.0 – wtedy nawet Windows potrafi utworzyć zmienną całkowitą o wielkości 64 bitów.
Wygląda więc na to, że wygenerowanie wartości losowej w sposób bezpieczny wymaga od dewelopera PHP znajomości środowiska, na którym aplikacja będzie w przyszłości eksploatowana.
No dobra, ale co z tym pieprzem?
Pieprz generowany jest przy wykorzystaniu pseudolosowych wartości od razu po zainicjalizowaniu PRNG na jeden z (dokładnie) miliona sposobów. Oznacza to, że system Moodle nie jest w stanie wytworzyć więcej niż milion unikatowych wersji pieprzu. Tak naprawdę to jeszcze przed wygenerowaniem pieprzu losowana jest jego długość z przedziału 24 ÷ 32, więc finalnie możemy wygenerować dokładnie 9 milionów unikatowych wartości pieprzu! Nie mniej ani nie więcej.
Chyba że na PHP w wersji 7.1 lub wyżej…
PHP od wersji 7.1 zmienił implementację funkcji rand()
i jest ona teraz aliasem funkcji mt_rand()
, a więc zaraz po zainicjalizowaniu PRNG ziarnem kradniemy trochę pseudolosowości w celu ustalenia wartości zmiennej $length
i od razu potem tworzymy pieprz. W praktyce oznacza to, że korzystając z aktualnej wersji interpretera PHP, da się wygenerować pieprz na jeden z miliona sposobów, ponieważ jego długość też łatwo przewidzieć.
Milion czy 9 milionów – bez znaczenia, dla średniej klasy komputera to maksymalnie kilka sekund pracy. Każda ofiara wycieku, która poznała hasz swojego hasła i pamięta je w formie jawnej może w ciągu chwili poznać wspólny dla wszystkich użytkowników sekret. Niestety odgadnięcie pieprzu nie jest też znacząco trudniejsze dla osób przypadkowych, mających złe intencje.
Jak można było zrobić to lepiej?
13 pechowych linijek w funkcji complex_random_string()
zastąpić jedną:
return base64_encode(random_bytes(32));
a hasz hasła utworzyć przy użyciu zalecanej funkcji skrótu.
Komentarze
Albo zamiast random_bytes zamienić cały algorytm na password_hash.
ZOMG
Miałem ochotę napisać, że ich pieprz mógł być w formie md5(cokolwiek), ale jeśli oni za 'cokolwiek’ podstawiają liczbę z przedziału 1..10.000.000, to to nie ma znaczenia. Żeby chociaż użyli md5(tajna_odporna_na_lamanie_fraza) i zaszyli te bity w kodzie…
Naprawdę, nie rozumiem, czemu ludzie biorą się za kryptografię nie rozumiejąc jej :(
Tzn. rozumiem, $$$, ale to jest po prostu smutne :/
md5(’pozdrawiam’);
Myślę, że to nie tyle $$$, co nieświadomość niejednego programisty, że zaprojektowanie i implementacja uwierzytelniania przy użyciu porządnej kryptografii to coś znacznie trudniejszego niż zaprojektowanie i implementacja CRUD.
Chcialem przestac czytac po stwierdzeniu faktu ze hasla byly trzymane jako hash MD5…
No ale przeczytalem calosc.
DEVy chyba pogwalcily wiekszosc podstawowych spraw zwiazanych z bezpiecznym kodowaniem:
– uzywanie MD5, ktory od lat jest odradzany…
– wlasna implementacja kryptografii …
– ignorowanie dokumentacji i rekomendacji tworcow jezyka
Czy można jednocześnie użyć soli i pieprzu? Czy takie rozwiązanie coś da?
mozna, cos da ale niekonieczne w sytuacji kiedy zepsujesz implementacje kazdej z tych rzeczy
Pytanie do fachowców: hasze z solą widziałem – sól jest na początku. Jak jest z pieprzem? Rozumiem, że jest trzymany poza bazą. Pytanie brzmi: czy atakujący może w łatwy sposób poznać, że baza hashy (którą właśnie kradnie) jest bezużyteczna, bo użyty został jeszcze pieprz? Czy dopiero zerkając w kod?
Pytanie2: Czy plik z pieprzem (zakładam, że jest w pliku – poprawcie jeśli nie) trudniej ukraść niż bazę? Czy można go tak umiejscowić, że… nie wiem… że tylko silnik bazy ma do niego dostęp? W jaki sposób powinno się go zabezpieczać?
Jeżeli kradnie kopie zapasową bazy i plików to pozna i sól, i pieprz. Wydaje mi się że przy dużej bazie można poznać że jest pieprzona bo największych wartościach z bazy, kilku-kilkunastu, nie będzie.Ale to będzie tylko pewna poszlaka.
Przy samej soli, to też nie byłoby najczęściej powtarzalnych haszy. Także dzięki za odpowiedź, ale pytanie dalej jest otwarte: jak zabezpiecza się pieprz?