Zpět na seznam článků     Číst komentáře (20)     Verze pro tisk

SQL Truncation Attack, aneb jak hacknout SOOM.cz

Autor: .cCuMiNn.   
19.5.2014

Znalost zranitelností spojených s databázemi se u mnoha lidí omezuje pouze na SQL Injection. To je ale docela škoda, protože jak si v tomto článku ukážeme, dají se ve spojení s SQL objevit i jiné neméně závažné problémy, jako je například SQL Truncation.


SQL Truncation.pngJak již název tohoto útoku napovídá, bude při něm využito zkracování. Konkrétně zkracování řetězců, které jsou delší než maximální délka konkrétního datového typu, případně delší než délka uvedená v definici sloupce. Předpokládám, že s návrhem databáze máte již určité zkušenosti a například typ varchar, na který se v následujícím textu zaměřím, vám není zcela neznámý. Tento typ se používá převážně tam, kde je známá maximální délka ukládaných řetězců. Často se tak s typem varchar setkáte například pro ukládání hodnot PSČ, telefonních čísel, IČ, DIČ, nebo třeba hashů hesel. Vlastností varcharu, ale nejen jeho (ve větším rozsahu se to týká i ostatních textových typů, viz. následující tabulka), je, že při ukládání řetězce delšího než definovaná velikost, ořeže SQL server ukládaný řetězec a uloží pouze jeho úvodní část, která se do sloupce konkrétního typu vejde. Zbývající část řetězce je jednoduše zahozena.


Přehled délek u jednotlivých textových typů v SQL
  1. CHAR()           Řetězec s fixní délkou 0 až 255 znaků.
  2. VARCHAR()        Řetězec s variabilní délkou 0 až 255 znaků.
  3. TINYTEXT         Text s maximální délkou 255 znaků.
  4. TEXT             Text s maximální délkou 65535 znaků.
  5. BLOB             Text s maximální délkou 65535 znaků.
  6. MEDIUMTEXT       Text s maximální délkou 16777215 znaků.
  7. MEDIUMBLOB       Text s maximální délkou 16777215 znaků.
  8. LONGTEXT         Text s maximální délkou 4294967295 znaků.
  9. LONGBLOB         Text s maximální délkou 4294967295 znaků.


Pískoviště

Dříve než vám popíši, jak bylo možné pomocí zranitelnosti SQL Truncation hacknout uživatelské účty na našem serveru SOOM.cz, pojďme si nejdříve uvedené údaje a jejich možný dopad na prolomení zabezpečení přiblížit na aplikaci, která v tabulce s uživatelskými účty obsahuje sloupec login typu varchar(20). Řekněme si také, že tato aplikace neumožní registraci dvou uživatelských účtů se shodným loginem, což je logické.

Jako pískoviště, kde si vše můžete v praxi vyzkoušet, nám jako už mnohokrát poslouží náš zranitelný webmail. Konkrétně obsahuje registrace do našeho webmailu kód podobný následujícímu (upozorňuji, že je v uvedeném kódu i mnoho dalších zranitelností, takže tento kód rozhodně nekopírujte do svých vlastních aplikací).


Kód registrace
  1. <?php
  2.   $login = $_POST["login"];
  3.   $password = $_POST["password"];
  4.   $query = "SELECT * FROM users WHERE login='$login'";
  5.   $res = mysql_query($query);
  6.   $n = mysql_num_rows($res);
  7.   if (!$n) {
  8.     $query = "INSERT INTO users(login, password) VALUES('$login', '$password')";
  9.   } else {
  10.     echo "Zadaný login je již použit";
  11.   }
  12. ?>


Nyní budeme předpokládat, že existuje uživatelský účet s loginem „admin“, ke kterému bychom se chtěli přihlásit pomocí SQL Truncation tak, že si založíme svůj vlastní účet se shodným loginem. Jak toho ale dosáhnout, když je z výše uvedeného kódu patrné, že probíhá kontrola, zda již účet se shodným loginem náhodou neexistuje? Je to jednoduché a využijeme k tomu právě vlastnost osekání dlouhých řetězců na straně SQL serveru.

Abychom zjistili, jak dlouhý login můžeme použít. Zaregistrujeme se s nějakým hodně dlouhým loginem například „totojemujhodneprehodnedlouhylogin“, který má 33 znaků a následně se k tomuto účtu zkusíme přihlásit. Nemělo by se nám to podařit, protože jak již víme, uložila se do tabulky users hodnota dlouhá pouze 20znaků, tedy „totojemujhodneprehod“. Zkoušíme tedy postupné přihlašování pomocí stejného hesla a stále se zkracujícího loginu. Ve chvíli, kdy se nám podaří přihlášení k našemu účtu, budeme vědět, na jakou délku byl řetězec osekán.

Nyní zjištěné skutečnosti využijeme k tomu, abychom tímto způsobem založili duplicití účet s loginem „admin“. Zaregistrujeme se tedy s loginem, který bude začínat požadovaným řetězcem „admin“ následovaným alespoň 15-ti mezerami a na konci s libovolnými znaky, které budou při uložení odseknuty. Podoba takového loginu by mohla být například tato: „admin abc“.

Registrace uvedeného loginu projde, protože kontrolou se zjistí, že neexistuje účet, kde by se login rovnal řetězci „admin abc“. Do databáze se ovšem uloží pouze oseknutá verze tohoto řetězce, tedy „admin“ a 15 mezer. Je patrné, že řetězece „admin“ a „admin “ s patnácti mezerami, jsou dvě rozdílné sekvence, které se vzájemně nerovnají, což je evidentně pravda. Neplatí to ale tak úplně v databázích, kde MySQL bude v případě následujícího dotazu zcela ignorovat koncové mezery a v řetězci a na tento dotaz vrátí oba dva uživatelské účty (bez mezer i s nimi).


  1. SELECT * FROM users WHERE login = 'admin'


Co se stane nyní, je již otázkou dalšího návrhu aplikace. Například náš zranitelný webmail autentizuje uživatele kódem podobným následujícímu, který nejprve zjistí, zda uživatel zadal správnou kombinaci jména a hesla, a následně podle loginu přiřadí patřičná oprávnění.


Kód funkce isAthotized()
  1. function isAuthenticated($login, $password) {
  2.   $query = "SELECT * FROM users WHERE login='$login' AND password='$password'";
  3.   $res = mysql_query($query);
  4.   $n = mysql_num_rows($res);
  5.   if ($n) {
  6.     return true;
  7.   } else {
  8.     return false;
  9.   }
  10. }

Kód Funkce setSession()
  1. function setSession($login) {
  2.   $query = "SELECT opravneni FROM users WHERE login='$login'";
  3.   $res = mysql_query($query);
  4.   $data = mysql_fetch_array($res);
  5.   $_SESSION["opravneni"] = $data["opravneni"];
  6. }

Kód přihlášení
  1. $login = $_POST["login"];
  2. $password = $_POST["password"];
  3. if (isAuthenticated($login, $password) {
  4.   setSession($login);
  5. } else {
  6.   echo "Přihlášení se nezdařilo";
  7. }


Vzhledem k tomu, že je celý přihlašovací process rozdělen do dvou kroků, stane se, že při přidělování oprávnění na základě loginu budou přidělena oprávnění jiného uživatele se stejným loginem, který byl nalezen jako první, tady „admin” a náš nově založený účet tedy získá jeho administrátorská práva.



Jak to bylo na SOOM.cz

Na našem serveru SOOM.cz byla situace podobná. I když jsme měli implementovány kontroly na nemožnost registrace dvou účtů se shodným loginem, nebylo bohužel nasazeno omezení, které by omezovalo délku loginu při registraci. Databázový sloupec pro login byl v našem případě typu text a umožňoval proto uchovávat loginy dlouhé až 65535 znaků. Stačilo se ale zaregistrovat s loginem „admin” následovaným šedesátipěti tisíci mezer a libovolným znakem na konci a registrace se zdařila.

Ověřovací process byl u nás samozřejmě odlišný od výše uvedených autentizačních kódů a tak k podobnému převzetí práv tímto způsobem naštěstí dojít nemohlo. Uživatel s duplicitním loginem se totiž nikdy nemohl ke svému novému účtu přihlásit, protože byla vždy vyžadována znalost hesla prvního z účtů. Co ovšem bylo možné, a kde se zranitelnosti SQL Truncation dalo zneužít, bylo zaslání náhradního hesla po jeho zapomenutí.


Formulář pro připomenutí hesla totiž umožňuje zaslání přístupových údajů nejen na základě zadaného loginu, ale i nicku. V případě zadání loginu, se náhradní heslo zaslalo správně na první z účtů nalezených v databázi. Pokud se ale zadal nick, který jsme si při registraci zvolili jedinečný, pak náhradní heslo putovalo na naši adresu. Do tabulky s náhradními hesly se v databázi ale uložila kombinace login : náhradní_heslo. Pokud jsme se tedy následně pokusili o přihlášení s těmito údaji, ocitli jsme se rázem na původním uživatelském účtu, který nám nepatřil.

Na SQL Truncation byly náchylné také například diskuze a komentáře, kdy neregistrovaný uživatel nemohl použít registrovaný nick. V případě, že by neregistrovaný uživatel použil registrovaný nick následovaný opět tisícemi mezer a nějakého toho postfixu, pak by kontrolou prošel, ale do databáze by se nick uložil bez postfixu. Snad se mi podařilo všechna kritická místa ošetřit...


Obrana

Obrana před útoky SQL Truncation spočívá například v osekání řetězců obdržených od uživatelů a teprve následných kontrol. V našem případě by tedy při registraci došlo k osekání znaků za hraniční délkou loginu, tedy za mezarami, následně by se odstranily bílé znaky na konci řetězce a teprve po té by se kontrolovalo, zda již stejný nick v databázi existuje.

Druhou variantou je použití operátoru LIKE namísto rovnítka v SQL dotazech, které při porovnání stejných řetězců s a bez koncových mezer vrátí false. Tímto řešením se ale vystavíte dalším problémům při vstupu se znakem procenta a s oštřováním case positive variant.

  1. SELECT * FROM users WHERE login LIKE 'admin'


Třetí možností je pak použít při SQL dotazech binární porovnání, které problémy s mezerami také čistě vyřeší:

  1. SELECT * FROM users WHERE BINARY login = 'admin'


Dalším elegantním a doporučeným postupem je nastavení striktního módu SQL, které znemožní ukládání ořezaných hodnot.

  1. SET sql_mode = 'STRICT_ALL_TABLES';


V neposlední řadě pomůže také nastavení unikátního klíče těm sloupcům, kde se vyžaduje ukládání jedinečných hodnot. Unikátní klíč vyloučí také vložení shodných záznamů s mezerami a bez nich.


Závěr

Co dodat… Nezbývá, než se poučit a počítat i s touto variantou útoku. A co vy? Podělte se v komentářích, jestli i některé z vašich kódů trpěli na SQL Truncation.


Použitý zdroj: http://resources.infosecinstitute.com/sql-truncation-attack


Líbil se Vám článek?
Budeme potěšeni, pokud vás zaujme také reklamní nabídka

Social Bookmarking

     





Hodnocení/Hlasovalo: 1.23/40

1  2  3  4  5    
(známkování jako ve škole)