Skip to content

Latest commit

 

History

History
executable file
·
198 lines (138 loc) · 18.9 KB

sql-injection.md

File metadata and controls

executable file
·
198 lines (138 loc) · 18.9 KB

SQL-инъекция (внедрение SQL кода) и способы борьбы с ней

SQL-инъекция - это уязвимость, которая возникает, когда у злоумышленника появляется возможность модифицировать SQL запрос в приложении. Она может привести как минимум к краже любых данных из базы (например, списка пользователей). В особо запущенных случаях она позволяет менять данные в базе, читать или создавать файлы с произвольным содержимым на сервере и даже выполнять какие-то команды.

Чтобы понять, как работает эта уязвимость, надо представлять, что такое SQL базы данных, и как обычно приложение с ними работает. База данных - это такое хранилище информации. Работа с ним ведется путем отправки ему запросов на языке SQL. Запросы могут получать или изменять какие-то данные в базе. Например, запрос SELECT * FROM news ORDER BY added_date DESC LIMIT 10 запрашивает из базы данных 10 последних новостей (при условии что колонка added_date хранит дату публикации новости).

Часто у нас появляется необходимость подставлять в запрос пришедшие от пользователя данные. В случае, если это делать неправильно (не используя проверок и экранирования), мы можем создать уязвимость, которая позволит злоумышленнику взломать наше приложение.

SQL инъекции не привязаны к конкретному языку программирования и базе данных. Они могут быть в любом приложении, которое использует SQL запросы. Мы будем рассматривать примеры кода на PHP, работающем с сервером баз данных MySQL.

Примеры уязвимостей

Допустим, у нас есть страница просмотра архива новостей за определенный год. При переходе по ссылке /archive.php?year=2011 мы должны увидеть заголовки новостей за этот год. Рассмотрим пример неправильного уязвимого кода на PHP, реализующего эту задачу:

// Опасное место: $year содержит присланные пользователем данные, 
// в которых может быть что угодно
$year = $_GET['year'];

// Формируем SQL запрос, подставляя в него данные от пользователя
// безо всякой проверки. В этой строчке и содержится уязвимость.
// (SQL-функция YEAR() получает год из даты)
$sql = "SELECT title FROM news WHERE YEAR(added_date) = $year";

// выполняем запрос и получаем массив результатов
$titles = executeQuery($sql);

// Выводим заголовки в виде HTML-списка
echo "<ul>\n";
foreach ($titles as $title) {
    // защищаемся от XSS и экранируем выводимые данные
    $titleHtml = htmlspecialchars($title);
    echo "<li>$titleHtml</li>\n";
}
echo "</ul>\n";

Если приглядеться к строчке $sql = "SELECT title FROM news WHERE YEAR(added_date) = $year";, то видно, что мы подставляем в запрос пришедшие от пользователя данные безо всякой проверки. Тут и кроется уязвимость. Что, если пользователь передаст нам в параметре year такую строку?

0 UNION SELECT name FROM users

В этом случае после подстановки получится SQL запрос:

SELECT title FROM news WHERE YEAR(added_date) = 0 UNION SELECT email FROM users

Первая часть этого запроса не найдет ни одной новости, а вторая (после UNION) - выберет из базы email-ы всех наших пользователей и выведет их вместо заголовков новостей. Разумеется, злоумышленник не будет ограничиваться email-ами - он может по очереди выбрать все данные из нашей базы. Более того, есть программы, автоматизирующие такое извлечение данных - достаточно дать им уязвимую ссылку, а остальное они сделают сами.

В данном случае уязвимости можно было бы избежать, преобразовав $year в число:

$year = intval($_GET['year']);

Но это не сработает в том случае, если подставляемый параметр - строка. Правильный способ борьбы - использовать подготовленные запросы с плейсхолдерами (о них ниже).

Рассмотрим второй пример уязвимости. Допустим, у нас есть такой неправильный код проверки логина и хеша пароля (мы разумеется не храним в базе сами пароли, а только их соленый хеш):

$login = $_POST['login'];
$password = $_POST['password'];

// Функция получает хеш пароля с солью, так как мы следуем 
// рекомендациям и не храним в базе пароли
$hash = getSaltedHash($login, $password);

// Проверяем, есть ли в базе пользователь с таким логином и хешем пароля
// В этой строчке уязвимость
$sql = "SELECT COUNT(*) FROM users WHERE login = '$login' AND hash = '$hash'";
$count = getOneResult($sql);

if ($count == 1) {
    echo "Вы успешно вошли в систему\n";
    ....
} else {
    echo "Неправильный логи или пароль\n";
    ....
}

Здесь мы опять подставляем данные от пользователя в запрос и как следствие получаем уязвимость. Злоумышленник может передать такой логин: login' -- . При подстановке в запрос получается:

SELECT COUNT(*) FROM users WHERE login = 'login' -- ' AND hash = 'xxxxx'

Знак -- обозначает в языке SQL комментарий и сервер базы данных проигнорирует все, что за ним, таким образом из условия пропадет проверка на совпадение хеша, что позволяет злоумышленнику залогиниться под любым логином, не зная пароля.

В данном случае методом защиты (кроме самого правильного способа - использования плейсхолдеров и подготовленных запросов) могло бы быть экранирование переданной строки. Например, в библиотеке mysqli это делается методом real_escape_string():

$escapedLogin = $mysqli->real_escape_string($login);
$sql = "... WHERE login = '$escapedLogin' ... ";

В PDO экранирование делается методом quote(), который не только экранирует переданную строку, но еще и заключает ее в нужные кавычки (в зависимости от используемой базы данных):

$quotedLogin = $pdo->quote($login);
$sql = "... WHERE login = $quotedLogin ... ";

При экранировании перед спецсимволами вроде символа кавычки (а также двойной кавычки, перевода строки, бекслеша) подставляются бекслеши. Таким образом, переданная злоумышленником строка login' -- преобразуется в login\' --, и экранированная бекслешем кавычка не закрывает строку, а является ее частью. И символ -- тоже воспринимается как часть строки, а не комментарий. Получается запрос WHERE login = 'login\' -- ' AND hash = '...' который работает как и задумано. Подробнее про экранирование можно прочесть в мануале по MySQL рус. англ..

Иногда результаты уязвимого запроса не выводятся на экран. Но даже в таком случае злоумышленник может получать данные, используя задержку в выполнении запроса (делая SQL запрос такого вида: если логин администратора начинается на "а", то сделать паузу в 5 секунд). Это называется "слепая" SQL инъекция.

Эксплуатация уязвимости

Найдя уязвимость, злоумышленник может полностью прочитать содержимое базы данных, изменять его. Также, в некоторых случаях он получает возможность читать и записывать файлы на сервере за счет соответствующих команд в SQL. Например, MySQL позволяет записать произвольные значения из таблицы в любой файл командой SELECT ... INTO OUTFILE '/var/www/example.com/file.txt', а также прочитать любой файл командой LOAD DATA INFILE '/etc/passwd' .... Конечно, для этого ему еще надо найти доступные на чтение или запись файлы, но я думаю, что при желании это вполне возможно.

Но даже доступ к базе многое дает. Злоумышленник может прочитать данные администратора или создать нового пользователя с такими правами. Так он получит доступ к админке сайта, в которой сможет найти другие уязвимости (например загрузку на сервер произвольных файлов).

В некоторых случаях (зависит от конфигурации сервера) некоторые базы данных позволяют через SQL запрос запустить произвольную программу на сервере.

Также, база данных может содержать какие-то персональные данные. Вот пример, показывающий какие последствия для бизнеса может иметь утечка данных.

В 2015 году злоумышленники взломали платный сайт знакомств "Эшли Мэддисон", который предлагал респектабельным женатым мужчинам возможность найти развлечения на стороне. Хакеры выложили в интернет личные данные пользователей, включавшие в себя имена, email, адрес и географические координаты пользователей. Кроме того, выяснилась еще пара интересных подробностей - во-первых, более 99% активных пользователей сайта были мужчинами, так что успешно познакомиться друг с другом могли разве что представители нетрадиционных ориентаций, во-вторых, несмотря на то, что услуга удаления аккаунта с сайта была платной, сайт в реальности не удалял данные. Очевидно, что после такой утечки сайт фактически прекратил свое существование.

Методы борьбы

Для борьбы с уязвимостью надо использовать подготовленные запросы, когда данные не вставляются в запрос напрямую, а передаются отдельно. В этом случае сама база данных беспокоится о корректном экранировании и модифицировать запрос невозможно. Вот пример проверки логина с использованием подготовленных запросов в PDO (мануал):

// Подготавливаем запрос
$statement = $pdo->prepare("SELECT COUNT(*) FROM users WHERE login = :login AND hash = :hash");
// задаем значения плейсхолдеров. PDO или база данных сами позаботятся о коректной
// вставке этих значений в запрос
$statement->bindValue(':login', $login);
$statement->bindValue(':hash', $hash);
// Выполняем запрос и получаем результат 
$count = $statement->fetchCoumn();

Вот, как можно использовать подготовленные запросы с библиотекой mysqli (мануал):

// Подготавливаем запрос
$statement = $mysqli->prepare("SELECT COUNT(*) FROM users WHERE login = ? AND hash = ?");
if (!$statement) {
    throw new Exception("MySQLi prepare error: {$mysqli->errno} {$mysqli->error}");
}

// передаем значения для пдейсхолдеров в том же порядке, 
// в котором они идут в запросе
// Буква "s" задает тип параметра - строка
if (!$statement->bind_param("s", $login)) {
    throw new Exception("MySQLi bind login error: {$mysqli->errno} {$mysqli->error}");
}
if (!$statement->bind_param("s", $hash)) {
    throw new Exception("MySQLi bind hash error: {$mysqli->errno} {$mysqli->error}");
}

// Выполняем запрос и получаем результат
if (!$statement->execute()) {
    throw new Exception("MySQLi execute error: {$mysqli->errno} {$mysqli->error}");
}

$result = $statement->get_result();

Код получился немного громоздкий из-за того, что в mysqli все ошибки надо проверять вручную, но суть в общем понятна.

Использование параметризованных запросов позволяет не беспокоиться о безопасности и сохраняет запрос читабельным.

Через плейсхолдеры можно подставлять только числа или строки, но нельзя подставлять имена таблиц или колонок. В этом случае придется вставлять их напрямую в запрос, проверив их по списку разрешенных значений. Например, если нам надо сортировать новости по заголовку или дате в зависимости от переданного параметра, можно поступить так:

$sort = $_GET['sort'];
// Разрешенные значения сортировки
$allowed = ['title', 'added_date'];

// Гарантируем, что в переменной будет только допустимое значение
if (!in_array($sort, $allowed)) {
    $sort = $allowed[0];
}

$sql = "SELECT * FROM news ORDER BY $sort LIMIT 10";

Другие способы борьбы

Кроме использования плейсхолдеров, можно вручную экранировать строковые значения через функции вроде real_escape_string() или quote() и intval/floatval для чисел, но это замусоривает код, и появляется вероятность, что кто-то подставит в запрос неэкранированное значение.

Экранирование или проверка данных всегда должны делаться рядом с тем местом, где выполняется запрос (а не в другом месте кода), чтобы с одного взгляда на функцию можно было понять, безопасная она или нет. Подготовленные запросы соответствуют этому требованию.

Дополнительные ссылки

Предупреждение

Не пытайся искать уязвимости на чужих сайтах без разрешения владельца. Это может быть уголовно наказуемым деянием.