Dzisiaj kontynuacja cyklu "zrób to sam w weekend". Tym razem przykład jak szukać błędów typu sql injection (patrz: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')) Podobnie jak ostatnim razem wpis nie będzie zawierał jakiejś wiedzy tajemnej. Chcę po prostu pokazać w jaki sposób błędy typu SQLi można w stosunkowo prosty sposób zidentyfikować. Oczywiście przy użyciu tej techniki nie da się znaleźć 100% błędów, ale przynajmniej całkiem sporą ich ilość. Nie jest to podejście typu fire and forget, wymaga trochę myślenia, ale to raczej jego zaleta, niż wada. No i każdy może w stosunkowo prosty sposób dostosować to podejście do swoich potrzeb.
Jak szukać SQLi - przykład
Nasza ofiara
Tym razem w roli ofiary wystąpi gościnnie przykład wykorzystany w ramach lekcji Więcej niż jedna ścieżka wykonania. Przykładowa aplikacja dostępna jest pod adresem http://bootcamp.threats.pl/lesson18/. Podobnie jak poprzednim razem, tym razem również dla każdej sesji dane w niej mogą wyglądać nieco inaczej.
To, co będziemy testować, to formatka wyszukiwania. Wygląda ona tak:

Jak działa ta formatka
Warto zastanowić się, czy wszystkie kombinacje parametrów mają sens. Jako wskazówkę polecam stary wpis Bootcamp XVIII: wskazówek część dalsza.
Na początek dobierzemy jednak jakieś sensowne parametry, które działają. W tym przypadku wybiorę wiadomość o identyfikatorze 1542446130 (wiem, że istnieje). Wówczas całość wygląda w sposób następujący:

W tym wypadku wysłane żądanie HTTP wyglądał tak:
POST http://bootcamp.threats.pl/lesson18/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded Cookie: PHPSESSID=7d4754fcdbc17bdd694fe703695989ec; action=search&id=1542446130&text=&type=0
Podpowiem, że w przypadku, gdy parametr id ma wartość, wartości parametrów text oraz type są kompletnie nieistotne, co każdy może sprawdzić we własnym zakresie.
Drugim żądaniem, które zwróci dokładnie ten sam rezultat, jest żądanie z ustawioną wartością tytułu, lub jej fragmentem. W tym przypadku będzie to ponownie fragment 1542446130, bo w tym przypadku identyfikator wiadomości również występuje w jej tytule. W tym przypadku żądanie HTTP wygląda następująco:
POST http://bootcamp.threats.pl/lesson18/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded Cookie: PHPSESSID=7d4754fcdbc17bdd694fe703695989ec; action=search&id=&text=1542446130&type=0
W tym przypadku wartość parametru type jest oczywiście istotna. Gdyby została ona ustawiona na 1 (wiadomość publiczna), zapytanie nie zwróciłoby rekordów, wiadomość zawierająca 1542446130 w tytule jest wiadomością prywatną.
Można jeszcze sprawdzić co się stanie, gdy do tej formatki w parametrze type zostanie przekazana wartość pusta (lub parametr zostanie usunięty). Można też sprawdzić co się stanie, jeśli w parametrze type przekazana zostanie wartość spoza zakresu 0 i 1. Dla potrzeb dalszych rozważań ograniczymy się jednak tylko do tych dwóch przykładowych żądań, które będziemy dalej fuzzować.
Jak będziemy szukać SQLi
Tym razem nasz fuzzing będzie polegał na modyfikacji wartości parametrów poprzez dodanie do nich pewnych ciągów znaków. Będą to następujące wartości (celowane w stringa):
'||' ' OR 1=1 -- ' OR 'A'='A ' AND 1=1 -- ' AND 'A'='A
I kilka wartości celowanych w wartość liczbową:
+0 -0 /1 /0
Dlaczego akurat takie wartości? Zastanówmy się nad tym na przykładzie takich dwóch zapytań:
SELECT * FROM tabela WHERE parametr='+value+' SELECT * FROM tabela WHERE (parametr='+value+' AND parametr2 > 3)
Wartością, którą będziemy modyfikować niech będzie test. W pierwszym przypadku nasz payload przyjmuje postać test'||'. Po wstawieniu tych wartości do zapytań przyjmą one następującą postać:
SELECT * FROM tabela WHERE parametr='test'||'' SELECT * FROM tabela WHERE (parametr='test'||'' AND parametr2 > 3)
Czy są one prawidłowe? Tak, ponieważ || jest operatorem konkatenacji, czyli na poziomie SQL pojawi się po prostu łączenie dwóch tekstów, jeden o wartości test, a drugi jest pusty. Rezultatem tego łączenia jest więc znów tekst test. Jeśli dane zwracane przez aplikację dla wartości test oraz test'||' są identyczne, może to sugerować SQLi.
Teraz zastanówmy się na drugim z kolei przykładem. Po jego wstawieniu do przykładowych zapytań, przyjmują one następującą postać:
SELECT * FROM tabela WHERE parametr='test' OR 1=1 --' SELECT * FROM tabela WHERE (parametr='test' OR 1=1 --' AND parametr2 > 3)
Tu sytuacja jest nieco inna. W tym przypadku tylko jedno z wynikowych zapytań jest prawidłowe, to pierwsze. W tym drugim komentarz odcina nie tylko niechciany znak ', ale również dalszą część zapytania, w szczególności zaś znak ). W rezultacie zapytanie zawiera nawias otwierający (, ale nie ma jego zamknięcia co czyni całe zapytanie SQL nieprawidłowym. Co się w tym przypadku stanie? To zależy, w bardzo sprzyjającej sytuacji otrzymamy komunikat błędu z bazy danych, w przypadku mniej pomyślnym - nie będzie różnicy między zapytaniem nieprawidłowym a zapytaniem, które nie zwraca żadnych danych.
Problem z nieprawidłową składnią SQL rozwiązuje kolejny payload. Po jego wstawieniu zapytania otrzymują postać:
SELECT * FROM tabela WHERE parametr='test' OR 'A'='A' SELECT * FROM tabela WHERE (parametr='test' OR 'A'='A' AND parametr2 > 3)
W tym przypadku "niepotrzebny" znak ', który poprzednim razem był neutralizowany przez komentarz, tym razem jest "zagospodarowywany" przez fragment 'A'='A. Przy okazji przypomnę, że OR 1=1 (lub równoważne) czasami wywołuje niepożądane skutki, dlatego też wariant z AND zamiast OR.
Co z atakami na wartość liczbową? Są one chyba dość łatwo zrozumiałe. W zapytaniu, w którego parametrze wartości 1, 1+0, 1-0 oraz 1/1 dają ten sam rezultat, mamy prawdopodobnie SQLi. A 1/0 to dość prosty sposób na wywołanie błędu w aplikacji - dzielenie przez zero. Oczywiście nie w każdej bazie danych błąd taki występuje, patrz sqlite.
No to do roboty
Do szukania SQLi wykorzystam mój fuzzer i nieco poszerzonego przeze mnie Fiddlera. Zasada działania "mojego" fuzzera jest dość prosta. Na wejściu dostaje on URL (wraz z query) oraz dane przesyłane w POST (jeśli takie są). Dla każdego parametru z query lub POST (o ile nazwa tego parametru nie znajduje się na liście "zakazanych") generowane jest żądanie, które modyfikowane jest w opisany przeze mnie sposób. Dodatkowo fuzzer ten na początku generuje żądanie bez modyfikacji i wstawia do niego nagłówek x-base-request, a w trakcie "normalnej" pracy wstawia nagłówek x-fuzzed-param z nazwą modyfikowanego parametru. Jest to trochę rozbudowana wersja skryptu, który pokazywałem poprzednim razem, nie zamierzam się jednak nią dzielić, przynajmniej teraz.
Moje rozszerzenia (a robi się je łatwo) do Fiddlera są również dość proste. W tym przypadku wykorzystałem:
- liczenie SHA1 z odpowiedzi serwera (bez nagłówka),
- kopiowanie wartości dowolnej flagi do kolumny Custom,
- kolorowanie sesji w zależności od wartości w kolumnie Custom,
Dla zainteresowanych rezultaty fuzzingu dla obu zestawów parametrów udostępniam w formacie zrozumiałym dla Fiddlera oczywiście:
Rezultat fuzzingu pierwszego zestawu parametrów jest taki sobie. Dla przypomnienia, zestaw ten wyglądał tak:
action=search&id=1542446130&text=&type=0
A tak wyglądają sesje w Fiddlerze po policzeniu SHA1 i ich pokolorowaniu:

Pierwsza z sesji, ta oznaczona na czerwono i pogrubiona, to sesja "bazowa", oryginalne żądanie, do którego nie zostały wprowadzone żadne zmiany. Odpowiedzi serwera dla wszystkich kolejnych żądań jest identyczna. Widać to zarówno po rozmiarze odpowiedzi, sumie SHA1 i kolorze.
Jak to jest możliwe? Co prawda parametr action nie był fuzzowany a parametry text oraz type są w tym przypadku nieistotne, ale co z modyfikacjami parametru id. Ma ktoś jakiś pomysł?
Ciekawiej przedstawia się rezultat dla drugiego przypadku, czyli dla tego zestawu parametrów:
action=search&id=&text=1542446130&type=0
W tym przypadku sesje po wyliczeniu ich SHA1, pokolorowaniu i posortowaniu wyglądają w sposób następujący:

Widać wyraźnie, że coś się zmieniło, są trzy grupy odpowiedzi. Wyglądają one następująco (w kolejności):

Warto zwrócić uwagę, że pierwsza grupa (patrz wyżej) jest identyczna z odpowiedzią serwera na zapytanie "bazowe". Można przypuszczać więc, że w tym wypadku zadziałał payload z AND 1=1 (dwa warianty) oraz łączenie stringów (jeden wariant). Po sprawdzeniu okazuje się, że tak jest rzeczywiście. Podatny na SQLi jest parametr type, a działające wartości parametru to:
- 0'||'
- 0' AND 1=1 --
- 0' AND 'A'='A
W przypadku drugiej grupy zapytanie zakończyło się niepowodzeniem. Może nie tyle niepowodzeniem, co nie zwróciło rezultatów.

Trzecia grupa (poniżej) to ewidentnie rezultat zapytania z OR 1=1 (dwa warianty). Zwracanych jest więcej danych, niż w zapytaniu bazowym. W tym wypadku podatny jest również parametr type, a działający payload to:
- 0' OR 1=1 --
- 0' OR 'A'='A

I kilka ćwiczeń na przyszłość
W ramach ćwiczeń trzy dodatkowe przykłady prawie tej samej aplikacji:
- http://bootcamp.threats.pl/lesson18a/
- http://bootcamp.threats.pl/lesson18b/
- http://bootcamp.threats.pl/lesson18c/
Ten drugi i trzeci przykład (lesson18b i lesson18c) pokazują dlaczego opieranie się na SHA1 nie zawsze jest dobrym pomysłem. Poza tym w ramach bonusu wprowadzona jest dodatkowa podatność poza SQLi.
Miłej zabawy :)
EDIT: Kolejny zestaw zapisanych sesji: Wyzwanie V - wskazówki, część druga, dotyczą one wyzwania V z mojego przewodnika po bezpieczeństwie aplikacji internetowych.
{N}-0 = 1542446130-0
{N}+0 = 1542446130+1
{N}/1 = 1542446130/1
Przy założeniu, że SQLi nie ma (wartość jest sanityzowana np. za pomocą funkcji intval (php)) wówczas wszystkie payloady zostaną przekształcone do wartości pierwotnej (1542446130), co z kolei wygeneruje masę fałszywych podejrzeń o SQLi. Z drugiej strony nawet gdy SQLi istnieje nadal nie mamy pewności.
Opisane problemy powinien rozwiązać następujący zestaw:
{N+1}-1 = 1542446131-1
{N-1}+1 = 1542446129+1
{N*2}/2 = 3084892260/2
Przy braku SQLi (zastosowana sanityzacja) odpowiedzi na zapytania powinny być różne od odpowiedzi bazowej. Jeżeli SQLi istnieje odpowiedź będzie taka sama jak odpowiedź bazowa, co z dużym prawdopodobieństwem wskazuje na SQLi.
1542446130
1542446131
1542446130
Czyli pierwsza i trzecia odpowiedź powinna zwrócić te same dane, druga natomiast inne.
Do tego zestawu można jeszcze dodać dzielenie przez zero, co często spowoduje również odmienne działanie aplikacji (np. błąd 500) i co również jest mocną przesłanką do tego, że COŚ się dzieje.
W przypadku, gdy jest (int) w PHP, wszystkie trzy żądania zwrócą te same dane.
W przypadku zaproponowanego przez Ciebie podejścia jest dokładnie odwrotnie. Jeśli jest injection, dostajesz te same dane, jeśli nie ma - różne.
Na podstawie obu zestawów możesz wnioskować odnośnie istnienia podatności. ja wolę podejście pierwsze, bo jakoś łatwiej dla mnie jest interpretować różnice jako przesłankę przemawiającą za istnieniem podatności, niż odwrotnie
- {N}+0 = 1542446130+1
+ {N}+0 = 1542446130+0
zmierzam do tego, że z zaproponowanych przekształceń:
+0
-0
/1
/0
tylko ostanie wskaże potencjalnego SQLi. Pozostałe trzy:
1542446130+0
1542446130-0
1542446130/1
w tym samym stopniu wskazują zarówno na SQLi jak i sanityzację (rzutowanie do int).
Pamiętaj też, że tam jest więcej przykładowych payloadów, na podstawie których można (próbować) określić, czy:
- dane wchodzą do stringa (payloady z ')
- dane nie są w stringu (te wersje bez ')
I tak na przykład jeśli dane wchodzą w stringa, to wszystkie wersje +0, -0 /1 /0 powinny nie zwrócić żadnych danych, bo prawdopodobnie żadne dane nie będą pasować do takich parametrów wyszukiwania.
Jeśli dane są poza stringiem, to masz jeszcze payloady typu {n} AND 1=1 oraz {n} AND 1=0 co w bardzo wielu wypadkach też zadziała, jeśli jest injection.
Poza tym nie chodzi tu o potwierdzenie ze 100% pewnością, że podatność istnieje, raczej chodzi o szybkie wytypowanie miejsc, w których jest jakieś podejrzane zachowanie.
Im więcej wiesz o testowanej aplikacji, tym efektywniej możesz ją testować. Jeśli np. znajdziesz jeden injection i wiesz jakiego zachowania aplikacji oczekiwać, możesz lepiej dostosować zarówno payloady, jak i to, czego szukasz.