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
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):
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.
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:
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'))
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
- MSSQL Injection Cheat Sheet | Pentest Monkey
- Full MSSQL Injection PWNage | Exploit-db
- Hack-Proofing Your ASP.NET Applications | MSDN
- Testing for SQL Server | OWASP
Rafał 'bl4de’ Janicki– bloorq[at]gmail.com
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ł
Z doświadczenia mogę powiedzieć, że z całą pewnością trafiają się aplikacje .NET, w których są sklejane zapytania. ;)
To chyba bardziej zależy od programisty niż od użytej technologii :)
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 :)
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.