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 :)
A gdyby w MySQLu dorzucić na końcu:
order by password desc
to powinno zadziałać :}
Na początek, bardzo fajny kurs:)
Druga sprawa, że walczę cały dzień z tym wyzwaniem i nie mogę tego ogarnąć.
Próbuję pobrać hash dla użytkownika admin czymś takim:
admin' union select password form users where login='admin
Czyli to drugie zapytanie by wyglądało w ten sposób, że:
select password from users WHERE login='admin' UNION SELECT password form users where login='admin';
Nie mam pojęcia czemu to nei działa.
Przykłady w tym wpisie dotyczą "wstrzyknięcia" hasha znanego hasła, ale nie do poznania tych hashy, które istnieją w systemie.
To ja też się pochwalę swoim rozwiązaniem .
Przez 2 dni z nim walczyłem, i dopiero teraz się udało (Paweł, ostro musiałem Ci przy tym serwer pytaniami bombardować ).
Ja sobie napisałem funkcję, którą narzędziem developerskim (np. firebug'iem) można wstrzyknąć i odpalić:
function getPass(user){
var oReq = new XMLHttpRequest();
var params1 = "login=";
var params2 = "&password=";
var params3 = "&token=";
var tmpParams = "";
var pass = "";
var chars = ["0","1","2","3","4","5","6","7","8","9"
,"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"
,"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];
var login1 = user + "' AND (SELECT substr(password,";
var login2 = ",1) from users where login='" + user + "') = '";
var login3 = "' --";
for (i=0; i
Dzięki za zadanie, bo jak dla mnie było mistrzowskie. Nauczyłem się z niego dużo .
Mam jeszcze pytanie: czy można było jakoś inaczej wyciągnąć hasło niż znak po znaku odpytywac?
Pozdrawiam!