Skip to content
On this page

SQL Injection

Co je SQL (Structured Query Language)

SQL je jazyk používaný pro komunikaci s relational databázemi, jako je MySQL, MariaDB, PostreSQL a další.
Tyto databáze jsou podobné excelovým tabulkám, mají více stránek (tables), pojmenované otypované sloupce (columns) a každá tabulka má spoustu řádků (rows). Spoustu aplikací používá SQL databáze na ukládání dat, jelikož kód, který ukládá a čte data ze souborů, může při větším množství dat pomalý a neefektivní.

SQL Queries

SQL Queries jsou jednotlivé žádosti, které aplikace posílá aby přečetla / uložila data do databáze. Jelikož Syntax SQL je blízký angličtině, je možné tyto queries přečíst a pochopit, co mají dělat.

sql
-- Přečti vše z tabulky users
SELECT * FROM users;

-- Přečti vše z tabulky users, kde id uživatele je 123
SELECT * FROM users WHERE id='123';

-- Zapiš do tabulky users nového uživatele se jménem marekvospel
INSERT INTO users SET (username) VALUES ('marekvospel');

Příklad použití SQL databáze v PHP aplikaci

V příkladě níže se PHP aplikace připojí k databázi, a na stránku vypíše uživatelské jméno uživatele, jehož id bylo zadáno v odkazu. (Query parametr "id")

php
<?php

// Vytvoří připojení
$conn = new mysqli('localhost:3306', 'root', 'heslo');

// Zkontroluje zda při připojování nedošlo k chybě
if ($conn->connect_error) {
    echo "MySQL connection error!";
}

// Pošle následující query
// SELECT * FROM users WHERE id='123';
$result = $conn->query("SELECT * FROM users WHERE id='" . $_GET["id"] . "';");

if ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        echo 'Username: ' . $row['username'];
    }
} else {
    echo 'Uživatel nenalezen';
}

SQL Injection

Nyní tedy k SQL Injection. Jelikož v příkladě výše vezmeme hodnotu $_GET["id"], a dáme ji do SQL query tak, že ho přidáme doprostřed textu SELECT... id=' a ';, nemáme jistotu co za znaky v $_GET["id"] bude Pokud tedy v id parametru bude např 123', SQL vyhodí chybu, jelikož příklad nebude platný.

sql
-- Jelikož v příkazu je ' navíc, SQL databáze nebude vědět co dělat, jelikož v příkazu je neukončený text (od poslední ', do konce celé query)
SELECT * FROM users WHERE id='123'';

Následující obsah parametru id tedy jak přečte uživatelské jméno, tak vrátí data z tabulky credit_cards. 123' UNION (SELECT card_number AS username FROM credit_cards); -- % tento payload způsobí že příkaz bude následující.

sql
-- Přečti vše z tabulky users, kde id uživatele je 123, a do výsledků přidej výsledek query (Přečti číslo karty, které přejmenuj na username, z tabulky kreditní karty)
SELECT * FROM users WHERE id='123' UNION (SELECT card_number AS username FROM credit_cards); -- %'

Jelikož aplikace čte pomocí pojmenovaných sloupců, je potřeba číslo karty přejmenovat na username. -- % na konci je komentář, aby nedošlo k výše vysvětlené chybě s neukončeným textem. (komentář je --, ale je třeba aby za ním byla mezera, proto radši přidávám mezeru a náhodný znak, jako %, kdyby náhodou aplikace ořezávala mezery na začátku / konci).

Dalším příkladem by byla třeba přihlašovací stránka, která kontroluje zda v databázi je uživatel s daným emailem a heslem. Aplikace může dělat následující žádost, a kontrolovat, zda databáze vrátila 1 nebo více hodnot.

sql
-- Přečti vše z tabulky users, kde email uživatele je (poslaný email), a heslo je (hash poslaného hesla)
SELECT * FROM users WHERE email='<POST param email>' AND password=HASHBYTES('SHA2_256', '<POST param password>');
-- Toto není správný příklad hashování hesla!!! V bezpečné aplikaci by se měl používat salt & pepper

Zde můžeme zneužít SQL Injection, jelikož aplikace opět neodebírá uvozovky.
Příklad: email: marek@vospel.cz' OR 1; -- %, password: jakýkoliv text

sql
-- Z takového payloadu vznikne následující příkaz
-- Přečti vše z tabulky users, kde email uživatele je marek@vospel.cz, nebo pravda
SELECT * FROM users WHERE email='marek@vospel.cz' OR 1; -- % AND password=HASHBYTES('SHA2_256', '<POST param password>');
-- Příkaz by se tedy dal simplifikovat na 
-- Přečti vše z tabulky users
SELECT * FROM users;

Jelikož takový příkaz vždy vrátí nějákého uživatele (pokud v databázi nějáký je), aplikace, která kontroluje, kdo se přihlásí, uživatele pustí, jelikož databáze uživatelů poslala hned několik a většinou se snaží získat pouze toho prvního, kterého to najde (email by měl být unikátní, takže neočekává že jich dostane víc.

Typy SQL Injection

Jeden z výše uvedených příkladů je Classic SQLi, přesněji UNION-based SQLi tedy jednoduchý útok, při kterém aplikace k datům, které má ukazovat normálně, přidá ještě data, o které útočník požádá.
Složitějším příkladem Classic SQLi je Error-based SQLi, tedy aplikace nezobrazuje data, o které útočník zažádal, ale je možné data exfiltrovat díky chybové hlášce, kterou aplikace vypisuje.

Další typ SQL Injection je tzv. Blind SQLi, tedy injekce, u které není možné vidět, co za data aplikace dostala. Příkladem tohoto typu útoku je příklad přihlášení. Aplikace neukazuje informace o uživateli, pouze zkontroluje zda takový uživtel existuje. Takový útok se nazývá Boolean-based SQLi, jelikož podle chování aplikace pouze víme, zda se útok povedl nebo ne. (true / false)

Druhým typem Blind SQLi je Time-based SQLi. Tento útok funguje na podobné bázi, jako Boolean-based SQLi, ale je možné ho provést i v případě, že aplikace vůbec neukáže změnu, ikdyž je váš útok úspěšný či neůspěšný. U tohoto typu útoku používáte SLEEP funkci. (Pokud je útok úspěšný, SLEEP x sekund) Podle délky načítání aplikace je možné rozeznat, zda útok byl / nebyl úspěšný.

Posledním typem útoku je Out-of-band SQLi. U tohoto útoku pošlete databázi takový query, který způsobí, že databáze udělá DNS, HTTP nebo jinou žádost, která obsahuje data, které se útočník snaží exfiltrovat.

Jak se proti SQL Injection bránit

Tento útok je podobně jako command injection způsoben neošetřeným uživatelským vstupem. V PHP je možné použít funkci mysqli_real_escape_string(connection, string), která dokáže znaky jako `'"`` atd. escapovat, takže je databáze nebude brát jako konec textu, ale bude je brát jako jeho součást.

Nejlepším řešením jak se proti SQL Injection chránit jsou Prepared Statements, které fungují takovým způsobem, že aplikace nejdříve pošle template pro daný SQL query, a poté data pošle zvlášť, tudíž SQL server ví, co je příkaz, a co jsou parametry a pokud se nový příkaz liší od toho starého, neprovede se.

Toto řešení je na úrovni SQL protokolu.

Příklad prepared statements

php
<?php
$conn = //connection
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id=?");

$id = $_GET["id"];

$stmt->bind_param("i", $id); // i znamená že id je integer (číslo)

$stmt->execute();

SQLMap

SQLMap je užitečný nástroj, který umí najít funkční typ SQLi, a poté ho využít. Narozdíl od lidí, SQLMap dokáže bez problémů vyřešit i blind SQLi, a to během chvíle. SQLMap má šikovný flag --wizard, který vás provede jednotlivými parametry, který SQLMap pro provedení útoku potřebuje.

Další zdroje

Shrnutí

  • Když vytváříme webovou stránku, která načítá informace z SQL databáze, používáme takzvané SQL statements
  • Pokud do statementů chceme přidat parametry jako ID, je nutné si uvědomit, že by se to nemělo dělat tak, že hodnotu pouze doplníme doprostřed statementu
  • Pokud text pouze doplňujeme a dostanem neočekávaný uživatelský vstup, díky doplnění znaků jako '" se může změnit význam původního statementu
  • Proti SQL Injection se můžeme bránit pomocí prepared statements
  • Nikdy nemůžeme věřit uživatelskému vstupu