Wprowadzenie do tematyki SQL Injection

Autorzy: Rafał Cypcer


C#, SQL, Visual Studio


Tworząc aplikacje wykorzystujące bazy danych programista musi przewidzieć scenariusze chroniące przed nieautoryzowanym dostępem do danych. Jedną z bardzo popularnych technik do zdobycia takiego dostępu jest SQL Injection. Poniższy artykuł przedstawi problem i zwiększy wyobraźnię początkującego programisty odnośnie bezpieczeństwa danych.

Wstęp

W poniższym artykule krok po kroku stworzymy bardzo prostą aplikację, która będzie formularzem: pozwoli zalogować się do programu. Dla przykładu "wstrzykniemy" kod SQL (SQL Injection, z ang. zastrzyk SQL), by zaprezentować ideę i prostotę ataku oraz zaimplementujemy mechanizmy obronne, które utrudnią ten atak w przyszłości. Nie będzie tutaj skomplikowanych i wyszukanych sposobów zabezpieczeń. W zamierzeniu ma to być nieskomplikowany akademicki przykład, który pozwoli na "zabawę w hakera we własnym podwórku". Do tego celu wykorzystamy Visual Studio 2010, SQL Server 2008, a językiem programowania będzie C#.

 

Cel

Zasymulujemy logowanie do programu. Jeżeli użytkownik będzie administratorem to mają wyświetlić się e-maile wszystkich użytkowników z bazy danych, w przeciwnym wypadku te dane nie mogą się wyświetlić.

 

Zaczynamy

Uruchamiamy Visual Studio. Następnie klikamy w menu File → New → Project. W nowootwartym oknie wybieramy z listy szablon dla Windows Forms Application (Visual C#). Na dolnym panelu ustalamy nazwę projektu Name: np. jako SQL_Injection. Wybieramy lokalizację Location, gdzie projekt ma być zapisany. Resztę zostawiamy domyślnie i zatwierdzamy poprzez OK.01.png

Kolejnym etapem będzie przygotowanie interfejsu do naszej aplikacji. Z Toolboksa wybierzmy następujące komponenty: jeden Button, trzy Textboksy i trzy Labele. Button nazwijmy Logowanie, jednemu z Textboksów ustawmy właściwość Multiline i powiększmy go. Labele nazwijmy Login, Hasło i e-maile. Obok Labelu Login umieśćmy textBox1, obok Labelu Hasło umieśćmy textBox2, z kolei przy Labelu e-mail textBox3. Powinniśmy uzyskać efekt podobny jak na poniższym obrazku.

02.png

Skoro interfejs mamy gotowy czas przygotować bazę danych. W tym celu musimy najpierw się do niej podłączyć. Wybieramy menu Tools → Connect to Database.

03.png

W oknie Add Connection z listy Server Name wybieramy nazwę istniejącego serwera, który utworzyliśmy w trakcie instalacji SQL Server.

 

Uwaga: W trakcie wyboru serwera z listy okno może się tymczasowo zwiesić i jest to normalne zjawisko. Po krótkim czasie powinno wrócić wszystko do normy.

Pozostałe ustawienia zostawiamy jak są domyślnie (zdjęcie poniżej) i zatwierdzamy poprzez przycisk OK.

04.png

Następnym krokiem będzie utworzenie przykładowej bazy danych do naszych testów. W tym celu odnajdujemy zakładkę Server Explorer i klikamy na nią. Z Server Explorera rozwijamy listę Data Connections i ponownie rozwijamy połączenie, które przed chwilą utworzyliśmy. Jak widzimy na poniższym obrazku w moim przypadku są już utworzone trzy tabele, jednak utworzymy zupełnie nową na której będziemy pracować.

05.png

Klikamy prawym przyciskiem myszy (PPM) na Tables i wybieramy pozycję Add New Table. Tworzymy nowe rekordy. Pierwszy z nich o nazwie id typu int, któremu nadajemy klucz główny (Primary Key) oraz właściwość Identity (patrz rysunek niżej). Kolejne to login, pass i mail typu varchar(50). Zapisujemy tabelę nazwą Tabela.

06.png

Struktura tabeli gotowa. Jednak my potrzebujemy ją zapełnić kilkoma wartościami. Odnajdujemy przed chwilą utworzoną tabelę w widoku Server Explorer. Klikamy PPM na tabelę Tabela i wybieramy Show Table Data. Wypełniamy tabelę danymi jak na poniższym obrazku.

07.png

Czas na kod

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data.SqlClient;

namespace SQL_Injection
{
    public partial class Form1 : Form
    {
        SqlConnection sqlconnection = new SqlConnection();
        string strConnection = @"Data Source=RAFAL-LAPTOP\SQLEXPRESS;Integrated Security=True";

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            textBox3.Text = "";
            sqlconnection = new SqlConnection(strConnection);
            sqlconnection.Open();
            string str1 = "SELECT id FROM Tabela WHERE login='" + textBox1.Text + "' AND pass='" + textBox2.Text + "';";
            SqlCommand cmd1 = new SqlCommand(str1, sqlconnection);

            SqlDataReader rd = cmd1.ExecuteReader();

            int id;
            
            try // przechwycenie wyjątku, gdy użytkownik poda niepoprawny login lub hasło
            {
                rd.Read();
                str1 = rd["id"].ToString();
                id = Int32.Parse(str1);
            }
            catch (Exception)
            {
                id = -1; 
            }

            AdminAccess(id);
            // Metoda AdminAccess() zwróci e-maile wszystkich osób w bazie
            // tylko w przypadku, gdy id należy do admina i poda hasło qwerty

            sqlconnection.Close();
            textBox1.Text = "";
            textBox2.Text = "";
        }

        void AdminAccess(int id)
        {
            if (id == 1)
            {
                string str1 = "SELECT mail FROM Tabela;";
                SqlCommand cmd2 = new SqlCommand(str1, sqlconnection);

                cmd2.Connection.Close();
                cmd2.Connection.Open();

                SqlDataReader rd2 = cmd2.ExecuteReader();

                while (rd2.Read())
                {
                    textBox3.Text += rd2["mail"].ToString() + "\r\n";
                }
            }
        }
    }
}

Gdy wykonamy powyższy kod to pozornie wydaje się wszystko w porządku. Gdy admin poda swój login i hasło to w textBox3 pojawią się e-maile wszystkich użytkowników w bazie. Gdy zostanie podane złe hasło, bądź login innego użytkownika niż admin hasło się nie pojawi. Nikt niepowołany (użytkownicy, którzy nie są adminem) nie uzyska dostępu do ważnych danych.

 

SQL injection

Załóżmy, że nie wiemy, że admin ma hasło qwerty. Ale wpiszmy poniższy login:

admin'; --

I nie wpisujmy hasła tylko kliknijmy od razu przycisk Logowanie.

 

Stało się coś bardzo dziwnego. W jakiś niewyjaśniony sposób, bez znajomości hasła admina zostały wypisane tajne dane (e-maile). Jak do tego doszło? Wyjaśnijmy to.

 

Nasze zapytanie zwracające id admina wygląda następująco:

"SELECT id FROM Tabela WHERE login='" + textBox1.Text + "' AND pass='" + textBox2.Text + "';"

Gdy wpisaliśmy login

admin'; --

to zapytanie, które wysłaliśmy do bazy wyglądało następująco

SELECT id FROM Tabela WHERE login='admin'; -- AND pass='';

Jak widać zapytanie zwraca id użytkownika o loginu admin. Podwójny myślnik w SQL (--) oznacza komentarz, więc wszystkie znaki następujące po nim nie są brane pod uwagę. Jak widać SQL injection jest bardzo niebezpieczną techniką, więc programista musi wykazać się zwiększoną czujnością i wyobraźnią.

 

Posiadając obecną wiedzę, spróbujmy inne warianty SQL injection. Tym razem nie wpisujmy loginu, ale wpiszmy w hasło następujący ciąg znaków:

' OR '1' = '1

Zapytanie, które trafiło do bazy w tym przypadku będzie wyglądało następująco:

SELECT id FROM Tabela WHERE login='' AND pass='' OR '1' = '1';

Jak widać drugi sposób jest jeszcze lepszy, ponieważ nie musimy znać nawet loginu administratora.  Jeśli chodzi o samo zapytanie to warunek login='' jest spełniony, bo nie wpisaliśmy nic, później mamy koniunkcję z alternatywą pass='', ale '1' = '1' jest zawsze spełnione więc ogólna koniunkcja jest spełniona i dzięki temu id może zostać wypisane. Zwróćmy uwagę na to, że to zapytanie zwróci nam id wszystkich użytkowników. Dlaczego zatem dostaliśmy dostęp do admina? Odpowiedź znajdziemy w poniższym fragmencie kodu:

str1 = rd["id"].ToString();

Dzięki temu, że do zmiennej str1 z SqlDataReadera zczytywany był tylko tylko pierwszy wiersz id, którym w naszym przypadku był identyfikatorem admina. Istnieje wiele scenariuszy, które może zastosować atakujący. Jednym z bardziej nieprzyjemnych mogłoby być usunięcie bazy danych. By to uzyskać to w login wystarczyłoby wpisać:

 

'; DROP DATABASE Tabela; --

i zalogować się. W efekcie zapytanie wyglądałoby tak:

 

SELECT id FROM Tabela WHERE login=''; DROP DATABASE Tabela; -- AND pass= '

Przykładów można mnożyć. Jednak poprzestaniemy na tych. Teraz zastanowimy się jak utrudnić w przyszłości taki atak SQL.

 

Ochrona bazy z poziomu aplikacji

W naszym prostym przykładzie moglibyśmy napisać metodę, która sprawdzałaby zawartości ciągów znaków w textBox1.Text i textBox2.Text. Sprawdzanie takie mogłoby polegać na sprawdzaniu czy ciągi znaków nie zawierają podwójnych średników, pojedynczych myślników, słów kluczowych SQL i wiele innych dostępnych scenariuszy, które by trzeba było rozważyć przed implementacją. Jeśli taka metoda stwierdziłaby zagrożenie to blokowałaby możliwość ryzykownego zapytania. Jest to na pewno sposób, który utrudni atak SQL. Jednak musimy pamiętać o tym, że istnieje pewne prawdopodobieństwo, że jakiegoś scenariusza po prostu nie przewidzimy. Warto wtedy poszukać innych możliwości zabezpieczenia bazy. Zaprezentuję jak za pomocą procedur składowanej i parametrów zrobić to bez pisania własnych metod.

 

Procedury składowane i parametry

W widoku projektu do naszego interfejsu dodajmy Button i nadajmy mu nazwę "Dodaj procedurę".

08.png

Następnie kliknijmy na niego dwukrotnie, by napisać do zdarzenia Click poniższy kod.

 

        private void button2_Click(object sender, EventArgs e)
        {
            string createProcedure = @"CREATE Procedure Logowanie (
                            @login nchar(20),
                            @passw nchar(20),
                            @result int OUTPUT)
                            AS
                            SELECT @result = id FROM Table1 WHERE login=@login AND password=@passw";
            sqlconnection = new SqlConnection(strConnection);
            sqlconnection.Open();
            SqlCommand cmd = new SqlCommand(createProcedure, sqlconnection);
            cmd.ExecuteNonQuery();
            MessageBox.Show("Procedura dodana.");
            sqlconnection.Close();
        }

Po uruchomieniu programu i kliknięciu na przycisk Dodaj procedurę powinniśmy otrzymać komunikat o dodaniu procedury.

 

Stwórzmy teraz jeszcze jeden przycisk, który umożliwi logowanie z wykorzystaniem utworzonej procedury i parametrami. Nazwijmy przycisk Logowanie Ulepszone.

 

09.png

Kliknijmy na niego dwukrotnie, by napisać kod zdarzenia.

 

private void button3_Click(object sender, EventArgs e)
        {
            textBox3.Text = "";
            SqlTransaction transaction = null;
            // tworzymy transakcję SQL, by w razie wystąpnienia wyjątku 
            // wycofać wykonanie

            try
            {
                sqlconnection.Open();
                transaction = sqlconnection.BeginTransaction();
                string StoredProcedure = "Logowanie";
                SqlCommand cmd1 = new SqlCommand(StoredProcedure, sqlconnection);
                cmd1.CommandType = CommandType.StoredProcedure;
                // CommandType informuje, że wykonywana będzie procedura składowana

                cmd1.Parameters.Add(new SqlParameter("@login", SqlDbType.NChar, 20));
                cmd1.Parameters["@login"].Value = textBox1.Text;
                // pierwsza metoda dodania parametru
                // musimy określić typ danych

                cmd1.Parameters.AddWithValue("@passw", textBox2.Text);
                // druga metoda dodania parametru, jak widzimy jest krótsza
                // i sama ustala typ danych

                SqlParameter output = new SqlParameter("@result", -1);
                // rezultat OUTPUT z naszego zapytania, czyli zwraca id użytkownika

                output.Direction = ParameterDirection.Output;
                // ustalamy, że rezultat jest wyjściowy

                cmd1.Parameters.Add(output);
                // trzeci sposób dodania parametru

                cmd1.Transaction = transaction;

                cmd1.ExecuteNonQuery();
                transaction.Commit();

                int id;

                try
                {
                    id = Int32.Parse(cmd1.Parameters["@result"].Value.ToString());
                }
                catch (Exception ex)
                {
                    // wyjątek, gdy z jakiegoś powodu nie będzie się dało sparsować id
                    MessageBox.Show(ex.Message);
                    id = -1;
                }

                finally
                {
                    textBox1.Text = "";
                    textBox2.Text = "";
                }
                AdminAccess(id);
            }
            catch (SqlException ex)
            {
                MessageBox.Show(ex.Message);
                transaction.Rollback(); // wycofanie transakcji, gdy się nie powiedzie
            }

            finally
            {
                sqlconnection.Close();
            }
        }

Sprawdźmy jak procedury składowane z parametrami są odporne na SQL Injection. Wpiszmy w login:

 

admin'; --

po czym kliknijmy na przycisk Logowanie Ulepszone.

 

Pojawił się nam komunikat wyjątku informujący o niepoprawnym formacie łańcucha wejściowego. Wpiszmy samo hasło:

 

' OR '1' = '1

Znów nie przeszło. Z bazy ani jedna informacja o danych nie wyciekła. Dla sprawdzenia wpiszmy login admin i hasło qwerty, a potem login krystyna i hasło gazownia. Widzimy, że użycie parametrów jest skuteczne w kwestii obrony ataku przed SQL Injection.

 

Oprócz kwestii bezpieczeństwa pozytywnym skutkiem ubocznym stosowania procedur składowanych jest możliwy wzrost wydajności spowodowany zmniejszeniem liczby interakcji z bazą danych.

 

Podsumowanie

Po przeczytaniu tego artykułu zwiększy się świadomość zagrożeń jakie występują przy tworzeniu aplikacji wykorzystujących bazy danych. Poznaliśmy sposoby jakimi możemy utrudnić atak SQL Injection. Zdobyta wiedza pozwoli na dalsze zagłębianie tematu.

 

Autor nie odpowiada za skuteczność bezpieczeństwa w aplikacjach tworzonych przez Czytelników. Niniejszy artykuł ma głównie cel edukacyjny.

Liczba ocen: 17 | Średnia ocen: 4.47

Czytaj także

 

Skomentuj

Twój komentarz
Dodaj

Komentarze

Wyrażenia regularne 10-09-2013
Można jeszcze dodać filtr wyrażeń regularnych dla nazw użytkowników
informatyka zaocznie 17-06-2012
Weź zredaguj ten tutek, bo nie działa. Niekonsekwencja w nazwach kolumn i zmiennych. Jak na drugi rok, to średnio ogarnięty student.
zdzis 17-06-2012
nie działa...

Nasz Fanpage

Popularne treści

  • .NET  
  • 3D  
  • 8  
  • ActiveDirectory  
  • AJAX  
  • amazon web services  
  • Android  
  • Android Market  
  • AngularJS  
  • Animacja  
  • API  
  • aplikacje  
  • Aplikacje wielojęzyczne  
  • asembler  
  • ASHX  
  • ASP.NET  
  • ASP.NET MVC  
  • assembler  
  • Automated Installation Kit  
  • Azure  
  • bezpieczeństwo  
  • bing  
  • Blender  
  • C#  
  • certyfikat  
  • chmura  
  • cloud computing  
  • cmd  
  • Cmdlet  
  • Cmdlet’ów  
  • core  
  • CSS  
  • Cycles  
  • developer  
  • Entity Framework  
  • Expression Blend  
  • fitl  
  • google  
  • google app engine  
  • googlemaps  
  • GPU  
  • Grafika  
  • GroupPolicy  
  • hamachi  
  • hyperv  
  • hyper-v  
  • IaaS  
  • ImageX  
  • instalacja aplikacji  
  • interface  
  • interfejs  
  • Iron Speed  
  • java  
  • JavaScript  
  • jQuery  
  • Kinect  
  • Knockout  
  • kolokacja  
  • konsola  
  • LINQ  
  • LINQ to SQL  
  • Linux  
  • MakeCert  
  • maps  
  • microsoft  
  • mobile  
  • moduły  
  • MVC  
  • mySQL  
  • OpenSource  
  • openstreet  
  • openvpn  
  • PaaS  
  • partycja  
  • PHP  
  • pliki apk  
  • pon  
  • powershell  
  • preview  
  • programowanie  
  • przeglądarka  
  • przetwarzanie w chmurze  
  • przewodnik  
  • Qt  
  • RAD  
  • Rendering  
  • SaaS  
  • script  
  • SDK  
  • server  
  • serwer  
  • Skalowanie  
  • SQL  
  • Systemy operacyjne  
  • światłowody  
  • Światłowód  
  • Template  
  • ubuntu  
  • virtual  
  • Visual Studio  
  • vpn  
  • WAIK  
  • WCF  
  • WebAdministration  
  • WebApi  
  • Windows  
  • windows azure  
  • Windows PE  
  • Windows Phone  
  • WinFroms  
  • wirtualizacja  
  • WPF  
  • XAML  
  • zdalny  
  • zdjęcia  
Komu polecasz tą stronę? (email)
Poleca (twoje imie/pseudonim)
Treść (opcjonalnie) Do Twojej treści zostanie dodany link polecanej strony
POLECAM