Podręcznik
5. Wstrzykiwanie
5.3. Wstrzykiwanie kodu SQL
Jak łatwo można wywnioskować z poprzednich przykładów, w tym przypadku celem atakującego jest modyfikacja zapytań SQL wykonywanych do bazy danych poprzez wstrzyknięcie fragmentów kodu do zapytań SQL lub, o czym się często zapomina do procedur osadzonych w bazie danych. Podobnie jak poprzednio wynika to z przekazania danych wprowadzonych przez użytkownika do bazy danych bez odpowiedniej weryfikacji. Udane wykorzystanie podatności prowadzi do:
- ujawnienia zawartości bazy danych, w tym informacji chronionych jak np. pytania bezpieczeństwa i odpowiedzi na nie,
- modyfikacji zawartości bazy danych,
- skasowania zawartości bazy danych.
Aby zobrazować problem zapoznajmy się z przykładem, który jest najbardziej klasycznym wariantem tej podatności.
Załóżmy, że nasza baza danych wygląda następująco:
USE demo;
CREATE TABLE users(
id INTEGER AUTO_INCREMENT,
username TEXT,
PRIMARY KEY(id));
INSERT INTO users(username) VALUES ('a'),('b'),('c'),('d');
A skrypt wyświetlający dane ma postać:
<?php
$mysqli = new mysqli("localhost","root","","demo");
if ($mysqli -> connect_errno) {
echo "Failed to connect to MySQL: " . $mysqli -> connect_error;
exit();
}
if ($_GET["id"]==""){
echo "Invalid id"; exit();
}
if ($result = $mysqli -> query("SELECT * FROM users WHERE id=".$_GET["id"].";")) {
while($row = $result->fetch_assoc()) {
echo "id: " . $row["id"]. " - Content: " . $row["username"]. "<br/>";
}
$result -> free_result();
}
$mysqli -> close();
Zauważmy, że zapytanie SQL powstaje w wyniku połączenia stałego kodu SQL z częścią pobieraną z zapytania GET przesłanego przez użytkownika. W normalnym przypadku
http://127.0.0.1:8088/users.php?id=1
wyświetlony zostanie oczekiwany rezultat

Jednakże, jeżeli zmodyfikujemy zapytanie na
http://127.0.0.1:8088/users.php?id=1%20or%201=1
efektem będzie wyświetlenie wszystkich rekordów:

Gdyż wykonane zapytanie będzie wyglądać następująco
SELECT * FROM users WHERE id=1 or 1=1;
W przypadku języka PHP funkcja mysql_query nie dopuszcza wykonania wielu zapytań w jednym wywołaniu (zapytań łańcuchowych), przez co próby typu
http://127.0.0.1:8088/users.php?id=1;DROP%20TABLE%20users;--%20
się nie powiodą, ale w innych językach takie zapytania są możliwe.
Należy też pamiętać, że SQL injection może zostać powiązane z dodatkową podatnością jak path traversal do uzyskania zdalnego wykonania kodu. Przykładowo zapytanie:
http://127.0.0.1:8088/users.php?id=-1%20UNION%20SELECT%200,%27%3C?php%20echo%20"!";%27%20INTO%20OUTFILE%20%27/var/lib/mysql-files/test.php%27;
tłumaczące się na SQL jako
SELECT * FROM USERS WHERE id=-1 UNION SELECT 0,'<?php echo "!";' INTO OUTFILE '/var/lib/mysql-files/test.php';
Zapisze plik o treści
<?php echo "!";
W katalogu /var/lib/mysql-files/ i jeżeli istnieje podatność path-traversal pozwalająca na wykonanie tego skryptu mamy możliwość wykonania dowolnego kodu zdalnie (RCE – remote code execution).
Projektując bazę danych należy unikać sklejania zapytań z danych przekazywanych z zewnątrz także w procedurach osadzonych, gdyż może to prowadzić do podobnych efektów. Przykładowa, podatna funkcja w bazie danych PostgreSQL wykorzystująca operator || - sklejania tekstów może wyglądać następująco:
CREATE FUNCTION policz_wiersze(tabela text)
RETURNS bigint
LANGUAGE plpgsql AS
<span class="nolink"><span class="MathJax_Preview"><a href="https://esezam.okno.pw.edu.pl/filter/tex/displaytex.php?texexp=%0D%0ADECLARE%0D%0A%20%20wynik%20bigint%3B%0D%0ABEGIN%0D%0A%20%20EXECUTE%20%27SELECT%20count%28%2A%29%20FROM%20%27%20%7C%7C%20tabela%0D%0A%20%20INTO%20wynik%3B%0D%0A%20%20RETURN%20wynik%3B%0D%0AEND%3B%0D%0A" id="action_link687c7bcf417272" class="" title="TeX" ><img class="texrender" title="
DECLARE
wynik bigint;
BEGIN
EXECUTE 'SELECT count(*) FROM ' || tabela
INTO wynik;
RETURN wynik;
END;
" alt="
DECLARE
wynik bigint;
BEGIN
EXECUTE 'SELECT count(*) FROM ' || tabela
INTO wynik;
RETURN wynik;
END;
" src="https://esezam.okno.pw.edu.pl/filter/tex/pix.php/ff33cdd0275a98de330410768b7fb938.gif" /></a></span><script type="math/tex">
DECLARE
wynik bigint;
BEGIN
EXECUTE 'SELECT count(*) FROM ' || tabela
INTO wynik;
RETURN wynik;
END;
</script></span>;
Tak skonstruowana funkcja może być nadużyta, o ile zawartość parametru tabela jest kontrolowana przez użytkownika. Ukrycie podatności wewnątrz funkcji, utworzonej w trakcie budowania schematu bazy danych znacząco utrudnia wykrycie takiej podatności na podstawie analizy kodu samej aplikacji.
Obrona sprowadza się do przestrzegania kilku zasad:
- używanie parametryzowanych kwerend (PREPARE) -- silnik bazy danych zadba o poprawne przekazanie parametrów,
- alternatywnie -- ścisła weryfikacja zawartości zmiennych wykorzystywanych w kwerendach -- lepiej nie wykonać niż przepuścić injection -- np. jeśli parametr ID ma być liczbą całkowitą to przepuszczamy go tylko wtedy gdy zawiera jedynie cyfry,
- odpowiednia konfiguracja bazy danych (ograniczenie zapisu, wyłączenie niepotrzebnych funkcji jak np. xp_cmdshell w MS SQL Severze) i uprawnienia systemu plików,
- unikać kwerend i funkcji budowanych dynamicznie, jeśli potrzeba używać w tym celu funkcji formatujących jak format() w PostgreSQL,
- korzystać, o ile to możliwe z systemów mapujących bazę danych na obiekty (ORM) jak Doctrine w PHP (https://www.doctrine-project.org/)