Lekcja 7: (blind) SQL injection
Wprowadzenie
Błędy typu SQL injection wynikają z dwóch głównych przyczyn:
- braku lub niewystarczającej walidacji danych wejściowych,
- braku kodowania znaków specjalnych przed wstawieniem do zapytania SQL,
Błędy tego typu znalazły się zarówno na liście OWASP Top10:Injection Flaws, jak i 2009 CWE/SANS Top 25 Most Dangerous Programming Errors jako CWE-89: Failure to Preserve SQL Query Structure ('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.
Przykład
W przygotowanym przykładzie: http://bootcamp.threats.pl/lesson07/ zadanie polega na odpowiedzi na pytania:
- jaka i w jakiej wersji baza jest wykorzystywana,
- jaka jest struktura bazy,
- dlaczego blind jest w nawiasie,
Sprawdzenie SQL injection
Przykład udaje 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):
- 0+1, 2-1, (...) - jeśli w obu przypadkach zwrócony zostanie wpis o identyfikatorze 1, prawdopodobnie jest SQL injection,
- 1 AND 1=1, 1 AND 1=0 - w pierwszym przypadku powinien się pojawić artykuł o identyfikatorze 1, w drugim - nic (lub jakiś błąd),
- 1 AND 1=(SELECT 1), 1 AND 1=(SELECT 0) - sprawdzenie, czy uda się wykonać subselect,
Ogólna koncepcja wykrywania SQL injection opiera się na doklejeniu fragmentu kodu SQL w celu stwierdzenia, czy zostanie on zinterpretowany w oczekiwany sposób.
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.
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:-1 UNION SELECT null, null, null, null, natomiast -1 UNION SELECT null, null, null lub -1 UNION SELECT null, null, null, null, null zakończą się niepowodzeniem.
W niektórych przypadkach konieczne jest 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 1 ORDER BY 4, jednak 1 ORDER BY 5 zakończy się błędem. 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, co zostało zweryfikowane przy pomocy 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żnaby wykorzytać 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 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)
Wersja z blind
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, który 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' czy id=1 AND (SELECT substr(sqlite_version(),0,1) ) > '9'.
Podsumowanie
Przedstawione zostały tu absolutne podstawy związane z wykorzystaniem podatności typu SQL injection. Proponuję poćwiczyć sobie zwłaszcza wariant związany z Blind SQL injection.
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.

