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.