Bezpieczeństwo web, SQL Injection – Wstrzykiwanie zapytań, które nie wyświetlają danych w UI

W bardziej skomplikowanych systemach, wszelkie zapytania SQL nie są wyświetlane bezpośrednio na stronie. W prostych stronach internetowych zwykle, rezultat zapytania SQL może być wyświetlony w formie np. tabeli. W aplikacjach webowych jest wiele warstw usług między stroną internetową a DAL. Ponadto, część zapytań po prostu nie jest wyświetlanych np.:

SELECT count(1) where UserName='Piotr' AND Password='1234'

Powyższe zapytanie, może być wykonane jako proces autoryzacji. Nawet jeśli uda nam się coś wstrzyknąć, nie będziemy o tym wiedzieć,  ani nie zobaczymy danych, które chcieliśmy uzyskać. Czy to znaczy, że w tych sytuacjach aplikacja jest zawsze odporna na SQL Injection?

Jest dokładnie odwrotnie. Proste aplikacje zwykle używają ORM (np. Entity Framework) i nie generują dynamicznych zapytań. W przypadku skomplikowanych systemów, łatwo ulec złudzeniu, że logika wykonywana głęboko w back-end (np. w systemie kolejkowym) jest bezpieczna. Inne zagrożenie to dane pochodzące z wewnętrznego systemu, które są traktowane przez programistów jako bezpieczne. Każdy argument, nawet pochodzący z wewnętrznego systemu powinien być postrzegany jako możliwość SQL Injection.

Dzisiaj przyjrzymy się technikom, które umożliwiają przeprowadzenie SQL Injection, nawet jeśli zapytanie jest ukryte głęboko w BE. Zakładamy, że system jest podatny na SQL Injection i musimy w jakiś sposób tylko wyprowadzić dane z niego ponieważ nie ma możliwości wyświetlenia ich w standardowej tabeli.

1. Zewnętrzny zapis danych
Niektóre silniki baz danych umożliwiają zapis danych do zewnętrznej bazy. W SQL Server do dyspozycji jest OpenRowSet.
W MySQL można zapisać wynik zapytania w pliku tekstowym za pomocą

SELECT ... INTO OUTFILE

Więcej informacji tutaj:
https://dev.mysql.com/doc/refman/5.1/en/select-into.html

Pomimo, że bezpośrednio danych nie widać na wynikowej stronie, to możemy wstrzyknąć zapytanie, którego rezultat zostanie zapisany w naszej bazie danych lub w pliku tekstowym w przypadku MySQL. W praktyce, ma to dość ograniczone możliwości ze względu na firewall.

2. Stopniowe odgadywanie danych

Innym sposobem jest tak zmodyfikowanie zapytania, aby w każdej iteracji można byłoby odgadnąć jeden znak. Przykład:

SELECT count(1) where UserName='admin' AND substring(Password,0,1) == 'A'

Wystarczy, że odgadniemy pierwszy znak hasła. Jeśli hasło nie zaczyna się od ‘A’, wtedy dostaniemy komunikat o błędzie autoryzacji. Wtedy możemy spróbować ponownie, np.:

SELECT count(1) where UserName='admin' AND substring(Password,0,1) == 'B'

Załóżmy, że udało nam się zalogować jako admin. Oznacza to, że hasło admina zaczyna się od ‘B’. Kolejna iteracja będzie wyglądać zatem:

SELECT count(1) where UserName='admin' AND substring(Password,1,1) == 'A'

O wiele łatwiej jest odgadnąć jeden znak, niż całe hasło.

3. Spowodowanie błędu
Zwykle jak wiemy, że aplikacja jest podatna na atak, wtedy można zastanowić się jak to wykorzystać. Problem w tym, że skoro aplikacja nie wyświetla żadnych danych, czasami decydujące jest odkrycie, czy przeprowadzony atak w ogóle może przynieść jakieś zagrożenia. Jeśli chcemy przekonać się, że nasze zapytanie rzeczywiście zostało wykonane, wtedy możemy spowodować błąd. Na przykład rozważmy schemat zapytania:

SELECT A from B where C

A zostanie wyłącznie wtedy, kiedy warunek C jest spełniony na tabeli B. Z kolei warunek C zostanie sprawdzony wyłącznie, kiedy zapytanie jest wykonywane.
Zwykle powyższą technikę stosuje się z punktem numer 2. W przykładzie pokazanym w punkcie drugim nie jest to potrzebne bo w przypadku nieprawidłowego hasła po prostu nie nastąpi autoryzacja. Czasami jednak nie możemy liczyć nawet na taką informacje i wtedy można wywołać błąd. A w jaki sposób najprościej wyrzucić wyjątek? Spójrzmy:

SELECT 1/0 from Users WHERE ...

4. Opóźnienie wykonania

Punkt 2 i 3 mają sens wyłącznie kiedy w jakiś sposób błąd autoryzacji (punkt 2) lub błąd wykonania zapytania (punkt 3) jest przekazywany z powrotem do użytkownika. Nie zawsze jednak ma to miejsce, ale wciąż aplikacja może być podatna na SQL Injection. Co jeśli następujące zapytanie zostanie wstrzyknięte?

if substring(Password,0,1)) = 'A'  waitfor delay '0:0:10'

Możemy obserwować czas przetwarzania zapytania. Jeśli będzie one dłuższe o 10 sekund, prawdopodobne jest, że nasz warunek został spełniony czyli pierwszy znak hasła to litera A.

Bezpieczeństwo web (część 6) – SQL Injection III, obchodzenie filtrów

W poprzednich postach opisałem ogólną zasadę działania SQL Injection. Niestety część programistów, próbując zapobiec atakowi, pisze własne filtry mające na celu usunięcie spreparowanego zapytania. Niestety w większości przypadków jest to bardzo zły i niebezpieczny pomysł. Dzisiaj przedstawię kilka sposobów na ominięcie typowych zabezpieczeń i filtrów stosowanych przez część programistów. Prawidłowe wykonanie zapytania polega na parametryzacji wszystkich dynamicznych części zapytania. Dzisiejszy wpis ma na celu pokazanie, jak bardzo skomplikowane jest zadanie napisania prawidłowych filtrów, działających we wszystkich sytuacjach.

1. Blokowanie komentarzy

Wcześniej wstrzykiwaliśmy kod za pomocą wykomentowania reszty zapytania tzn.

' or 1=1 --

W rezultacie tworzyło to:

SELECT * from Articles where Text='test' or 1=1 --'

Istnieje inny sposób, jeśli filtry usuwają wszelkie komentarze np.:

' or 'a'='a

W rezultacie da to:

SELECT * from Articles where Text='test' or 'a'='a'

2. Blokowanie spacji
Niektóre, bardzo proste filtry usuwają spacje np. jeśli przekażemy ‘ or 1=1–, otrzymamy ‘or1=1– co oczywiście jest błędną składnią. Podobnie SELECT * FROM table zostanie zamieniony na SELECT*FROMTABLE. Sztuczka wtedy polega na wstrzyknięciu komentarzy zamiast spacji, czyli:

SELECT/*Cokolwiek/*COL1/*COKOLWIEK*/FROM/*COKOLWIEK*/table

3. Blokowanie słów kluczowych
Niektóre systemy IDS (Intrusion detection system) blokują konkretne słowa kluczowe jak np. SELECT. Wtedy można użyć kodowania URI i zamiast SELECT spróbować można:
– %00SELECT
– %53%45%4c%45%43%54 (pojedyncze kodowanie)
– %2553%2545%254c%2545%2543%2554 (podwójne kodowanie)
4. Kodowanie apostrof
Naturalnie, filtry próbują pozbyć się apostrof, np. jeśli przekazujemy ‘ or 1=1– może zostać zamienione to na ” or 1=1–.
Na pierwszy rzut oka kod wydaje się bezpieczny. Apostrof zostanie uznany za zwykły tekst. Niestety, np. w MySQL istnieją dwa sposoby kodowania znaków. Pierwszy z nich widzimy wyżej – za pomocą dodania kolejnego apostrofa (‘->”). Drugi sposób to użycie slasha (‘ -> \’).
Jeśli zatem wstrzykniemy \’ or 1=1–, po filtrowaniu uzyskamy \” or 1 =1. Pierwszy apostrof zostanie uznany za tekst, a drugi zamknie frazę, umożliwiając przeprowadzenie ataku.
5. Wprowadzenie tekstu bez użycia apostrof
Inny sposób to wstrzyknięcie tekstu za pomocą ASCI. Na przykład zamiast:

SELECT * FROM Persons where name='piotr'

Możemy:

SELECT * FROM Persons where name=char(80)||char(73)||char(79)||char(84)||char(82)

6. Składnia
Co prawda jest to bardzo naiwne podejście, ale warto sprawdzić czy “SELECT” jest interpretowany tak samo jak np. “SeLeCT”. Większość skanerów, próbuje wstrzyknąć ten sam kod za pomocą małych i wielkich liter.
7. Zakończenie NULL byte
Część systemów IDS\WAF jest zaimplementowana w językach niezarządzanych takich jak CPP. String inaczej jest interpretowany w CPP niż w C#. W CPP \null kończy dany strumień znaków. W C#, istnieje osobne pole określające długość strumienia znaków (napisu).
W praktyce oznacza to, że “ABC\nullDEF” w CPP sprowadzi się do “ABC”, z kolei w C# będzie to oczywiście “ABC\nullDEF”.Innymi słowy, można tak przygotować zapytanie, że zostanie one przepuszczone przez IDS\WAF, a zarządzany kod (ASP.NET) wykona złośliwy kod.