Zrozumieć ataki SQL Injection

Wstęp

SQL Injection to popularna technika ataku. Polega na wstrzykiwaniu złośliwego kodu SQL do zapytań, aby manipulować bazą danych i uzyskać dostęp do nieautoryzowanych informacji. W tym artykule przedstawimy, jak są przeprowadzane ataki SQL Injection. Omówimy przykłady w oparciu o prostą aplikację PHP i pokażemy, jak zabezpieczyć aplikację przed tym rodzajem ataków.

Środowisko badawcze: XAMPP z Apache, PHP i MariaDB

W celach edukacyjnych i demonstracyjnych, będziemy korzystać ze środowiska XAMPP, które zawiera następujące aplikacje i konfiguracje:

  • Apache/2.4.54 (Win64) OpenSSL/1.1.1p
  • PHP/8.1.12
  • Wersja klienta bazy danych: libmysql – mysqlnd 8.1.12
  • Rozszerzenia PHP: mysqli, curl, mbstring

XAMPP to łatwy w obsłudze pakiet oprogramowania. Oferuje zintegrowane środowisko do tworzenia, testowania i uruchamiania aplikacji PHP i baz danych MariaDB (fork MySQL) na platformie Windows. Dzięki XAMPP, możemy szybko rozpocząć eksperymentowanie z różnymi aspektami bezpieczeństwa aplikacji internetowych.

W kolejnych rozdziałach artykułu przeanalizujemy przykład prostej aplikacji PHP. Jest ona podatna na ataki SQL Injection. Następnie pokażemy, jak zabezpieczyć tę aplikację, stosując różne techniki i metody ochrony.

Prosta aplikacja PHP z formularzem: Omówienie i analiza podatności na SQL Injection

W celach edukacyjnych stworzyliśmy prostą aplikację PHP. Składa się z formularza logowania oraz skryptu PHP, który przetwarza dane wprowadzone przez użytkownika. Skrypt PHP wykonuje zapytanie SQL, aby sprawdzić, czy wprowadzone przez użytkownika dane są prawidłowe. Aplikacja ta służy jako przykład do omówienia podatności na ataki SQL Injection.

Aplikacja składa się z następujących elementów:

Formularz logowania HTML, który pozwala użytkownikowi wprowadzić nazwę użytkownika i hasło:

<form action="login.php" method="post">
  <label for="username">Username:</label>
  <input type="text" name="username" id="username" required>
  <br>
  <label for="password">Password:</label>
  <input type="password" name="password" id="password" required>
  <br>
  <input type="submit" value="Log in">
</form>

Tabela w bazie danych wraz w przykładowymi rekordami:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE,
    first_name VARCHAR(255),
    last_name VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (username, password, email, first_name, last_name)
VALUES
    ('admin', 'qwer1234', 'admin@example.com', 'Jan', 'Kowalski'),
    ('superadmin', 's3cr3t123', 'superadmin@example.com', 'Agnieszka', 'Zielińska'),
    ('tester', 'test1234', 'tester@example.com', 'Michał', 'Szymański'),
    ('prezes', 'prezes987', 'prezes@example.com', 'Katarzyna', 'Krawczyk');

Skrypt PHP, który łączy się z bazą danych i wykonuje zapytanie SQL na podstawie wprowadzonych danych:

<?php
$username = $_POST['username'];
$password = $_POST['password'];

$conn = mysqli_connect("localhost", "root", "", "sqli");

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
echo $sql.'<br>';

if (mysqli_multi_query($conn, $sql)) {
    $result = mysqli_store_result($conn);

    if (mysqli_num_rows($result) > 0) {
        echo "User(s) logged in:<br>";
        while ($row = mysqli_fetch_assoc($result)) {
            echo "ID: " . $row["id"] . " - Username: " . $row["username"] . " - Email: " . $row["email"] . "<br>";
        }
    } else {
        echo "Login failed!";
    }
} else {
    echo "Error: " . mysqli_error($conn);
}

mysqli_close($conn);
?>

W aplikacji używamy funkcji mysqli_multi_query do obsługi zapytań SQL. Ta funkcja pozwala na przeprowadzenie wielu zapytań SQL jednocześnie, co jest przydatne w celach edukacyjnych, aby zademonstrować ataki SQL Injection, takie jak usunięcie tabeli poprzez wprowadzenie '; DROP TABLE users; #.

Należy zauważyć, że używanie mysqli_multi_query w rzeczywistych aplikacjach jest niezalecane, ponieważ zwiększa podatność na ataki SQL Injection. Zamiast tego, należy używać funkcji mysqli_query lub parametryzowanych zapytań, które są bezpieczniejsze.

Przykład ataku SQL Injection, który działa w tej aplikacji, to wprowadzenie ciągu ' OR 1=1# w polu „Username”. Ten atak pozwala na zalogowanie się bez znajomości poprawnego hasła, ponieważ zapytanie SQL zwraca wszystkich użytkowników, pomijając warunek hasła. Znacznik # sprawia, że reszta linii jest traktowana jako komentarz, a więc nie jest wykonywana przez serwer bazy danych.

Celem tego przykładu jest pokazanie, jak ważne jest zabezpieczanie aplikacji przed atakami SQL Injection. Chcemy uświadomić programistom zagrożenia związane z niewłaściwą obsługą danych wejściowych.

Przykład 1: ' OR 1=1#

Wprowadzenie ciągu „' OR 1=1#” w polu „Username” powoduje, że zapytanie SQL zwraca wszystkich użytkowników, pomijając warunek hasła. Znacznik # sprawia, że reszta linii jest traktowana jako komentarz.

Przykład 2: '; DROP TABLE users; #

Wprowadzenie ciągu „'; DROP TABLE users; #” w polu „Username” powoduje usunięcie tabeli „users” z bazy danych. Warto zauważyć, że większość konfiguracji baz danych domyślnie nie pozwala na wykonanie wielu zapytań w jednym wywołaniu. W związku z czym atak może nie zadziałać w większości przypadków.

Zabezpieczenie aplikacji przed SQL Injection

W celu zabezpieczenia aplikacji przed atakami SQL Injection, można zastosować następujące środki:

Parametryzowane zapytania

Stosowanie parametryzowanych zapytań pomaga uniknąć wstrzykiwania złośliwego kodu SQL przez oddzielenie danych wejściowych od struktury zapytania.

<?php
$username = $_POST['username'];
$password = $_POST['password'];

$conn = mysqli_connect("localhost", "db_user", "db_password", "db_name");

if (!$conn) {
    die("Connection failed: " . mysqli_connect_error());
}

$sql = "SELECT * FROM users WHERE username = ? AND password = ?";
$stmt = mysqli_prepare($conn, $sql);

if ($stmt) {
    mysqli_stmt_bind_param($stmt, "ss", $username, $password);
    mysqli_stmt_execute($stmt);

    $result = mysqli_stmt_get_result($stmt);
    if (mysqli_num_rows($result) > 0) {
        echo "User(s) logged in:<br>";
        while ($row = mysqli_fetch_assoc($result)) {
            echo "ID: " . $row["id"] . " - Username: " . $row["username"] . " - Email: " . $row["email"] . "<br>";
        }
    } else {
        echo "Login failed!";
    }

    mysqli_stmt_close($stmt);
} else {
    echo "Error: " . mysqli_error($conn);
}

mysqli_close($conn);
?>

W powyższym kodzie zamiast bezpośredniego wstawiania danych użytkownika do zapytania SQL, używamy funkcji mysqli_prepare() do przygotowania szablonu zapytania z symbolami zastępczymi ?. Następnie, funkcja mysqli_stmt_bind_param() wiąże zmienne $username i $password z symbolami zastępczymi. Parametr „ss” w mysqli_stmt_bind_param() oznacza, że obie zmienne są typu string. Na końcu, mysqli_stmt_execute() wykonuje zapytanie z uwzględnieniem powiązanych wartości.

Filtrowanie i walidacja danych wejściowych

Weryfikowanie i filtrowanie danych wejściowych może pomóc ograniczyć możliwość przeprowadzenia ataku SQL Injection. Należy sprawdzić, czy dane wejściowe spełniają oczekiwane kryteria, takie jak długość, typ i format.

Oto przykład kodu PHP, który pokazuje, jak filtrować i walidować dane wejściowe przed ich użyciem w zapytaniach SQL:

<?php
$username = $_POST['username'];
$password = $_POST['password'];

// Walidacja długości danych wejściowych
if (strlen($username) > 50 || strlen($password) > 50) {
    die("Invalid input: username and password must be 50 characters or less.");
}

// Walidacja typu danych wejściowych (opcjonalnie, jeśli oczekujemy tylko liter i cyfr)
if (!ctype_alnum($username) || !ctype_alnum($password)) {
    die("Invalid input: username and password must only contain letters and numbers.");
}

$conn = mysqli_connect("localhost", "db_user", "db_password", "db_name");

if (!$conn) {
    die("Connection failed: " . mysqli_connect_error());
}

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);

if (mysqli_num_rows($result) > 0) {
    echo "User(s) logged in:<br>";
    while ($row = mysqli_fetch_assoc($result)) {
        echo "ID: " . $row["id"] . " - Username: " . $row["username"] . " - Email: " . $row["email"] . "<br>";
    }
} else {
    echo "Login failed!";
}

mysqli_close($conn);
?>

W powyższym kodzie wprowadzono dwie proste metody walidacji danych wejściowych:

  1. Walidacja długości: sprawdzenie, czy długość nazwy użytkownika i hasła nie przekracza 50 znaków.
  2. Walidacja typu: sprawdzenie, czy nazwa użytkownika i hasło składają się tylko z liter i cyfr (opcjonalne, w zależności od wymagań aplikacji).

Te proste metody walidacji pomagają ograniczyć możliwość wprowadzenia złośliwego kodu SQL przez użytkownika. Jednak warto zauważyć, że walidacja danych wejściowych jest tylko jednym z wielu kroków, które należy podjąć, aby zabezpieczyć aplikację przed atakami SQL Injection. Stosowanie parametryzowanych zapytań jest zdecydowanie zalecaną praktyką dla jeszcze większego zabezpieczenia.

Zabezpieczenie mysqli_real_escape_string()

Funkcja mysqli_real_escape_string() jest kolejną metodą ochrony przed atakami SQL Injection. Ta funkcja dodaje znaki ucieczki do specjalnych znaków w łańcuchach, które mają być użyte w zapytaniach SQL, co sprawia, że są one bezpieczne do użycia w zapytaniach. Oto jak można zastosować mysqli_real_escape_string() w naszym przykładzie:

<?php
$username = $_POST['username'];
$password = $_POST['password'];

$conn = mysqli_connect("localhost", "db_user", "db_password", "db_name");

if (!$conn) {
    die("Connection failed: " . mysqli_connect_error());
}

// Użycie mysqli_real_escape_string() do zabezpieczenia danych wejściowych
$escaped_username = mysqli_real_escape_string($conn, $username);
$escaped_password = mysqli_real_escape_string($conn, $password);

$sql = "SELECT * FROM users WHERE username = '$escaped_username' AND password = '$escaped_password'";
$result = mysqli_query($conn, $sql);

if (mysqli_num_rows($result) > 0) {
    echo "User(s) logged in:<br>";
    while ($row = mysqli_fetch_assoc($result)) {
        echo "ID: " . $row["id"] . " - Username: " . $row["username"] . " - Email: " . $row["email"] . "<br>";
    }
} else {
    echo "Login failed!";
}

mysqli_close($conn);
?>

W powyższym kodzie użyliśmy mysqli_real_escape_string() do zabezpieczenia danych wejściowych przed użyciem w zapytaniu SQL. Chociaż mysqli_real_escape_string() może pomóc w ochronie przed atakami SQL Injection, warto pamiętać, że najlepszą praktyką jest stosowanie parametryzowanych zapytań, ponieważ są one jeszcze bardziej odporne na tego rodzaju ataki.

Ograniczenie uprawnień użytkowników bazy danych

Ograniczenie uprawnień użytkowników bazy danych jest ważnym krokiem w zabezpieczaniu aplikacji przed atakami SQL Injection. Dzięki przydzielaniu tylko niezbędnych uprawnień dla kont użytkowników bazy danych, można zmniejszyć ryzyko uszkodzenia danych w przypadku udanego ataku. Zasada minimalnych uprawnień polega na tym, że użytkownikowi bazy danych powinny być przydzielone tylko te uprawnienia, których rzeczywiście potrzebuje do wykonywania swoich zadań.

Na przykład, jeśli użytkownik bazy danych ma tylko odczytywać dane z tabel, nie powinien mieć uprawnień do ich modyfikacji, usuwania lub tworzenia nowych tabel.

Aby skonfigurować uprawnienia dla użytkowników bazy danych w MySQL/MariaDB, można użyć następujących poleceń SQL:

Tworzenie nowego użytkownika z ograniczonymi uprawnieniami:

CREATE USER 'limited_user'@'localhost' IDENTIFIED BY 'strong_password';

Przydzielenie tylko niezbędnych uprawnień dla użytkownika (na przykład, odczyt danych z tabeli users):

GRANT SELECT ON database_name.users TO 'limited_user'@'localhost';

Zastosowanie zmian uprawnień:

FLUSH PRIVILEGES;

W przypadku aplikacji PHP, należy zmienić dane logowania w skrypcie łączącym się z bazą danych, aby używać konta limited_user zamiast konta root:

$conn = mysqli_connect("localhost", "limited_user", "strong_password", "database_name");

Pamiętaj, że konfiguracja uprawnień użytkowników bazy danych powinna być dostosowana do konkretnych potrzeb aplikacji. Warto również regularnie przeglądać uprawnienia użytkowników i aktualizować je w miarę potrzeb, aby zapewnić najwyższy poziom bezpieczeństwa.

Inny przykład ataku SQL Injection w PHP

Aby lepiej zrozumieć, jak aplikacje mogą być narażone na ataki SQL Injection, przeanalizujmy kolejny przykład. W poniższym kodzie PHP, aplikacja wyświetla zawartość strony na podstawie wartości przekazanej przez parametr GET 'page’. Dodatkowo, inkrementuje licznik wyświetleń dla danej strony. Niestety, w tym przypadku nie zabezpieczono danych wejściowych przed atakami SQL Injection.

<?php
$page = $_GET['page'];

$conn = mysqli_connect("localhost", "root", "", "sqli");

// Zapytanie do pobrania zawartości strony
$sql_page = "SELECT * FROM content WHERE id = $page";
// Zapytanie do inkrementacji licznika odwiedzin
$sql_increment_views = "UPDATE content SET views = views + 1 WHERE id = $page";
// Przygotowanie stringa z zapytaniami
$sql = $sql_page . "; " . $sql_increment_views;

// Wykonanie wielokrotnego zapytania
mysqli_multi_query($conn, $sql);

// Otrzymanie rezultatu pierwszego zapytania
$result_page = mysqli_store_result($conn);

// Wyświetlenie danych pobranych z bazy na stronie
if (mysqli_num_rows($result_page) > 0) {
    $row = mysqli_fetch_assoc($result_page);
    echo "Title: " . $row["title"] . "<br>";
    echo "Content: " . $row["body"] . "<br>";
    echo "Views: " . $row["views"] + 1 . "<br>";
} else {
    echo "No content found for page " . $page;
}

mysqli_close($conn);
?>

Tabela oraz jej przykładowa zawartość:

CREATE TABLE content (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL,
    views INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO content (title, body)
VALUES
    ('Title 1', 'Content for page 1'),
    ('Title 2', 'Content for page 2'),
    ('Title 3', 'Content for page 3');

Zawartość strony o id równym 1 uzyskujemy poprzez wejście pod adres:

http://localhost/index.php?page=1

W powyższym przykładzie, atakujący może manipulować wartością 'page’ w celu uzyskania nieautoryzowanego dostępu do danych lub zmiany zachowania aplikacji. Na przykład, atakujący może wprowadzić wartość:

index.php?page=1; INSERT INTO content (title, body) VALUES ('Injected Title', 'Injected Content');

Pozwala to na dodanie nowego rekordu do tabeli „content” z niepożądanym tytułem i zawartością.

Zawartość tabeli content z wstawionym poprzez atak SQL Injection rekordem

Ponadto, atakujący może także wprowadzić wartość:

index.php?page=1; DROP TABLE content;

Spowoduje to usunięcie tabeli „content” z bazy danych.

Aby zabezpieczyć ten przykład przed atakami SQL Injection, można zastosować techniki opisane wcześniej w artykule, takie jak stosowanie parametryzowanych zapytań, filtrowanie i walidacja danych wejściowych oraz ograniczenie uprawnień użytkowników bazy danych. Stosując te metody, aplikacja będzie bardziej odporna na ataki SQL Injection, co przyczyni się do ochrony danych i poprawy bezpieczeństwa aplikacji.

Podsumowanie

W tym artykule omówiliśmy zagadnienie SQL Injection, przedstawiając różne metody zabezpieczania aplikacji przed tego rodzaju atakami, takie jak stosowanie parametryzowanych zapytań, filtrowanie i walidacja danych wejściowych oraz ograniczenie uprawnień użytkowników bazy danych. Bazując na przykładzie prostej aplikacji PHP z formularzem logowania oraz dodatkowym przykładzie związanych z manipulacją wartością 'page’, przedyskutowaliśmy koncept SQL Injection oraz jego wpływ na bezpieczeństwo aplikacji, jak przeprowadzić atak oraz jakie zapytania są wykorzystywane w takim ataku. Podkreśliliśmy, że zastosowanie opisanych technik może pomóc zabezpieczyć aplikację przed takimi atakami i zwiększyć jej bezpieczeństwo, a stosowanie parametryzowanych zapytań jest jedną z najlepszych praktyk w tym zakresie. Aby zapewnić wysoki poziom bezpieczeństwa aplikacji, warto stosować przedstawione metody łącznie oraz regularnie przeglądać i aktualizować praktyki zabezpieczania aplikacji.

Leave a Comment

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Scroll to Top