Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book

Error-based SQL Injection w aplikacjach ASP(.NET) + MS SQL

14 października 2014, 21:34 | Teksty | komentarzy 5

Błędy umożliwiające ataki SQL Injection wciąż należą do najczęściej spotykanych w aplikacjach internetowych, a skutki, jakie niesie ze sobą ich udane wykorzystanie mogą być bardzo niebezpieczne – od wykradnięcia bądź zmodyfikowania danych w bazie do całkowitego przejęcia kontroli nad serwerem czy nawet całą siecią, w której znajduje się skompromitowana aplikacja internetowa.

Dla osób, które wcześniej nie spotkały się z tą klasą ataków, polecam zapoznanie się z wprowadzeniem dostępnym na stronach organizacji OWASP – SQL Injection lub wcześniejszymi tekstami na Sekuraku. Przykładowe metody ataków w zależności od wykorzystanego systemu bazy danych znajdują się pod adresem pentestmonkey – SQL Injection.

W tym artykule przyjrzymy się, w jaki sposób wykorzystać podatność SQL Injection w aplikacji zbudowanej w oparciu o technologie Microsoft: ASP.NET (lub ASP) oraz MSSQL. Technika, którą wykorzystamy, znana jest jako „error based injection” – wszystkie interesujące nas informacje uzyskamy dzięki komunikatom błędów, jakie będzie zwracał nam serwer w odpowiedzi na nasze modyfikowane zapytania.

Jak znaleźć SQLi w aplikacji ASP.NET

Podobnie jak w przypadku aplikacji zbudowanych w oparciu o inne technologie (PHP/MySQL, PHP/PostgreSQL itp.), najprostszą metodą jest sprawdzenie, jak zachowają się parametry przesyłane np. w żądaniu GET, gdy dodamy do nich znaki specjalne, na przykład apostrof czy cudzysłów (najlepiej nadają się do tego wszelkiego rodzaju identyfikatory, np. ID artykułu w sklepie internetowym):

Strona ASP.NET podatna na atak SQL Injection - test z apostrofem w parametrze o nazwie ID

Strona ASP.NET podatna na atak SQL Injection – test z apostrofem w parametrze o nazwie ID

Jeśli w rezultacie w przeglądarce ujrzymy stronę np. bez treści, która widniała na niej, gdy parametr nie był zmodyfikowany lub wręcz ukaże nam się komunikat błędu zwrócony przez serwer WWW – oznacza to, że aplikacja jest podatna na atak SQL Injection i możemy przejść do dalszych kroków.

Pierwszym będzie sprawdzenie, czy serwer zwraca wystarczająco opisowe komunikaty. Dodając za podatnym na atak parametrem GET ciąg having 1=1-- utworzymy adres url w postaci:

http://victim.com/index.aspx?id=1 having 1=1--

Przykładowy komunikat błędu może wyglądać tak, jak na ekranie poniżej. Jako bonus otrzymaliśmy nazwę jednej z tabel (Products) oraz kolumny z tej tabeli (ID) – w dalszej części artykułu przedstawiony zostanie dokładny proces uzyskiwania nazw tabel i kolumn oraz pobierania z nich danych.

Komunikat błędu zwracający interesujące nas informacje

Komunikat błędu zwracający interesujące nas informacje

Uzyskanie informacji o serwerze i bazie danych

Przyjrzyjmy się, jak możemy uzyskać podstawowe informacje o bazie danych, takie jak jej nazwa, nazwa użytkownika czy nazwy tabel i kolumn. W tym celu musimy zastosować pewien trik, który interesujące nas informacje wstawi do komunikatu błędu jako te, które ten błąd powodują. Polega on na wywołaniu funkcji SQL  convert(), która przyjmuje dwa argumenty – pierwszy z nich to typ danych, na jaki ma zostać skonwertowany drugi argument:

// próba konwersji ciągu znaków (wersja serwera SQL) do typu całkowitoliczbowego

convert(int, @@version)

Oczywiście taka konwersja nie ma sensu i jest zgłaszana jako błąd, w treści którego ciąg @@version  pojawia nam się w swojej przetworzonej przez serwer postaci:

Błąd konwersji wykorzystany do odczytania wartości zmiennej @@version

Błąd konwersji wykorzystany do odczytania wartości zmiennej @@version

W analogiczny sposób możemy uzyskać informację o nazwie użytkownika bazy danych (w miejsce @@version podstawiamy wywołanie funkcji  user_name()) czy nazwie bazy (db_name()).

Uzyskanie informacji o nazwach tabel i kolumn

W tym momencie możemy przystąpić do uzyskania pełnej informacji o interesującej nas bazie danych: nazwach tabel oraz kolumn. Posłużymy się zaprezentowaną w poprzednim punkcie sztuczką z funkcją convert(), zmodyfikujemy jedynie ciąg do skonwertowania. Będzie nim zapytanie do specjalnej bazy danych, dostępnej nie tylko w serwerze MSSQL, ale także np. w PostgreSQL 8.x, 9.x czy MySQL 5.x – information_schema. Zawiera ona nazwy wszystkich baz danych, tabel i kolumn tworzących bazy danych na konkretnym serwerze bazodanowym.

Przykładowe zapytanie o pierwszą tabelę w interesującej nas bazie danych wygląda następująco:

//nazwa_bazy_danych - odczytana wcześniej nazwa interesującej nas bazy
select top 1 table_name from information_schema.tables where db_name() = nazwa_bazy_danych

Po podstawieniu takiego zapytania do przykładu z poprzedniego punktu uzyskujemy oczekiwany rezultat:

http://victim.com/index.aspx?id=1 and and 1=convert(int,(select top 1 table_name from information_schema.tables where db_name() = 'nazwa_bazy_danych'))

Uzyskana nazwa pierwszej tabeli

Uzyskana nazwa pierwszej tabeli

Składnia:

SELECT TOP 1

powoduje, że zapytanie zwraca tyko jeden wiersz (po zmianie tej wartości na 2 lub więcej otrzymamy komunikat błędu o tym, że podzapytanie zwróciło więcej niż jeden wiersz). W jaki sposób zatem wyciągnąć kolejne nazwy tabel?

Pomoże nam w tym operator NOT IN (lista, wartosci, oddzielonych, przecinkami) używany w składni SQL. Powoduje on zwrócenie wyników, które nie zawierają podanych w nawiasach wartości. Aby uzyskać nazwę drugiej tabeli, musimy zatem nieco zmodyfikować nasze podzapytanie dla funkcji convert():

http://victim.com/index.aspx?id=1 and 1=convert(int,(select top 1 table_name from information_schema.tables where db_name() = 'nazwa_bazy_danych' and table_name not in('settings') ))--

Jeśli nazwą drugiej tabeli będzie przykładowo users, trzecią tabelę uzyskamy poprzez dodanie users do listy wartości operatora NOT IN:

http://victim.com/index.aspx?id=1 and 1=convert(int,(select top 1 table_name from information_schema.tables where db_name() = 'nazwa_bazy_danych' and table_name not in('settings', 'users') ))--

Czynność powtarzamy aż do uzyskania listy wszystkich interesujących nas tabel. W analogiczny sposób uzyskujemy nazwy kolumn w tabelach:

http://victim.com/index.aspx?id=1 and 1=convert(int,(select top 1 column_name from information_schema.columns where table_name='settings' and column_name not in ('ID'))--

Odczytanie danych zapisanych w bazie

Do pełni sukcesu brakuje nam już tylko danych zapisanych w bazie. Stosując analogiczną składnię, jak w poprzednich krokach, do wyciągnięcia wartości ID z pierwszego rekordu tabeli settings  możemy posłużyć się zapytaniem:

http://victim.com/index.aspx?id=1 and 1=convert(int,(select top 1 ID from settings))--

Powyższe zapytanie powinno zwrócić nam wartość kolumny ID dla pierwszego rekordu z tabeli settings.

Podsumowanie

Technika bazująca na komunikatach błędów może być bardzo efektywnym i dość szybkim sposobem na uzyskanie danych z bazy. Co ciekawe, podobny mechanizm można wykorzystywać w innych silnikach bazodanowych – dla porównania odsyłam do artykułu o XPATH a SQL injection, który pokazuje analogiczny atak dla silnika MySQL.

Polecam samemu poeksperymentować z różnymi specyficznymi dla MS SQL funkcjami i zmiennymi. Warto wspomnieć także o procedurze xp_cmdshell umożliwiającej wykonanie dowolnego polecenia w systemie operacyjnym.

Źródła i zasoby dodatkowe

Rafał 'bl4de’ Janicki– bloorq[at]gmail.com

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Mała uwaga – Pokazane w przykładach (screenshoty) rozszerzenie stron asp raczej sugeruje aplikację ASP a nie ASP.NET.

    Choć oczywiście zdarzają się programiści którzy w ASP.NET sklejają SQLa to jednak taki styl bardziej promował właśnie ASP, który chyba nawet nie bardzo miał cokolwiek innego do użycia (choć nie miałem przyjemności używać).

    Paweł

    Odpowiedz
    • Z doświadczenia mogę powiedzieć, że z całą pewnością trafiają się aplikacje .NET, w których są sklejane zapytania. ;)

      Odpowiedz
      • To chyba bardziej zależy od programisty niż od użytej technologii :)

        Odpowiedz
        • Hattori Hanzo

          Czasem jednak @parametry SQLowe nie wystarczą i trzeba kleić stringi… Na przykład w zastosowaniach, gdzie nazwę obiektu (schemat+tabela) określa się dynamicznie w trakcie wykonania. Pierwszy projekt jaki przychodzi mi na myśl to staging ETL. Na szczęście to jest rzadko wystawiane do internetu, niemniej na taką klasę problemów nie ma ORMa i większość własnych rozwiązań ETLowych ma SQLi :)

          Odpowiedz
  2. bl4de

    Problem „sklejanych” SQL-i dotyczy w zasadzie każdego stacka, mimo dostępności np. ORM-owych bibliotek czy zaleceń, czy używać parametryzowanych zapytań :)

    Stąd wciąż bardzo wysoka popularność błędów SQLi w aplikacjach webowych.

    Odpowiedz

Odpowiedz