There's more than one way to skin a cat

Rzecz dotyczy udostępnionego przeze mnie już jakiś czas temu wyzwania. Do tej pory wszystkie rozwiązania były zgodne z moim założeniem. Za pomocą blind SQLi rozpoznawana była struktura bazy danych, z odpowiedniej kolumny pobierany był hash hasła dla konkretnego użytkownika, hash ten był następnie w odpowiedni sposób wykorzystywany. Właśnie to odpowiednie wykorzystanie pobranego z bazy hasha było, w moim zamierzeniu, głównym elementem wyzwania. Wszystko po to, by pokazać, że nie wystarczy hashować hasła, trzeba jeszcze robić to z głową.

Dziś otrzymałem rozwiązanie, które wykracza poza ten schemat. Warto mu się przyjrzeć, bo jest nie tylko ciekawe, ale i w pewnym stopniu... przypadkowe.

Na początek rozwiązanie podane przez Dariusza:

Login: admin' union select '5f4dcc3b5aa765d61d8327deb882cf99 Hasło: password

Jak widać rozwiązanie jest genialne w swej prostocie, tylko... dlaczego działa? I dlaczego nie działa następujący wariant:

Login: ' union select '5f4dcc3b5aa765d61d8327deb882cf99 Hasło: password

Na początku wyjaśnię to drugie pytanie. W przykładzie zostały wykorzystane dwa zapytania SQL. Pierwsze, które sprawdza, czy dany użytkownik istnieje i drugie, które jest wykorzystywane w operacji weryfikacji jego hasła. Pierwsze zapytanie w uproszczeniu wygląda następująco (SQLi w parametrze $login):

SELECT count(*) FROM users WHERE login='$login'

Rezultat tego zapytania pobierany jest przy pomocy funkcji fetch i sprawdzana jest jego wartość. Jeśli jest ona równa 1, wówczas oznacza to, że użytkownik istnieje, jeśli nie – logika funkcji uwierzytelnienia zostaje przerwana. Z tego powodu bez podania nazwy istniejącego(!) użytkownika, całość nie zadziała.

Drugie zapytanie również jest bardzo proste, wygląda w sposób następujący (ponownie SQLi w parametrze $login):

SELECT password FROM users WHERE login='$login'

Rezultat tego zapytania pobierany jest przy pomocy funkcji fetchColumn i zwrócona w pierwszym rekordzie wartość hasła (właściwie hasha hasła) jest wykorzystywana w procesie uwierzytelnienia. I właśnie ta przypadkowość rozwiązania, o której pisałem, zawiera się w tym, co zostanie zwrócone jako pierwszy rekord.

Patrząc bezpośrednio na poziomie bazy danych (SQLite):

sqlite> select password from users WHERE login='admin' UNION SELECT '5f4dcc3b5aa765d61d8327deb882cf99'; 5f4dcc3b5aa765d61d8327deb882cf99 8804804c508f765f22903be5a9a6bb67

Zgodnie z oczekiwaniami zwracane są dwie wartości hasha hasła, przy czym wartość wstrzyknięta przy pomocy UNION SELECT jest zwracana na pierwszej pozycji. Z tego powodu jest wykorzystywana w dalszym procesie uwierzytelnienia. Dzieje się tak, ponieważ SQLite niejawnie sortuje rezultaty zapytania:

sqlite> SELECT 2 UNION SELECT 1; 1 2

Analogiczne zapytanie w MySQL zwraca już inną kolejność rekordów:

mysql> SELECT 2 FROM DUAL UNION SELECT 1 FROM DUAL; +—–+ | 2 | +—–+ | 2 | | 1 | +—–+ 2 rows in set (0.00 sec)

Innymi słowy, gdyby w przykładzie wykorzystana została baza MySQL a nie SQLite, wówczas zgłoszone rozwiązanie najprawdopodobniej by nie zadziałało. Po prostu wstrzyknięty hash hasła nie byłby uwzględniony w procesie uwierzytelnienia.

Wykorzystanie w przykładzie SQLite to jednak tylko jeden z dwóch elementów, które były potrzebne do zadziałania tego przykładu. Proponuję sprawdzić to namacalnie z wykorzystaniem dwóch prostych haseł, a oraz b.

a: 0cc175b9c0f1b6a831c399e269772661 b: 92eb5ffee6ae2fec3ad71c777531578f

Jak łatwo sprawdzić, przykład z hasłem a zadziała, w przypadku b jednak próba uwierzytelnienia kończy się niepowodzeniem. Powód jest już chyba oczywisty, ale na wszelki wypadek jeszcze raz można rzucić okiem na wyniki zapytań:

sqlite> select password from users WHERE login='admin' UNION SELECT '0cc175b9c0f1b6a831c399e269772661'; 0cc175b9c0f1b6a831c399e269772661 8804804c508f765f22903be5a9a6bb67

sqlite> select password from users WHERE login='admin' UNION SELECT '92eb5ffee6ae2fec3ad71c777531578f'; 8804804c508f765f22903be5a9a6bb67 92eb5ffee6ae2fec3ad71c777531578f

W pierwszym przypadku próba uwierzytelnienia kończy się sukcesem, ponieważ tak się składa, że md5(a) jest “mniejszy” niż hash hasła użytkownika admin, więc znajduje się na pierwszej pozycji. W drugim przypadku md5(b) jest już “większe” niż hash i znajduje się na drugiej pozycji, w związku z czym to oryginalny hash, a nie ten wstrzyknięty przez UNION SELECT zostanie wykorzystany w operacji uwierzytelnienia.

Tak jak kiedyś pisałem, podatności stają się oczywiste, jak się je już znajdzie. Czasami nawet jak się je znajdzie, wcale nie są takie oczywiste.

Przy okazji ten przykład i to konkretne jego rozwiązanie pokazuje, jak wiele czasami zależy od szczęścia. Gdyby atakowany użytkownik (admin) miał inny hash hasła, albo gdyby próbowane było inne hasło, wówczas to rozwiązanie pozostałoby nieodkryte. Zresztą sprawdźcie sami. W przykładzie istnieją użytkownicy:

Na część z nich można się zalogować korzystając z przykładu Dariusza, na część nie. Sprawdźcie też podane przeze mnie przykładowe hasła a oraz b. Nie wiem jak Wam, ale mnie ten przykład się wyjątkowo podoba :)

Oryginał tego wpisu dostępny jest pod adresem There's more than one way to skin a cat

Autor: Paweł Goleń