Wpis z mikrobloga

#programowanie #zagadkihakerskie #sqlinjection

Ostatnio rzuciłem wyzwanie, żeby wykorzystując błąd typu SQL Injection, wykraść hasło użytkownika admin ze strony http://hakerium.cba.pl/zad1/.

Niestety, wyniki okazały się dość słabe, stąd podsyłam krok po kroku jak mógłby wyglądać tok myślenia osoby, która chciałaby przełamać zabezpieczenie tej strony.

Przede wszystkim, w przypadku testowania normalnej strony nie ma takiej sytuacji, jaka była tutaj, tj. że z góry było wiadomo, że podatność na SQL Injection istnieje. Najpierw trzeba ją zweryfikować i w tym celu trzeba będzie wykonać kilka prostych zapytań.

Mamy pewność, że istnieje użytkownik

admin
, a więc wpiszmy taką nazwę użytkownika z dowolnym hasłem.

W odpowiedzi dostaniemy Nieudane logowanie.

Wpiszmy zatem inną nazwę użytkownika; taką, która z całą pewnością w systemie nie będzie istnieć, np.

abcsfds
.

W odpowiedzi dostaniemy Nieudane logowanie

Zauważamy, że w poprzednim przypadku na końcu komunikatu była kropka, a teraz jej nie ma. Wniosek z tego jest taki, że strona odpowiada innym komunikatem o błędzie w zależności od tego jaka jest przyczyna.

Nie wiemy jak wygląda zapytanie SQL-owe po stronie serwera, ale możemy się spodziewać, że będzie w nim kod podobny do:

SELECT * FROM users WHERE username='$username'
Gdyby jako nazwę użytkownika podać

ad'+'min
to zapytanie zamieni się w:

SELECT * FROM users WHERE username='ad'+'min'
. Okazuje się, że w tym przypadku też dostaniemy Nieudane logowanie. a więc odpowiedź z kropką na końcu.

Wpiszmy jako nazwę użytkownika coś co zawiera jakiś większy fragment zapytania SQL-owego np.

admin' OR '1'='1
, wówczas mamy zapytanie

SELECT * FROM users WHERE username='admin OR '1'='1'
. Ten drugi warunek będzie zawsze prawdziwy. Kolejny raz będzie odpowiedź z kropką na końcu. A co się stanie, gdy damy zamiast tego

admin' AND '1'='2
(a więc zapytanie zawsze fałszywe)? Odpowiedź będzie Nieudane logowanie czyli znów bez kropki.

Możemy już spokojnie wyciągnąć wniosek: strona zwraca Nieudane logowanie. gdy zapytanie SQL-owe zwróciło co najmniej jeden wiersz lub Nieudane logowanie gdy zapytanie zwraca zero wierszy. Kolejnym krokiem jest jeszcze sprawdzenie jak się strona zachowa dla zapytań, które nie są poprawnym SQL-em, wpiszmy np.

admin' ANDdd '1'='2
z czego będzie zapytanie

SELECT * FROM users WHERE username='admin' ANDdd '1'=2'
. Po raz kolejny mamy Nieudane logowanie - komunikat bez kropki.

MySQL ma taką specyfikę, że jeśli zapytanie jest poprawne syntaktycznie, ale zawiera na przykład nieistniejącą nazwę kolumny, to też wtedy zwraca błąd. Możemy takie zachowanie wykorzystać do sprawdzenia jakie kolumny istnieją w naszej tablicy. Pokażę to na przykładzie, jeśli mamy zapytanie:

SELECT * FROM users WHERE username='admin' OR 1=1 OR ''
, dostaniemy wtedy wynik z kropką (a więc zostanie coś zwrócone). Jeśli zamienimy

1=1
na

user=user
to zapytanie ma dokładnie taki sam sens z jedną różnicą - nie wykona się, jeśli nie istnieje kolumna

user
. A zatem wpiszmy

admin' OR user=user OR'
co da komunikat bez kropki - a więc taka kolumna nie istnieje. Jednak już dla

admin' OR username=username OR '
dostajemy odpowiedź z kropką - czyli trafiliśmy z nazwą kolumny. Na podobnej zasadzie możemy potwierdzić istnienie kolumny

password
, poprzez wpisanie

admin' OR password=password OR'
.

No to przejdźmy wreszcie do konkretów :) Może na początek poznajmy długość hasła tego użytkownika? Skorzystamy z metody

LENGTH
i wpisujemy

admin' AND LENGTH(password)=8 OR '
, z tego powstaje zapytanie:

SELECT * FROM users WHERE username='admin' AND LENGTH(password)=8 OR ''
. Jeśli warunek z długością będzie prawdą to zapytanie zwróci wynik, w przeciwnym przypadku nie zwróci nic. A więc jedziemy:

admin' AND LENGTH(password)=8 OR '
- zwraca Nieudane logowanie

admin' AND LENGTH(password)=9 OR '
- zwraca Nieudane logowanie

admin' AND LENGTH(password)=10 OR '
- zwraca Nieudane logowanie.. Bingo! Wiemy już, że hasło ma 10 liter!

Do wyciągania pojedynczych liter hasła użyjemy metody

SUBSTRING
. Przyjmuje ona trzy parametry: pierwszy określa z czego wyciągamy podciąg (np. nazwę kolumny), drugi parametr określa początek podciągu (indeksowane od 1), trzeci parametr zaś to długość. No to jedziemy:

admin' AND SUBSTRING(password,1,1)='q' OR '
- jeśli pierwszy znak hasła to q - dostaniemy wynik z kropką, w przeciwnym wypadku bez kropki. Okaże się, że pierwszy znak to nie jest q, gdy jednak damy zapytanie:

admin' AND SUBSTRING(password,1,1)='w' OR '
- jest odpowiedź Nieudane logowanie.. Pierwszy znak hasła to w. Warto jednak zdać sobie sprawę z tego, że porównywanie ciągów znaków w MySQL domyślnie jest niewrażliwe jak wielkość liter. Nie wiemy więc, czy pierwszy znak to W czy w. Aby to naprawić, dodajmy słówko

BINARY
.

admin' AND SUBSTRING(password,1,1) = BINARY 'w' OR'
- Nieudane logowanie

admin' AND SUBSTRING(password,1,1) = BINARY 'W' OR'
- Nieudane logowanie. - a więc tak naprawdę pierwszą literą hasła jest duże W. Gdy będziemy teraz chcieli wyciągnąć drugi znak hasła, to zmieniamy drugi parametr w

SUBSTRING
na

2
. Oczywiście wyciąganie całych haseł w ten sposób ręcznie jest bardzo pracochłonne i długotrwałe, stąd znacznie prościej napisać sobie do tego skrypt. Oto prosty skrypt w Pythonie, który napisałem w tym celu. Po jego odpaleniu dowiemy się, że całe hasło to WyKoP!@123 i że skrypt potrzebował wykonać 398 zapytań HTTP, by je odkryć (nie jest to strasznie duża liczba).

A więc wracamy na stronę, wpisujemy user: admin, pass: WyKoP!@123 i udało się! Zalogowany jako admin! :)

Jeśli macie jakieś pytania, pytajcie bez skrępowania ;)
  • 11
  • Odpowiedz
Taki sposób przeprowadzania SQL Injectiona nazywa się blind SQL Injection i polega na tym, że trzeba zadawać pytania, na które odpowiedź brzmi PRAWDA/FAŁSZ i analizować odpowiedzi serwera. Krótki opis znajdziecie na tej stronie w rozdziale "Wykorzystanie SQL injection - wersja blind SQL injection".

Dodatkowe zadanie dla chętnych: w bazie istnieje jeszcze jeden użytkownik. Jaką ma nazwę i jakie ma hasło? :)
  • Odpowiedz
@mpisz: zakładając, że całe zapytanie wyglądałoby tak:

SELECT * FROM users WHERE username='$username'
(tak naprawdę wygląda inaczej, ale jest to uproszczenie, które spokojnie może być tutaj użyte), Twoja propozycja tworzy coś takiego:

SELECT * FROM users WHERE username='admin' AND username like
  • Odpowiedz
Ochrona przed zagrożeniem:

Tak naprawdę istotą tej zagadki było pokazanie, że nawet jeśli informacje ze strony serwera są skąpe, to na podstawie obserwacji jego zachowania w różnych sytuacjach można z bazy wyciągnąć dane jeśli istnieje podatność na SQL Injection.

A jak się przed SQL Injection bronić? Zawsze mówiło się o potrzebie odpowiedniego enkodowania danych przed wrzuceniem ich do bazy. Najlepszą jednak metodą i najpewniejszą jest stosowanie parametryzowanych zapytań (prepared statements). Każdy liczący się język i silnik bazy danych ma mechanizmy do tego. Wówczas wy już praktycznie nie musicie się przejmować odpowiednim filtrowaniem danych, ale zrobi to za was silnik baz danych. Ogólnie rzecz biorąc polega to na tym, że wy przygotowujecie zapytanie SQL-owe gdzie w miejsce parametrów wpisuje się pytajniki (lub nazwy tych parametrów, zależy od konkretnej implementacji) i potem podpinacie odpowiednie wartości. Przykład oparty na PHP:

$stmt
  • Odpowiedz