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

Okno przeglądarki z wyświetlonym wynikiem zapytania

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:

Okno przeglądarki z wyświetlonym wynikiem zapytania

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 &#039;SELECT count(*) FROM &#039; || tabela
  INTO wynik;
  RETURN wynik;
END;
" alt="
DECLARE
  wynik bigint;
BEGIN
  EXECUTE &#039;SELECT count(*) FROM &#039; || 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/)