threats.pl > Bezpieczeństwo aplikacji internetowych > Lekcja 7: (blind) SQL injection

Lekcja 7: (blind) SQL injection

Wprowadzenie

Błędy typu SQL injection wynikają z dwóch głównych przyczyn:

Błędy tego typu znalazły się zarówno na liście OWASP Top10: Injection Flaws, jak i 2010 CWE/SANS Top 25 Most Dangerous Software Errors jako CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').

Skutkiem takiego błędu może być ujawnienie, modyfikacja lub usunięcie danych. Atakujący może również wykorzystać błąd tego typu do przejęcia serwera, na którym zainstalowana jest baza danych, na przykład poprzez wykonanie poleceń systemu operacyjnego za pośrednictwem SQL injection.

Różnica między "zwykłym" a blind SQL injection

Zarówno "zwykłe", jak i blind SQL injection polega na takim zmodyfikowaniu zapytania wykonywanego przez bazę danych, by zapytanie to wykonało akcję (np. pobrało dane) pożądaną przez atakującego. Różnica między "zwykłym" a blind SQL Injection polega głównie na sposobie prezentowania wyników zapytania.

W przypadku "zwykłego" (tradycyjnego) SQL injection wynik zapytania prezentowany jest na stronie w sposób "jawny". Cel ten może zostać osiągnięty między innymi poprzez:

Blind SQL injection dostarcza jedynie pośredniej informacji na temat rezultatu wykonanego zapytania. W szczególności chodzi tu o informację, czy zapytanie zakończyło się sukcesem, czy niepowodzeniem. Więcej informacji na temat specyfiki blind SQL injection znajduje się we wpisie: O blind sql injection.

Przykład

W przygotowanym przykładzie: http://bootcamp.threats.pl/lesson07/ zadanie polega na odpowiedzi na pytania:

Testowanie podatności na SQL injection

Przygotowany na potrzeby tej lekcji przykład symuluje stronę z informacjami/artykułami, gdzie artykuł identyfikowany jest przez wartość liczbową przekazywaną w parametrze id. Dla uproszczenia wykorzystana jest metoda GET, nie wprowadziłem również żadnych losowych tokenów, które wykorzystanie podatności mogłyby utrudnić.

Istnieje kilka sposobów sprawdzenia, czy istnieje podatność typu SQL injection, przykładowo (wartości parametru id):

Ogólna koncepcja wykrywania SQL injection opiera się na doklejeniu fragmentu kodu SQL w celu stwierdzenia, czy zostanie on zinterpretowany w oczekiwany sposób. Więcej przykładów odnośnie testowania podatności aplikacji na (blind) SQL injection zawartych jest we wpisie na blogu Jak szukać SQLi - przykład oraz w Wyzwanie V - wskazówki, część druga. W ramach ciekawostki zobacz też:

W zależności od wykorzystanej bazy danych może być konieczne podanie nazwy tabeli, z której SELECT ma zostać wykonany. Tu z pomocą przychodzi pseudotabela o nazwie DUAL. Fakt wymagania bądź niewymagania podania nazwy tabeli może być wykorzystany przy określeniu typu bazy danych.

Wykorzystanie SQL Injection - wersja z UNION SELECT

Wykorzystanie podatności SQL injection poprzez UNION SELECT polega na dopisaniu drugiego zapytania SQL, którego rezultat (zwrócone rekordy) będzie połączony z zapytaniem oryginalnym. Przykładowo zapytanie typu:

SELECT * FROM tabela WHERE id=$parametr

gdzie $parametr jest podatny na SQL injection może zostać sprowadzone do postaci:

SELECT * FROM tabela WHERE id=-1 UNION SELECT * FROM tabela1 

By ta składnia zadziałała konieczne jest, by:

Z tego powodu atak z wykorzystaniem metody UNION SELECT musi zawierać:

W przypadku niektórych baz danych dobranie odpowiednich typów może nie być kluczowe, bazy te mogą dokonywać automatycznej konwersji na pożądany typ danych, na przykład dla sqlite:

sqlite> select 1,2,3 union select 'a','b',1;
1|2|3
a|b|1

Określenie ilości kolumn

Przed wykorzystaniem podatności SQL injection z wykorzystaniem UNION SELECT potrzebne jest określenie ilości kolumn, które zapytanie zwraca. Zwykle wykonuje się to poprzez konstrukcję typu:

UNION SELECT null, null, null (...)

gdzie ilość null odpowiada ilości kolumn. W chwili, gdy ilość kolumn w części UNION SELECT zostanie dobrana prawidłowa, zapytanie powiedzie się. Warto przy tym jako wartość parametru id podać wartość nieistniejącej informacji, tak, by wypisane na ekran zostało to, co zwraca część z UNION SELECT. W omawianym przypadku powodzeniem zakończy się zapytanie postaci (mowa o wartości podatnego parametru, nie całym zapytaniu SQL):

-1 UNION SELECT null, null, null, null

natomiast poniższe zapytania zakończą się niepowodzeniem:

-1 UNION SELECT null, null, null
-1 UNION SELECT null, null, null, null, null

W niektórych przypadkach konieczne jest również określenie typów poszczególnych kolumn. Akurat wartość null jest o tyle sympatyczna, że rzutuje się zwykle na dowolny typ. Określanie typów polega po prostu na wstawianiu na określonej pozycji danych konkretnego typu i sprawdzeniu, kiedy zapytanie się wykona. W przypadku części baz rzutowania dokonują się automatycznie i krok z określaniem typów nie jest potrzebny.

Inną metodą ustalenia ilości kolumn w tabeli, z której wykonywany jest SELECT jest wykorzystanie ORDER BY. Zamiast nazwy kolumny można podać identyfikator kolumny (poczynając od 1). Ilość kolumn w tabeli nie musi się wprost przekładać na ilość kolumn w rezultacie zapytania, informacja ta może być jednak przydatna. Podanie identyfikatora kolumny, która nie istnieje (czyli większego od ilości kolumn) spowoduje błąd. W przypadku omawianego przykładu zadziała zapytanie typu:

1 ORDER BY 4

Błędem natomiast zakończy się zapytanie postaci:

1 ORDER BY 5

Wynika z tego, że w tabeli, na której wykonywany jest SELECT znajdują się cztery kolumny, co jest zgodne z ilością kolumn w rezultatach zapytania SQL i co zostało zweryfikowane przy pomocy techniki UNION SELECT null, (...).

Określenie wersji bazy danych

Wiele osób próbowało wykorzystać narzędzie sqlmap, które nie potrafiło jednak określić wykorzystanej wersji bazy. Jest to przesłanka sugerująca, że wykorzystana jest baza nieobsługiwana przez to narzędzie. Odrzucając MySQL, MS SQL, PostreSQL oraz Oracle i biorąc pod uwagę wykorzystaną technologię oraz hosting, można przypuszczać, że wykorzystany będzie sqlite.

W celu zweryfikowania tego domysłu można posłużyć się specyficzną dla tej bazy funkcją, po którą wystarczy sięgnąć do dokumentacji: Core Functions. Dobrym kandydatem wydaje się funkcja sqlite_version(). Idąc po najmniejszej linii oporu: sprawdzanie funkcji sqlite_version(). Pozwala to na odpowiedź na jedno z pytań: wykorzystana jest baza sqlite w wersji 3.3.7.

Struktura bazy danych

Jeśli byłaby to "zwykła" baza danych, do określenia jej struktury można byłoby wykorzystać INFORMATION_SCHEMA, jednak w przypadku sqlite takiej możliwości nie ma. Zamiast tego trzeba skorzystać z nieco innej metody: How do I list all tables/indices contained in an SQLite database.

Pozyskiwanie informacji o strukturze bazy można rozpocząć od ustalenia ilości tabel. W tym celu można wykonać następujące zapytanie:

-1 UNION SELECT null, null, null, (SELECT count(*) FROM sqlite_master WHERE type='table')

Rezultat tego zapytania to 1, czyli w bazie istnieje tylko jedna tabela, a skoro tak, to jej strukturę można pozyskać poprzez zapytanie postaci:

-1 UNION SELECT null, null, null, sql from sqlite_master

Odpowiedź na pytanie o strukturę bazy danych wyraża się więc poprzez następujący kod SQL:

CREATE TABLE news (id INTEGER PRIMARY KEY, message TEXT, public NUMERIC, title TEXT)

Wykorzystanie SQL injection - wersja blind SQL injection

Blind był w nawiasie, bo nie ma konieczności wykorzystywania tej metody. W praktyce jednak może ona być wygodniejsza od metody z UNION SELECT głównie dlatego, że łatwiejsze może być dobranie odpowiedniej składni SQL, szczególnie w przypadku, gdy injection znajduje się w bardziej złożonym zapytaniu.

Koncepcja Blind SQL injection jest prosta - wystarczy zadawać pytania, na które otrzymuje się odpowiedź typu tak/nie. Dla przykładu funkcja sqlite_version() zwraca string, który zawiera informację o wersji bazy. Długość tego stringu można ustalić z wykorzystaniem funkcji length, a konkretniej przy pomocy zapytania postaci:

1 AND (SELECT length(sqlite_version())) > 0

W tym przypadku wyświetlona zostanie treść artykułu, ponieważ długość stringu zawierającego wersję bazy jest większa od zera. Jeśli jednak zamiast 0 wstawi się wartość 10, warunek będzie miał wartość false, a co za tym idzie treść artykułu nie zostanie wyświetlona. Stosując metodę bisekcji (metodę równego podziału) dochodzi się do warunku postaci:

1 AND (SELECT length(sqlite_version())) = 5

Warunek ten powoduje wyświetlenie oczekiwanego artykułu, co oznacza, że szukana długość stringu to 5.

W analogiczny sposób można określić jaki znak występuje na określonej pozycji. W tym celu można wykorzystać zapytania postaci:

1 AND (SELECT substr(sqlite_version(),0,1) ) > '0'
1 AND (SELECT substr(sqlite_version(),0,1) ) > '9'

Podsumowanie

Przedstawione zostały tu absolutne podstawy związane z badaniem podatności aplikacji na SQL injection oraz wykorzystaniem tej podatności. Proponuję poćwiczyć sobie zwłaszcza wariant związany z Blind SQL injection. Zapraszam również do rozwiązywania pozostałych zadań związanych z tematem SQL injection:

Zapraszam również do obejrzenia videocastów:

Gratulacje

Gratulacje dla Krzyśka Kotowicza, który już 10 maja zgłosił prawidłowe rozwiązanie. Posłużył się przy tym metodą z UNION SELECT. Warto zapoznać się również z jego prezentacją Przewodnik po SQL injection dla developerów PHP.