V tomto díle se budeme hlavně zabývat Sql injection. Ukážeme si co a jak se touto metodou dá spáchat a hlavně jak se tomu bránit. K demnostraci využijeme častou chybu vyskytující se v autentikačních procedurách. Povíme si něco o parametrizovaných příkazech a uložených procedurách.
Jedná se o techniku, která staví na nesprávném ošetření vstupních dat. Ty jsou následně vloženy do SQL dotazu a ten se vykoná na serveru.
Data podle, kterých chceme uživatele vypsat, v tomto případě chceme všechny uživatele, kteří se jmenují Maurenc, většinou pocházejí od uživatele. Ten do těchto dat může přidat neočekávaný SQL kód(tzv. injekci) a tím manipulovat s naším SQL serverem, tak jak bychom to určitě nechtěli.
Podívejme se tedy na jeden příklad, který je zřejmě nejčastější chybou v aplikacích používajících SQL databázi. Jedná se o ověřování uživatelů neboli autentikaci. Klasicky na webových stránkách přes formulář, vyplní se Login a Heslo a klikne se na tlačítko Přihlásit.
Vytvořme si tedy testovací databázi, tabulku uživatelů a vložme do ní 2 uživatele.
Dále si na aspx stránku vložíme pár ovládacích prvků:
<form id="Form1" method="post" runat="server">
Login:
<asp:textbox id="Login" runat="server">
</asp:textbox><BR>
Heslo:
<asp:textbox id="Heslo" runat="server" TextMode="Password">
</asp:textbox><BR>
<asp:button id="Button" runat="server" Text="Login">
</asp:button><BR>
<asp:label id="Label" runat="server">
</asp:label>
</form>
Při stisku tlačítka Button se zavolá následující metoda:
private void OnLogIn( object sender, System.EventArgs e)
{
if (OveritUzivatele(Login.Text,Heslo.Text))
{
Label.Text = "Prihlaseno za: "+Login.Text;
}
else
{
Label.Text = "Neplatne prihlaseni";
}
}
Metoda OnLogIn využívá, pro autentikaci uživatelů tuto funkci:
bool OveritUzivatele(string login,string heslo)
{
SqlConnection prip = new SqlConnection
("server=localhost;database=mojeDatabaze;uid=sa;pwd=");
try
{
prip.Open();
SqlCommand prikaz = new SqlCommand
("SELECT count(*) FROM uzivatele WHERE login='"+login+"'"+
"AND heslo='"+heslo+"'",prip);
int pocet = (int)prikaz.ExecuteScalar();
return (pocet > 0);
}
catch(SqlException)
{
return false;
}
finally
{
prip.Close();
}
}
Tento kód možná na první pohled vypadá v pořádku, ale je tu jedna obrovská bezpečnostní díra. Konkrétně ve funkci OveritUzivatele.
V normálním případě uživatel zadá do TextBoxu svůj Login a Heslo, stiskne tlačítko, zavolá se metoda OnLogIn, ta zavolá funkci OveritUzivatele, která na databázi vykoná tento SQL příkaz:
SELECT count(*) FROM uzivatele WHERE login='Nývlt' AND heslo='abcd'
Ten následně vrátí 1. Což je signál, že autentikace uživatele proběhla úspěšně a vše je v tomto případě v pořádku.
Co se ale stane, když do Login uživatel zadá: Admin a do Heslo: cokoliv' OR 1=1;--. Nám se následně na serveru spustí tento příkaz:
SELECT count(*) FROM uzivatele WHERE login='Admin' AND heslo='cokoliv' OR 1=1;--'
A jelikož 1=1 je pravda vždy a -- je začátek SQL komentáře, bude celý příkaz korektní a návratová hodnota nenulová. Z toho plyne, že útočník je nyní přihlášen za uživatele Admin( To nám rovněž říka, že není dobré mít administrátora s loginem Admin, případně Administrator… to je totiž první věc, kterou útočník zkusí). V tomto případě ale útočník může nového uživatele i vytvořit, uhodne-li název a strukturu tabulky.
Login: cokoliv
Heslo: x'; INSERT INTO uzivatele VALUES('Hacker','heslo');--
SELECT count(*) FROM uzivatele WHERE login='cokoliv' AND heslo= 'x'; INSERT INTO uzivatele VALUES('Hacker','heslo');--'
Jako heslo je zde x, ale na tom vůbec nezáleží. Důležité je, že po vykonání tohoto příkazu je v systému nový uživatel. To samozřejmě lze pouze pokud není databáze read-only.
Když útočník bude mít spíše destruktivní náladu, je možné zadat takovýto vstup:
Login: cokoliv
Heslo: x'; DROP TABLE uzivatele;--
SELECT count(*) FROM uzivatele WHERE login='cokoliv' AND heslo='x'; DROP TABLE uzivatele;--'
To už opravdu nebude příjemné, zvláště pokud se neprovádějí zálohy. A takovýmto způsobem nemusí odstranit pouze tabulku uživatelů, ale jakoukoliv tabulku v této databázi. Rovněž se může stát, že vám chce třeba jen vyřadit server na nějakou chvíli z provozu, pak není nic hezčího než příkaz SHUTDOWN WITH NOWAIT.
Login: cokoliv
Heslo: x'; SHUTDOWN WITH NOWAIT;--
SELECT count(*) FROM uzivatele WHERE login='cokoliv' AND heslo='x'; SHUTDOWN WITH NOWAIT;--'
Po vykonání tohoto příkazu následuje okamžité vypnutí SQL serveru. Náš útočník může rovněž využít tzv. Extended Stored Procedury, které jsou defaultně na MSSQL serveru nainstalovány. Jedná se o exportované funkce v DLL knihovnách, které můžeme volat v SQL dotazech, tyto funkce začínají prefixem xp_. Dostane se mu tak do ruky velmi mocný nástroj, kterým může mazat soubory, prohlížet adresáře, vykonávat příkazy shellu… Jako příklad zde uvádím restartování serveru IIS.
Login: cokoliv
Heslo: x'; exec master..xp_cmdshell 'iisreset'; --
SELECT count(*) FROM uzivatele WHERE login='cokoliv' AND heslo='x'; exec master..xp_cmdshell 'iisreset'; --'
Když dostane útočník přístup k těmto procedurám je v podstatě už po serveru. To je zároveň doporučení, abyste zvážili smazání potenciálně nebezpečných a nepotřebných procedur ( samozřejmě jen pokud víte jistě, co mažete ), hlavně tedy xp_cmdshell a xp_grantlogin, ty jsou z nich nejnebezpečnější.
Těchto pár příkladů je pouze malá ukázka toho, co vše může nějaký útočník vaší aplikaci způsobit. Dále se podíváme, jak nás .net sám dokáže proti těmto útokům chránit.
Parametrizované příkazy
Je více způsobů, jak SqlInjection zabránit. V .Net je doporučováno používat parametrizované příkazy. Podívejme se tedy na přepsanou metodu OveritUzivatele, která je již proti SQLInjection imunní.
bool OveritUzivatele(string login,string heslo)
{
SqlConnection prip = new SqlConnection
("server=localhost;database=mojeDatabaze;uid=sa;pwd=");
try
{
prip.Open();
SqlCommand prikaz = new SqlCommand
("SELECT count(*) FROM uzivatele WHERE login=@login AND"+
"heslo=@heslo",prip);
prikaz.Parameters.Add("@login",SqlDbType.VarChar);
prikaz.Parameters.Add("@heslo",SqlDbType.VarChar);
prikaz.Parameters["@login"].Value = login;
prikaz.Parameters["@heslo"].Value = heslo;
int pocet = (int)prikaz.ExecuteScalar();
return (pocet > 0);
}
catch(SqlException)
{
return false;
}
finally
{
prip.Close();
}
}
Jak tento příklad demonstruje, pro přidání parametrů k příkazu SqlCommand se volá metoda Add na kolekci Parametres objektu SqlCommand. Ta do objektu SqlCommand přidá objekt SqlParameter.
Toto samozřejmě platí pouze pokud používáte poskytovatele .NET SQL Serveru. Používáte-li poskytovatele .NET OLE DB bude tato metoda vypadat trošku odlišně.
bool OveritUzivatele(string login,string heslo)
{
OleDbConnection prip = new OleDbConnection
("provider=sqloledb;server=localhost;database="+
"mojeDatabaze;uid=sa;pwd=");
try
{
prip.Open();
OleDbCommand prikaz = new OleDbCommand
("SELECT count(*) FROM uzivatele WHERE login=? AND"+
"heslo=?",prip);
prikaz.Parameters.Add("@login",OleDbType.VarChar);
prikaz.Parameters.Add("@heslo",OleDbType.VarChar);
prikaz.Parameters["@login"].Value = login;
prikaz.Parameters["@heslo"].Value = heslo;
int pocet = (int)prikaz.ExecuteScalar();
return (pocet > 0);
}
catch(OleDbException)
{
return false;
}
finally
{
prip.Close();
}
}
Hlavní rozdíl mezi oběma kódy je v tom, že poskytovatel .NET SQL serveru nepřijímá otazníky v řetězci SQL příkazu, ale přímo jména parametrů. To nás rovněž u poskytovatele .NET OLE DB nutí volat metodu Add kolekce Parametres ve správném pořadí, v takovém v jakém chceme aby se nahradili za otazníky. Na to je třeba dát si pozor, protože pokud mají parametry stejný typ a jsou v špatném pořadí, nedojde k žádné výjimce. U poskytovatele .NET SQL Serveru na pořadí volání Add nezáleží.
Ukázali jsme si tedy parametrizované příkazy jako mocnou zbraň v boji se SQLInjection,je to spíše jejich druhotná vlastnost. V první řadě se jedná v podstatě o podroprogramy pro databázové programování.
Dále si ukážeme ještě jeden způsob, jak metodu OveritUzivatele implementovat.
Uložené procedury - Stored Procedures
Jedná se o zkompilované parametrizované příkazy, tudíž se vykonávají rychleji než klasické dynamické příkazy SQL. Nárůst výkonnosti je srovnatelný s rozdílem výkonnosti mezi interpretovaným a kompilovaným kódem. A jelikož je u aplikací používajících databázi výkon kritická veličina používají se velmi často.
Pokud budete surfovat v SDK všimnete si metody Prepare u třídy SqlCommand. Ta vám parametrizovaný příkaz zkompiluje. Nemělo by se to však používat, protože tu od toho jsou Stored Procedures.
Vytvořme si tedy stored proceduru na MSSQL serveru, kterou bude následně volat metoda OveritUzivatele.
CREATE PROCEDURE proc_OveritUzivatele,
@login varchar(30 ),
@heslo varchar(30 ),
@pocet int OUTPUT
AS
SELECT @pocet = count(*) FROM uzivatele WHERE login=@login AND heslo=@heslo
GO
Toto je velmi jednoduchá stored procedura, pro náš účel bude však stačit. Všimněte si parametru @pocet. Jedná se o výstupní parametr, v kterém se po vykonání stored procedury bude nacházet počet řádek výstupu z příkazu SELECT.
Následuje nová verze metody OveritUzivatele využívající stored proceduru proc_OveritUzivatele.
bool OveritUzivatele(string login,string heslo)
{
SqlConnection prip = newSqlConnection
("server=localhost;database=mojeDatabaze;uid=sa;pwd=");
try
{
prip.Open();
SqlCommand prikaz = new SqlCommand("proc_OveritUzivatele",prip);
prikaz.CommandType = CommandType.StoredProcedure;
prikaz.Parameters.Add("@login",login);
prikaz.Parameters.Add("@heslo",heslo);
SqlParameter pocet = prikaz.Parameters.Add
("@pocet",SqlDbType.Int);
pocet.Direction = ParameterDirection.Output;
prikaz.ExecuteScalar();
return ((int)pocet.Value > 0);
}
catch(SqlException)
{
return false;
}
finally
{
prip.Close();
}
}
V tomto případě je nutné dát objektu SqlCommand vědět, že chceme používat StoredProceduru, to se provede nastavením vlastnosti CommandType na CommandType.StoredProcedure. Dále se u SqlParametru počet nastaví vlastnost Direction na ParameterDirection.Output, čímž se parameter počet nastaví jako výstupní.
Toto zdaleka nepokrývá potenciál Stored Procedure, to by však bylo přinejmenším na další článek.