Как исправить sql injection

Время на прочтение
8 мин

Количество просмотров 22K

В прошлой статье мы уже рассматривали тему SQL-инъекций.

SQL-инъекции’ union select null,null,null —

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

habr.com

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

SQL-инъекция – это попытка злоумышленника изменить запрос к базе данных для ее компрометации.

Возможные SQLi:

  • Соблюдение условия WHERE к истинному результату при любых значениях параметров.

  • Объединение запросов через оператор UNION.

  • Комментирование части запроса.

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

Как выявлять SQL-инъекции

Статья носит информационный характер. Не нарушайте законодательство.

Ручной поиск

Шаг 1.

Во все параметры подставляем спецсимволы (кавычки, двойные кавычки и т.д. ). В первую очередь проверяются параметры, принимающие в качестве аргументов пользовательский ввод. Если при передаче сервер вернул ошибку, то есть подозрение на наличие инъекции. Когда веб-приложение не отреагировало на спецсимволы, то возможны 2 причины:

  • SQL-инъекции нет.

  • Отключен вывод ошибок на веб-сервере.

Пример:

http://example.com/?id=1’

http://example.com/?id=1” 

http://example.com/?id=1’) 

и т. д.

Шаг 2.

Если инъекция обнаружена, то используем метод UNION-Based, который применяется, если SQL-инъекция возникает в запросе с использованием оператора SELECT. Благодаря такому методу можно объединить два запроса в один набор результатов. Особенность его заключается в том, что он будет работать только в случае, если количество столбцов, возвращаемых первым запросом, будет равно их количеству, возвращаемых во втором запросе. Для определения количества столбцов можно воспользоваться 3 методами:

  1. Добавление столбца при каждой итерации проверки. Это не совсем удобно, так как их может быть бесконечное количество.

    Пример:

    ?id=1' union select null --
    
    ?id=1' union select null,null --
    
    ?id=1' union select null,null,null --

    и т.д.

    Поиск количества столбцов

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

  2. Использование оператора ORDER BY для определения количества столбцов. В данном случае нужно опираться на появление ошибки о несоответствии количества столбцов в запросе. Стоит начать с большого количества столбцов и уменьшать их вдвое при каждой итерации проверки. Если количество не будет соответствовать фактическому значению, то вернется ошибка:

    ?id=1' order by 20 --
    
    ?id=1' order by 10 --
    
    ?id=1' order by 5 --

    и т.д.

    Проверка с помощью order by

  3. Применение оператора GROUP BY, который основывается на обратном методе проверки, в отличие от ORDER BY.

    Пример:

    ?id=1' group by 5 --
    
    ?id=1' group by 10 --
    
    ?id=1' group by 20 -- 

    и т.д.

Теперь, когда мы знаем, сколько столбцов имеет текущая таблица, используем UNION SELECT, чтобы увидеть, какой столбец уязвим. Уязвимым столбцом будет являться тот, данные которого отображаются на странице.

Пример:

?id=1’ union select 1,2,3,4 --

Когда уязвимый столбец найден, вместо его названия можем указать полезные команды для сбора информации о СУБД и получения данных из интересующих таблиц:

?id=1’ union select 1,null,version(),null – версия СУБД

?id=1’ union select 1,null,database(),null – текущая база данных

?id=1’ union select 1,null,@@port,null – порт, используемый СУБД

?id=1’ union select 1,null,user(),null – пользователь СУБД

?id=1’ union select 1,null,table_name,null from information_schema.tables – список таблиц с применением information_schema.

Шаг 3.

Если наличие инъекции при подстановке спецсимволов не подтвердилось, то воспользуемся одной из техник поиска слепых инъекций:

  • Boolean Based SQLi

Такой метод эксплуатации слепых SQL-инъекций, при котором информация извлекается исходя из реакции на условные выражения. Атака называется «слепой» в тех случаях, когда нет видимой реакции от веб-приложения. Например, при подстановке кавычек в потенциально уязвимый параметр, ошибка, связанная с нарушением логики SQL-запроса, не появляется, а страница отображается без изменений.

Пример:

?id=1 and 1=1

В этом случае содержимое страницы останется неизменным, потому что оба условия в операторе SQL истинные. Если изменить условие на 1 = 2, то ничего не возвращается, так как 1 не равно 2, а должны выполняться оба условия.

?id=1 and 1=2

  • Time Based SQL-injection 

Существует метод, при котором используются функции СУБД (например, SLEEP), вызывающие задержку ответа от базы данных. Такой способ также применяется для эксплуатации слепых инъекций, когда отсутствует какой-либо вывод информации, в том числе в случаях, описанных в Boolean Based SQL-injection.

Пример:

?id=1 and sleep(10)

Функция sleep() выполнится на стороне СУБД при условии обращения к записи с id=1 соответствующей таблицы. Возможно использование функций BENCHMARK или WAITFOR.

Пример:

BENCHMARK(5000000,ENCODE('MSG','by 5 seconds'))

 id=1' waitfor delay '00:00:10'  (MS-SQL) 

pg_sleep() (PostgreSQL)  
  • Stacked Query Based SQL-injections

В SQL точка с запятой указывает на конец запроса, после которого можно начать новый. Это позволяет выполнять несколько операторов в одном вызове сервера базы данных. В отличие от UNION-Based, который ограничен операторами SELECT, составные запросы могут использоваться для выполнения любой процедуры SQL. Стоит также отметить, что не все БД поддерживают эту функцию. Например, при использовании MySQL и SQLite3, нельзя воспользоваться этими операторами запросов, но в PostrgeSQL такая возможность есть.

Пример:

?id=1; delete from table_name

Автоматизированный анализ

SQLmap – мощный кроссплатформенный консольный инструмент для поиска, эксплуатации SQL-инъекций любого вида и сложности, написанный на языке Python.

Основные функции SQLmap:

  • полная поддержка системы управления базами данных (MySQL, Oracle, PostgreSQL, Microsoft SQL Server, Microsoft Access, IBM DB2, SQLite, Firebird, Sybase, SAP MaxDB и HSQLDB) и прямое подключение к ним;

  • автоматическое распознавание форматов хешей паролей и предложение их подбора с использованием атаки по словарю;

  • поддержка выполнения произвольных команд на ОС сервера БД, получение их стандартного вывода при использовании MySQL, PostgreSQL или Microsoft SQL Server и многое другое.

Основные ключи, которые используют при работе с MySQL : –dbs, -D,T,C, –level,–risk, –random-agent.

Пример:

# sqlmap -u http://example.com/?id=1 --dbs

 # sqlmap -u http://example.com/?id=1 -D test_db -T test_tables –dump

Дамп пользовательской таблицы

JSQ injection – еще один кроссплатформенный инструмент для выявления SQL-уязвимостей, написанный на языке Java и имеющий графический интерфейс. Этот инструмент поддерживает работу с 33 базами данных (Access, Altibase, Firebird, MemSQL, MySQL, Oracle, PostgreSQL, Presto, SQLite, SQL Server и т.д). По набору функций jSQL injection собрал в себе средство поиска и эксплуатации SQLi, функционал Dirb и Hashcat:

  • поддержка различных видов инъекций (стандартные, error-based, stacked-based, blind и time-based);

  • создание и внедрение веб-шелла и SQL-шелла;

  • аутентификация с использованием Basic, Digest, NTLM и Kerberos;

  • прокси-соединение по HTTP, SOCKS4 и SOCKS5;

  • кодирование и декодирование текста;

  • перебор паролей по хешу и др.

Пример:

Просмотр содержимого таблиц

SQLmap и jSQL injection поддерживают поиск уязвимостей в cookie. За счет более обширной базы пейлоадов, использование SQLmap при выявлении уязвимостей будет приносить положительный результат намного чаще.

Защита от SQL-инъекций

Встречаются SQL-инъекции в числовом и строковом параметрах в запросах, использующих оператор SELECT, которые являются самыми распространенными. Поэтому проверять нужно всё: числа, строки, даты и другие данные в специальных форматах.

1) Числа

Функция is_numeric(n) используется для проверки переменной на числовое значение, которая вернёт true, если параметр n – число, а в противном случае – false. Также переопределить тип возможно вручную.

Пример:

if (isset($_GET['id'])){ $id = $_GET['id'];

if ( is_numeric($id) == true){ … }

2) Строки

Компрометации через SQL-конструкции происходят и по причине нахождения в строках небезопасных кавычек и других специальных символов. Для предотвращения такой угрозы необходимо использовать функцию addslashes($str), которая возвращает строку $str с добавленным обратным слешем () перед каждым специальным символом. Данный процесс называется экранированием. Для этого в PHP используют функцию mysqli_real_escape_string($str).

3) Параметризированные запросы (PDO)

PDO заставляют разработчика сначала определить весь код SQL, а затем передать каждый параметр в запрос. Это позволяет БД различать код и данные независимо от того, что вводится пользователем. Подготовленные операторы гарантируют, что злоумышленник не сможет изменить логику запроса, даже, если атакующий вставляет команды SQL. Правильно параметризированный запрос не позволит базе данных обработать информацию о пользователе, как часть SQL-запроса. Для обхода аутентификации злоумышленник может использовать конструкцию:

аdmin’ or ‘1’=’1

При использовании PDO сначала передается запрос в БД, а потом в него подставляются данные из переменных. Таким образом, за имя пользователя будет приниматься вся строка admin’ or ‘1’=’1, введенная им, что не позволит хакеру проэксплуатировать SQL-инъекцию.

Пример кода:

if (isset($_GET['id'])){

$id = $_GET['id'];

if ( is_numeric($id) == true){ 

try{

$dbh = new PDO('mysql:host=localhost;dbname=sql_injection_example', 'dbuser', 'dbpasswd');

 $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 

$q = "SELECT username FROM users WHERE id = :id"; 

$sth = $dbh->prepare($q); $sth->bindParam(':id', $id); 

$sth->execute(); $sth->setFetchMode(PDO::FETCH_ASSOC); 

$result = $sth->fetchColumn(); print( htmlentities($result) ); … 

4) Хранимые процедуры

Объекты базы данных требуют, чтобы разработчик сгруппировал один или несколько операторов SQL в логическую единицу для создания плана выполнения. Последующие исполнения позволяют автоматически параметризовать операторы. То есть, это тип кода, который можно сохранить и многократно использовать в последующем. Когда необходимо выполнить запрос, вместо того, чтобы писать его, можно будет вызвать хранимую процедуру.

5) Использование WAF

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

В одной из статей мы рассматривали способы тестирования Nemesida WAF различными инструментами.

Я твой WAF payload шатал

Пару недель назад команда Vulners опубликовала сравнение нескольких популярных WAF. Поймав себя на м…

habr.com

6) Принцип наименьших привилегий

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

Резюмируя написанное выше, приведем несколько рекомендаций по защите веб-приложений от SQL-инъекций:

  • обрабатывайте ввод отдельно и формируйте запрос из безопасных значений;

  • создавайте «белые» списки;

  • регулярно устанавливайте обновления;

  • удаляйте неиспользуемый функционал;

  • задавайте конкретные значения названиям таблиц, полей и базам;

  • периодически проводите анализ защищенности веб-приложений вручную и с помощью инструментов автоматизации (например, Wapiti)

  • используйте средства защиты веб-приложений (WAF).

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

Заводишь сайт, наполняешь его контентом, запускаешь рекламную кампанию — трафик стабильно растет, пользователи активно комментируют и делятся статьями. Всё хорошо до того момента, пока в один не предвещающий беды день на сайте не оказывается ни одной статьи. Заходишь в лог запросов и видишь, что кто-то сделал DROP…

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

<form action='reg.php' method='post'>
<input type='text' name='login' placeholder='Логин' required><br>
<input type='password' name='password' placeholder='Пароль' required><br>
<input type='submit' value='Зарегистрироваться'>
</form>

Они отправляются на сервер и вставляются в запрос такого вида:

$query = «INSERT INTO userlist („.$keys.“) VALUES („.$values.“)»;

В переменной $keys содержатся ключи из супермассива $_POST, а в $values — значения:

$keys = "";
$values = "";
$first = 1;
foreach($_POST as $key => $value) {
if($first == 0) { //Добавляем запятую перед каждым пунктом, кроме первого
       $keys .= ",";
       $values .= ",";
}
$keys .= $key;
$values .= "'".$value."'";
$first = 0;
}

Это удобно, если в форме очень много полей, но из-за этого появляется уязвимость: если пользователь захочет запустить SQL-инъекцию, то он может просто создать в форме новое поле с нужным ему именем и ввести любое значение. Например, можно создать поле admin и ввести значение 1.

Если сайт плохо защищен, то хакер может совершить много попыток, но в итоге все же получить доступ к правам администратора.

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

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

$keys = "";
$values = "";
$first = 1;
$allowed = array("login", "password", "email","nickname");
foreach($_POST as $key => $array) {
$allowedKey = array_search($key,$allowed);
if($allowedKey) {
       if($first == 0) {
              $keys .= ",";
              $values .= ",";
       }
       $keys .= $allowedKey;
       $values .= "'".$value."'";
$first = 0;
}
}

Теперь в переменные $key и $values попадут только те значения, которые входят в белый список $allowed.

Передавать переменные этим способом очень опасно, потому что они оказываются на виду у пользователей. Если информация важная, то лучше используйте POST. Из ссылки злоумышленник может узнать не только имена переменных, но и какие значения должны быть в них — так он сможет выбрать оптимальную переменную для ввода инъекции.

Не советуем использовать переменные прямиком из супермассива — лучше поместить их в другую переменную, предварительно проверив данные.

Старайтесь экранировать кавычки, заменять служебные символы, удалять лишние пробелы и так далее. Давайте посмотрим, что будет, если этого не делать:

Используйте функции trim (убирает лишние пробелы), htmlspecialchars (заменяет треугольные скобки и другие спецсимволы), addslashes (экранирует кавычки и специальные символы) и другие.

В приведенной форме регистрации, например, злоумышленник мог бы использовать кавычки и запятые, чтобы добавить новый ключ и новое значение. То есть, введя в поле что-то вроде «pass’, ’something», он бы добавил какую-нибудь еще информацию, которая не проходит белый список.

Также вы можете проверять типы переменных — число, строка, файл и так далее.

Недостаточно просто обработать данные — нужно узнать, откуда они пришли. Отследить источник можно несколькими способами:

  • проверять его в $_SERVER[’HTTP_REFERER’];
  • создать скрытое поле в форме;
  • указать имя формы;
  • указать имя кнопки отправки и так далее.

Конечно, это лишь незначительный бонус к защите, но зато он отсеивает часть неопытных взломщиков, которые бросят свои потуги после нескольких попыток.

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

$db = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
$stmt = $db->prepare("SELECT * FROM articles WHERE id=':id'");
$stmt->bindParam(':id', $id);
$stmt->execute();

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

Этот способ, пожалуй, эффективнее остальных, поэтому переходите на PDO как можно скорее. Хотя его использование не означает, что другие меры безопасности уже не нужны — чем больше защитных механизмов вы установите, тем сохраннее будет информация.

Часто проверками пренебрегают самоучки, которые изучают темы «по верхушкам», — они могут решить задачу, но редко задумываются о том, чтобы устранить уязвимости. Поэтому мы предлагаем курс с системным обучением — вы научитесь создавать эффективные и надежные системы, которым не будут страшны инъекции и утечки данных.

Кроме того, чтобы обезопасить сам запрос и переменные, нужно сделать всё, чтобы хакер не смог найти уязвимости. Для этого устраните все возможные дыры, из-за которых может появиться утечка информации и вероятность несанкционированного доступа к правам администратора.

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

Поэтому лучше все подключаемые файлы переместить в отдельную директорию и запретить к ней прямой доступ. Например, можно создать папку includes и поместить туда:

  • отдельные блоки сайта;
  • библиотеки пользовательских функций;
  • файл подключения к базе данных;
  • обработчики форм и так далее.

Для этого создайте в этой папке файл .htaccess и добавьте туда такую строчку:

Прямой доступ заблокируется, но подключать эти файлы через PHP все еще будет можно.

Порой нам всем бывает нужна помощь коллег, поэтому мы выкладываем фрагменты своего кода на форумы или Stackoverflow. Делать это рекомендуется только в очень крайних случаях и лишь тогда, когда вы убедитесь, что никто не сможет вычислить ваш сайт.

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

Всегда проверяйте код, который вы копируете, — в нем может быть какой-то незаметный эксплойт, который навредит вашему сайту или позволит автору кода получить к нему доступ. Старайтесь хотя бы читать то, что вы вставляете. Даже если там всего одна строчка, ее может быть достаточно, чтобы создать брешь в вашей защите.

В идеале лучше вручную переписывать код — так вы точно заметите все подозрительные команды.

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

Отключить вывод можно в файле .htaccess, добавив туда следующие строки:

php_flag display_errors offphp_value error_reporting 0

Кроме того, уберите вывод ошибок, который вы прописали в самом коде.

Обычно для подключения к базе данных создают учетную запись со всеми правами, чтобы можно было использовать все преимущества SQL, но лучше их ограничить. Для этого зайдите в phpmyadmin, добавьте нового пользователя и выдайте ему только часть прав:

Пока ни одна галочка не отмечена — поставьте ее на пункт «Данные». Так можно будет оперировать существующими данными, но нельзя будет создать новую таблицу или удалить старую. Так вы защититесь в том числе и от атаки с помощью команды DROP, которая может удалить все статьи или комментарии с сайта.

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

Также лучше поставить пароль на root, чтобы взломщик не смог подключиться через него.

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

Да, в обновлении тоже могут быть баги, но не такие опасные. Тем более о них никто не будет знать, если после релиза прошло немного времени.

Банально, но простой пароль можно подобрать за несколько секунд. Особенно если он содержит персональные данные:

  • дату рождения;
  • кличку питомца;
  • годовщину свадьбы;
  • девичью фамилию матери и так далее.

Усугубляет положение то, что эту информацию мы часто выкладываем в свободный доступ.

Безопасных систем не существует — вы можете только снизить вероятность взлома. Для этого нужно самому быть немного хакером:

  • вводите инъекции;
  • меняйте типы данных;
  • добавляйте в поля кавычки и знаки экранирования;
  • попробуйте загрузить на сайт файл .php с вредоносным кодом и так далее.

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

Информационная безопасность, Тестирование веб-сервисов


Рекомендация: подборка платных и бесплатных курсов Smm – https://katalog-kursov.ru/

В прошлой статье мы уже рассматривали тему SQL-инъекций.

SQL-инъекции’ union select null,null,null —

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

habr.com

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

SQL-инъекция – это попытка злоумышленника изменить запрос к базе данных для ее компрометации.

Возможные SQLi:

  • Соблюдение условия WHERE к истинному результату при любых значениях параметров.

  • Объединение запросов через оператор UNION.

  • Комментирование части запроса.

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

Как выявлять SQL-инъекции

Статья носит информационный характер. Не нарушайте законодательство.

Ручной поиск

Шаг 1.

Во все параметры подставляем спецсимволы (кавычки, двойные кавычки и т.д. ). В первую очередь проверяются параметры, принимающие в качестве аргументов пользовательский ввод. Если при передаче сервер вернул ошибку, то есть подозрение на наличие инъекции. Когда веб-приложение не отреагировало на спецсимволы, то возможны 2 причины:

  • SQL-инъекции нет.

  • Отключен вывод ошибок на веб-сервере.

Пример:

http://example.com/?id=1’

http://example.com/?id=1”

http://example.com/?id=1’)

и т. д.

Шаг 2.

Если инъекция обнаружена, то используем метод UNION-Based, который применяется, если SQL-инъекция возникает в запросе с использованием оператора SELECT. Благодаря такому методу можно объединить два запроса в один набор результатов. Особенность его заключается в том, что он будет работать только в случае, если количество столбцов, возвращаемых первым запросом, будет равно их количеству, возвращаемых во втором запросе. Для определения количества столбцов можно воспользоваться 3 методами:

  1. Добавление столбца при каждой итерации проверки. Это не совсем удобно, так как их может быть бесконечное количество.

    Пример:

    ?id=1′ union select null —

    ?id=1′ union select null,null —

    ?id=1′ union select null,null,null —

и т. д.

Поиск количества столбцов

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

2. Использование оператора ORDER BY для определения количества столбцов. В данном случае нужно опираться на появление ошибки о несоответствии количества столбцов в запросе. Стоит начать с большого количества столбцов и уменьшать их вдвое при каждой итерации проверки. Если количество не будет соответствовать фактическому значению, то вернется ошибка:

?id=1′ order by 20 — ?id=1′ order by 10 — ?id=1′ order by 5 —

и т.д.

Проверка с помощью order by

3. Применение оператора GROUP BY, который основывается на обратном методе проверки, в отличие от ORDER BY.

Пример:

?id=1′ group by 5 —

?id=1′ group by 10 —

?id=1′ group by 20 —

и т. д.

Теперь, когда мы знаем, сколько столбцов имеет текущая таблица, используем UNION SELECT, чтобы увидеть, какой столбец уязвим. Уязвимым столбцом будет являться тот, данные которого отображаются на странице.

Пример:

?id=1’ union select 1,2,3,4 —

Когда уязвимый столбец найден, вместо его названия можем указать полезные команды для сбора информации о СУБД и получения данных из интересующих таблиц:

?id=1’ union select 1,null,version(),null – версия СУБД

?id=1’ union select 1,null,database(),null – текущая база данных

?id=1’ union select 1,null,@@port,null – порт, используемый СУБД

?id=1’ union select 1,null,user(),null – пользователь СУБД

?id=1’ union select 1,null,table_name,null from information_schema.tables – список таблиц с применением information_schema.

Шаг 3.

Если наличие инъекции при подстановке спецсимволов не подтвердилось, то воспользуемся одной из техник поиска слепых инъекций:

  • Boolean Based SQLi

Такой метод эксплуатации слепых SQL-инъекций, при котором информация извлекается исходя из реакции на условные выражения. Атака называется «слепой» в тех случаях, когда нет видимой реакции от веб-приложения. Например, при подстановке кавычек в потенциально уязвимый параметр, ошибка, связанная с нарушением логики SQL-запроса, не появляется, а страница отображается без изменений.

Пример:

?id=1 and 1=1

В этом случае содержимое страницы останется неизменным, потому что оба условия в операторе SQL истинные. Если изменить условие на 1 = 2, то ничего не возвращается, так как 1 не равно 2, а должны выполняться оба условия.

?id=1 and 1=2

  • Time Based SQL-injection 

Существует метод, при котором используются функции СУБД (например, SLEEP), вызывающие задержку ответа от базы данных. Такой способ также применяется для эксплуатации слепых инъекций, когда отсутствует какой-либо вывод информации, в том числе в случаях, описанных в Boolean Based SQL-injection.

Пример:

?id=1 and sleep(10)

Функция sleep() выполнится на стороне СУБД при условии обращения к записи с id=1 соответствующей таблицы. Возможно использование функций BENCHMARK или WAITFOR.

Пример:

BENCHMARK(5000000,ENCODE(‘MSG’,’by 5 seconds’));

id=1′ waitfor delay ’00:00:10′ (MS-SQL);

pg_sleep() (PostgreSQL).

  • Stacked Query Based SQL-injections

В SQL точка с запятой указывает на конец запроса, после которого можно начать новый. Это позволяет выполнять несколько операторов в одном вызове сервера базы данных. В отличие от UNION-Based, который ограничен операторами SELECT, составные запросы могут использоваться для выполнения любой процедуры SQL. Стоит также отметить, что не все БД поддерживают эту функцию. Например, при использовании MySQL и SQLite3, нельзя воспользоваться этими операторами запросов, но в PostrgeSQL такая возможность есть.

Пример:

?id=1; delete from table_name

Автоматизированный анализ

SQLmap – мощный кроссплатформенный консольный инструмент для поиска, эксплуатации SQL-инъекций любого вида и сложности, написанный на языке Python.

Основные функции SQLmap:

  • полная поддержка системы управления базами данных (MySQL, Oracle, PostgreSQL, Microsoft SQL Server, Microsoft Access, IBM DB2, SQLite, Firebird, Sybase, SAP MaxDB и HSQLDB) и прямое подключение к ним;

  • автоматическое распознавание форматов хешей паролей и предложение их подбора с использованием атаки по словарю;

  • поддержка выполнения произвольных команд на ОС сервера БД, получение их стандартного вывода при использовании MySQL, PostgreSQL или Microsoft SQL Server и многое другое.

Основные ключи, которые используют при работе с MySQL : –dbs, -D,T,C, –level,–risk, –random-agent.

Пример:

# sqlmap -u http://example.com/?id=1 –dbs

# sqlmap -u http://example.com/?id=1 -D test_db -T test_tables –dump

Дамп пользовательской таблицы

JSQ injection – еще один кроссплатформенный инструмент для выявления SQL-уязвимостей, написанный на языке Java и имеющий графический интерфейс. Этот инструмент поддерживает работу с 33 базами данных (Access, Altibase, Firebird, MemSQL, MySQL, Oracle, PostgreSQL, Presto, SQLite, SQL Server и т.д). По набору функций jSQL injection собрал в себе средство поиска и эксплуатации SQLi, функционал Dirb и Hashcat:

  • поддержка различных видов инъекций (стандартные, error-based, stacked-based, blind и time-based);

  • создание и внедрение веб-шелла и SQL-шелла;

  • аутентификация с использованием Basic, Digest, NTLM и Kerberos;

  • прокси-соединение по HTTP, SOCKS4 и SOCKS5;

  • кодирование и декодирование текста;

  • перебор паролей по хешу и др.

Пример:

Просмотр содержимого таблиц

SQLmap и jSQL injection поддерживают поиск уязвимостей в cookie. За счет более обширной базы пейлоадов, использование SQLmap при выявлении уязвимостей будет приносить положительный результат намного чаще.

Защита от SQL-инъекций

Встречаются SQL-инъекции в числовом и строковом параметрах в запросах, использующих оператор SELECT, которые являются самыми распространенными. Поэтому проверять нужно всё: числа, строки, даты и другие данные в специальных форматах.

1) Числа

Функция is_numeric(n) используется для проверки переменной на числовое значение, которая вернёт true, если параметр n – число, а в противном случае – false. Также переопределить тип возможно вручную.

Пример:

if (isset($_GET[‘id’])){ $id = $_GET[‘id’];

if ( is_numeric($id) == true){ … }

2) Строки

Компрометации через SQL-конструкции происходят и по причине нахождения в строках небезопасных кавычек и других специальных символов. Для предотвращения такой угрозы необходимо использовать функцию addslashes($str), которая возвращает строку $str с добавленным обратным слешем () перед каждым специальным символом. Данный процесс называется экранированием. Для этого в PHP используют две функции:

mysqli($str) и mysqli_real_escape_string($str).

3) Параметризированные запросы (PDO)

PDO заставляют разработчика сначала определить весь код SQL, а затем передать каждый параметр в запрос. Это позволяет БД различать код и данные независимо от того, что вводится пользователем. Подготовленные операторы гарантируют, что злоумышленник не сможет изменить логику запроса, даже, если атакующий вставляет команды SQL. Правильно параметризированный запрос не позволит базе данных обработать информацию о пользователе, как часть SQL-запроса. Для обхода аутентификации злоумышленник может использовать конструкцию:

аdmin’ or ‘1’=’1

При использовании PDO сначала передается запрос в БД, а потом в него подставляются данные из переменных. Таким образом, за имя пользователя будет приниматься вся строка admin’ or ‘1’=’1, введенная им, что не позволит хакеру проэксплуатировать SQL-инъекцию.

Пример кода:

if (isset($_GET['id'])){

$id = $_GET['id'];

if ( is_numeric($id) == true){ 

try{

$dbh = new PDO('mysql:host=localhost;dbname=sql_injection_example', 'dbuser', 'dbpasswd');

 $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 

$q = "SELECT username FROM users WHERE id = :id"; 

$sth = $dbh->prepare($q); $sth->bindParam(':id', $id); 

$sth->execute(); $sth->setFetchMode(PDO::FETCH_ASSOC); 

$result = $sth->fetchColumn(); print( htmlentities($result) ); … 

4) Хранимые процедуры

Объекты базы данных требуют, чтобы разработчик сгруппировал один или несколько операторов SQL в логическую единицу для создания плана выполнения. Последующие исполнения позволяют автоматически параметризовать операторы. То есть, это тип кода, который можно сохранить и многократно использовать в последующем. Когда необходимо выполнить запрос, вместо того, чтобы писать его, можно будет вызвать хранимую процедуру.

5) Использование WAF

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

В одной из статей мы рассматривали способы тестирования Nemesida WAF различными инструментами.

Я твой WAF payload шатал

Пару недель назад команда Vulners опубликовала сравнение нескольких популярных WAF. Поймав себя на м…

habr.com

6) Принцип наименьших привилегий

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

Резюмируя написанное выше, приведем несколько рекомендаций по защите веб-приложений от SQL-инъекций:

  • обрабатывайте ввод отдельно и формируйте запрос из безопасных значений;

  • создавайте «белые» списки;

  • регулярно устанавливайте обновления;

  • удаляйте неиспользуемый функционал;

  • задавайте конкретные значения названиям таблиц, полей и базам;

  • периодически проводите анализ защищенности веб-приложений вручную и с помощью инструментов автоматизации (например, Wapiti)

  • используйте средства защиты веб-приложений (WAF).

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

Что такое SQL-инъекция

SQL инъекция – это подстановка в SQL-запрос таких данных, которые меняют структуру этого запроса. Злоумышленник может использовать уязвимость для выполнения произвольного SQL-кода.

Представим типичную задачу – вывод статей на сайте. При переходе по адресу /index.php?id=15 должна быть отображена статья, идентификатор которой в базе данных равен числу 15.

Как начинающие разработчики обычно пишут запрос к базе данных:

$query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id'];

Разработчик ожидает, что в $_GET[‘id’] будет число и конечный запрос станет таким:

SELECT * FROM `articles` WHERE `id` = 15

Но вместо этого злоумышленник может передать строку -1 OR 1=1:

SELECT * FROM `articles` WHERE `id` = -1 OR 1=1

При запуске этого запроса будут выбраны все записи вместо одной, поскольку записей с отрицательными идентификаторами скорее всего нет в базе, а условие 1=1 всегда истинно.

Но суть в другом. После фрагмента 1=1 злоумышленник может дополнить запрос любым произвольным SQL-кодом.

Что может сделать злоумышленник?

Это зависит от конкретного запроса, а также способа его запуска.

Если запрос выполняется не через функцию mysqli_multi_query(), которая поддерживает мультизапросы (несколько запросов через точку с запятой), тогда у злоумышленника нет возможности выполнить совсем произвольный запрос вроде такого:

SELECT * FROM `articles` WHERE `id` = 1; DROP TABLE `articles`

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

Но кое-что плохое злоумышленник сделать может. Например, с помощью UNION можно получить любые данные из любых таблиц.

Представим, что у нас есть таблица articles с 4 полями: id | title | content | created_at, а также таблица users с 3 полями: id | login | password.

Поскольку UNION позволяет объединять данные из таблиц только с одинаковым количеством столбцов, злоумышленник может указать 2 необходимых ему столбца, а остальные 2 заполнить любыми значениями, например единицами:

SELECT * FROM `articles` WHERE `id` = -1 UNION SELECT 1, `login`, `password`, 1 FROM `users`

В итоге вместо title и content на страницу будут выведены login и password одного из пользователей. И это только один из десятков возможных вариантов взлома.

Экранирование кавычек

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

Возьмём такой пример:

$name = 'Вася';
$query = "UPDATE `users` SET `name` = '$name'";

С этим запросом всё в порядке, он выполнится как мы и ожидаем:

UPDATE `users` SET `name` = 'Вася'

Но что если в переменной $name будет одинарная кавычка?

$name = "Д'Артаньян";

Тогда SQL-запрос станет таким:

UPDATE `users` SET `name` = 'Д'Артаньян'

Попытка выполнить этот запрос приведёт к ошибке синтаксиса. Чтобы её не было, вторую кавычку нужно экранировать, т.е. добавить к ней обратный слеш.

Способы экранирования и их надёжность разберём чуть ниже, а сейчас для простоты возьмём addslashes():

<?php
$name = "Д'Артаньян";
$name = addslashes($name);
$query = "UPDATE `users` SET `name` = '$name'";

Что будет в итоге:

UPDATE `users` SET `name` = 'Д'Артаньян'

Готово, запрос выполнится даже при наличии кавычек.

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

А теперь важный момент. Некоторые разработчики считают, экранирования достаточно для полной защиты от SQL-инъекций.

Хорошо, ещё раз посмотрим на самый первый пример с SQL-инъекцией:

$_GET['id'] = '-1 OR 1=1';
$query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id'];
SELECT * FROM articles WHERE id = -1 OR 1=1

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

Неэффективные способы защиты от SQL-инъекций

Очевидно, самый худший вариант – не иметь никакой защиты от SQL инъекций и передавать данные, полученные от пользователя, напрямую в SQL-запрос.

$query = 'SELECT * FROM `users` WHERE `id` = ' . $_GET['id'];

Никогда так не делай! Любые данные перед подстановкой в SQL-запрос должны проходить фильтрацию и/или валидацию.

1. Функция htmlspecialchars()

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

$name = "Д'Артаньян";
$name = htmlspecialchars($name);
$query = "UPDATE `users` SET `name` = '$name'";

Это опасно! Штука в том, что функция htmlspecialchars() пропускает без экранирования опасные символы: (слеш), (nul-байт) и b (backspace).

Вот полный пример кода, демонстрирующего уязвимость:

$mysqli = new mysqli('localhost', 'root', 'password', 'database');

$login = '\';
$password = ' OR 1=1 #';

$login = htmlspecialchars($login, ENT_QUOTES, 'UTF-8');
$password = htmlspecialchars($password, ENT_QUOTES, 'UTF-8');

$sql = "SELECT * FROM `users` WHERE `login` = '$login' AND `password` = '$password'";

$items = $mysqli->query($sql) or die($mysqli->error);

while($item = $items->fetch_assoc())
{
	var_dump($item);
	echo '<br>';
}

В итоге SQL-запрос будет таким:

SELECT * FROM `users` WHERE `login` = '' AND `password` = ' OR 1=1 #'

С помощью / экранируется кавычка, идущая сразу после $login. `login` = ‘$login’ по факту превращается в `login` = ” AND `password` = ‘. После этого любой код, который мы напишем, будет выполнен, в нашем случае это просто OR 1=1. В конце добавляем # (комментарий), чтобы скрыть последнюю кавычку.

2. Фильтрация по чёрному списку символов

По каким-то непонятным мне причинам ещё существуют разработчики, использующие чёрные списки символов:

$disallow = ['~', ''', '"', '<', '>', '.', '%'];
$name = 'Вася';

$name = str_replace($disallow, '', $name);

$query = "SELECT * FROM `users` WHERE `name` = '$name'";

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

Я не хочу сказать, что этот подход не будет работать, но его применение под большим вопросом:

  • Зачем вообще составлять какие-то списки, если есть более простые и надёжные способы защиты?
  • Нужно знать все потенциально опасные символы.
  • Что делать если нужно разрешить пользователям использовать какие-либо символы из списка?

Кроме этого, я считаю фильтрацию в SQL-запросах плохой идеей. Если в строке есть недопустимые символы – лучше сообщить о них пользователю и попросить исправить, а не просто обрезать часть контента.

К примеру, пользователь хочет использовать логин ~!Mega_!Pihar!_!9000!~, а после регистрации оказывается, что его ник превратился в MegaPihar9000.

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

3. Функция stripslashes()

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

Раньше в PHP была такая штука как волшебные кавычки (Документация). Если эта директива была включена, то все данные, содержащиеся в $_GET, $_POST и $_COOKIE автоматически экранировались.

Сделано это было для защиты новичков, которые подставляли данные напрямую в SQL-запросы. На практике это было не самое удачное решение:

  • Не очень удобно, когда все данные по-умолчанию экранируются, ведь зачастую они нужны в исходном виде.
  • В идеале экранирование должно учитывать кодировку соединения с базой данных, о чём мы поговорим чуть позже. Из-за этого разработчикам приходилось убирать экранирование функцией stripslashes() и затем опять экранировать данные более подходящими функциями, в случае MySQL это была mysql_real_escape_string().

Вот почему функцию stripslashes() можно встретить в старых учебниках. Чтобы отменить экранирование символов и получить исходную строку.

Начиная с PHP 5.4 функционал волшебных кавычек удалён, поэтому использовать stripslashes() перед записью в базу нет никакого смысла.

4. Функция addslashes()

В некоторых книгах ещё можно встретить рекомендации экранировать данные функцией addslashes().

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

Поэтому даже в документации прямо написано, что эту функцию не нужно использовать для защиты от SQL инъекций.

Вырезка из документации по поводу уязвимости функции addslashes()

Вырезка из документации про функцию addslashes()

Эффективные способы защиты

1. Функция mysql(i)_real_escape_string

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

Есть две важные детали, которые вы должны знать, когда используете эту функцию.

Первая – вы всегда должны подставлять экранированные данные в кавычки. Если этого не делать, толку от экранирования не будет:

// Неправильно, сначала надо экранировать!
$query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id'];

// Экранируем
$id = $mysqli->real_escape_string($_GET['id']);

// Тоже неправильно, нет кавычек
$query = 'SELECT * FROM `articles` WHERE `id` = ' . $id;

// Правильно
$query = "SELECT * FROM `articles` WHERE `id` = '$id'";

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

Почитать о проблеме можно тут (блог разработчика, обнаружившего ошибку), здесь и подробней с примерами там.

2. Приведение к числу

Простой и эффективный способ защиты для числовых полей – приведение данных к числу. Пример:

$_POST['id'] = '15';

$id = (int) $_POST['id'];
// Или так:
$id = intval($_POST['id']);
// Или для дробных чисел:
$id = (float) $_POST['id'];

$query = 'SELECT * FROM `users` WHERE `name` = ' . $id;

Кавычки здесь не обязательны, поскольку в запрос в любом случае подставится число.

Есть один нюанс. Как я писал выше, мне не очень нравится идея фильтрации данных и здесь она может выйти боком с точки зрения SEO.

Допустим, есть интернет-магазин, где URL адреса страниц товаров выглядят как /product/15, где 15 – идентификатор товара.

Если алгоритм поиска статьи заключается в том, что мы берём вторую часть URL и приводим её к числу, вроде такого:

$segments[2] = '15';
$id = (int) $segments[2];

Тогда можно писать какие угодно символы после числа 15 (только один следующий символ должен быть не цифровым), например /product/15abcde13824_ahaha_lol и система всё равно будет отображать статью c id = 15.

3. Подготовленные запросы

Один из лучших способов защиты от SQL инъекций. Суть в том, что SQL запрос сначала “подготавливается”, а затем в него отдельно передаются данные.

$stmt = $db->prepare('SELECT * FROM `users` WHERE `name` LIKE ?');
$stmt->execute([$_GET['name']]);

Такой подход гарантирует отсутствие SQL-инъекций в момент подстановки данных, поскольку запрос уже “подготовлен” и не может быть изменён.

Но, как обычно, всё портят детали.

Первая деталь. Чуть выше я указывал ссылку на обсуждение уязвимости mysql_real_escape_string.

Если ты героически прочитал его до конца (нет), там есть интересное утверждение – что PDO с подготовленными запросами также может иметь уязвимость, связанную с кодировками.

Чтобы её избежать, нужно либо отключить эмуляцию подготовленных запросов, либо использовать только надёжные кодировки (например UTF-8), либо обязательно указывать кодировку соединения (через $mysqli->set_charset($charset) или DSN для PDO, но не через SQL-запрос SET NAMES).

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

$stmt = $db->prepare("SELECT * FROM `users` WHERE `name` = '$_POST[name]'");
$stmt->execute();

Тогда его не спасут никакие подготовленные запросы.

И третья деталь. В подготовленные запросы нельзя подставлять названия столбцов и таблиц.

// Так делать нельзя
$stmt = $pdo->prepare('SELECT ? FROM ?);

Прекрасно. И что теперь делать?

Один из распространённых вариантов – белые списки. Простой пример:

$_POST['product'] = [
	'title' => 'Название товара',
	'article' => 'Артикул товара',
	'content' => 'Описание товара'
];

$allowed = ['title', 'article', 'content'];

foreach($_POST['product'] as $k => $v)
{
	if(!in_array($k, $allowed, true))
		die('Некорректное поле: ' . $k);
}

Если полей много и не хочется всех их вбивать ручками – можно просто достать их всех из базы (SHOW COLUMNS FROM `products`).

Другой логичный вариант – валидировать названия столбцов, разрешая, к примеру, только буквы, цифры и подчёркивания.

В общем, опять надо что-то вручную допиливать, придумывать собственные функции генерации запросов. Не комильфо. Рекомендую поступить иначе.

4. Готовые библиотеки

Разработчики популярных библиотек наверняка гораздо умней и опытней нас. Они давно всё продумали и протестировали на десятках тысяч программистов. Так почему нет?

Для простых проектов вполне хватит Medoo или RedBeanPHP, для средних рекомендую (и всегда использую) Eloquent, ну а для крупных проектов лучше всего подойдёт мощная и суровая Doctrine.

In a previous post on Fixing Command Injection Vulnerabilities
you saw the damage that can be caused when an attacker gets access to your system. It’s basically Game Over!

Nelson from the simpsons laughing at a Game Over screen

The same is true of SQL Injection also known as SQLi. The dangers of SQL Injection have been talked about for
a long time, but for many developers they’ve never seen it in practice. This post is going to explore what a
SQL Injection is, why you need to fix it, and how to fix it!

What is a SQL Injection Vulnerability?

SQL Injection falls into the Injection category of vulnerabilities detailed in the OWASP Top 10.
SQL Injection is easy to exploit, occurs commonly, and the impact is severe. As a
professional software developer it is your job to recognize and fix these vulnerabilities!

This is what SQL Injection looks like:

1
User.where("email = #{payload}").first

That’s all that’s required for an attacker to gain access to your entire database. Don’t believe me? Let’s see
how an attacker could own your database.

Basic Exploitation

Since an attacker has full control of payload (for example sake let’s say via params[:email])
they can insert whatever they’d like into your where query. Here’s an example:

1
2
3
4
5
6
7
# http://domain.com/query?email=') or 1=1--
payload = "') or 1=1--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "a@a.com", name: "A", admin: false, created_at: "2015-10-02 13:14:38", updated_at: "2015-10-02 13:14:38">

Above the attacker is sending a payload of ') or 1=1--. It works like this:

  1. The first part of the payload ') sets the query to return zero results; email is blank: email=''.
  2. The second part, 1=1 always equals true, which results in the first entry in the users table being returned.
  3. The final part, -- is a SQL comment. This is a technique to cancel out any further query modifications that
    could occur server side. Essentially, this reduces the fine tuning to make a payload work.

Simplified, most SQL Injections will follow this type of payload format:

  1. Close the query
  2. Insert the attack
  3. Prevent server modifications

While this seems trivial, an attacker can now manipulate payloads to get access to juicier
information. Let’s see another example:

1
2
3
4
5
6
7
# http://domain.com/query?email=') or admin='t'--
payload = "') or admin='t'--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 193, email: "admin1@email.com", name: "Admin1", admin: true, created_at: "2015-09-28 01:33:39", updated_at: "2015-09-28 01:58:35">

Using the payload ') or admin='t'-- the attacker has gotten the system to return an admin user. They now
have knowledge about an admin in your database.

Enumerating

In order to get a full dump of admin accounts the attacker needs to be able to enumerate through your admin
table. It turns out that this is trivial to accomplish using an id filter:

1
2
3
4
5
6
7
# http://domain.com/query?email=') or admin='t' and id > 193--
payload = "') or admin='t' and id > 193--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 291, email: "admin2@email.com", name: "Admin2", admin: true, created_at: "2015-09-28 01:33:39", updated_at: "2015-09-28 01:58:35">

Here the attacker adds and id > 193 to get the next admin user. At this point, they keep incrementing
id until they dump every admin out of your database.

In the back of your mind maybe you’re thinking:

“My user table gets owned, but I encrypt my passwords so at least the damage is just limited to a single
table. Big deal if someone gets access to all my user’s … that’s not too bad … right?”

Now you’re smart so I’m sure you didn’t say that to yourself. Because this is bad. And an attacker can do
worse!

Discovering Other Tables

How can an attacker find out what other tables exist in the application? Via the sqlite_master table.
This table lists the entire database’s schema including tables and indexes.

In order to access this information a couple of new techniques will be required. Let’s see
the payload first and then look at the techniques:

1
2
3
4
5
6
7
# http://domain.com/query?email=') union select 1,name,1,1,1,1 from sqlite_master--
payload = "') union select 1,name,1,1,1,1 from sqlite_master--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "schema_migrations", name: "1", admin: true, created_at: 1, updated_at: 1>

The first new technique is the addition of the union
operator. This is a SQL operator (not limited to sqlite3) that combines the result of two select statements.

This payload also introduces a new technique of querying a system table:

1
select 1,name,1,1,1,1 from sqlite_master--

What’s happening here is that the attacker is selecting the name column from the sqlite_master table, and then inserting
1’s to fill out the remaining columns. Without those 1’s the database would throw an exception:

1
2
3
SELECTs to the left and right of UNION do not have the same number of result columns:
SELECT "users".* FROM "users" WHERE (email = '') union select
name, 1, 1, 1, 1 FROM sqlite_master--') ORDER BY "users"."id" ASC LIMIT 1

The end query that gets sent to the database looks like this:

1
2
3
SELECT "users".* FROM "users" WHERE (email = '')
  UNION
SELECT 1,name,1,1,1,1 FROM sqlite_master--')  ORDER BY "users"."id" ASC LIMIT 1

Remember that the first query to users doesn’t return a result so the result of the second query is interpreted
as a User and fills a User object with the sqlite_master information. Specifically, the payload is crafted
so that the name field corresponds with the email field in User.

In this particular example the result was email: "schema_migration" which isn’t helpful. Of course an attacker
could use the enumeratation technique from eariler to traverse the entires in the sqlite_master table, but
that’s slow. Instead the payload can be modified to use a function and get all the tables in the database at once!

1
2
3
4
5
6
7
# http://domain.com/query?email=') union select 1,group_concat(name, ','),1,1,1,1 from sqlite_master--
payload = "') union select 1,group_concat(name, ','),1,1,1,1 from sqlite_master--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "users,credit_cards,schema_migrations,unique_schema_migrations,sqlite_sequence", name: "1", admin: true, created_at: 1, updated_at: 1>

Above the payload is using the group_concat function provided by sqlite3
to pull together all of the tables into a single value: users,credit_cards,schema_migrations,unique_schema_migrations,sqlite_sequence
And viola, the attacker now has knowledge of every table in your database, including the credit_cards table!

Accessing Other Tables

Now that the attacker has discovered the credit_cards table in the application, they’re going to pull as much
out of it as they can. Using the same union technique from above:

1
2
3
4
5
6
7
# http://domain.com/query?email=') union select 1,number, 1, 1, 1, 1 FROM credit_cards--
payload = "') union select 1,number,1,1,1,1 FROM credit_cards--"

@user = User.where("email = #{payload}").first
render @user

#=> #<User id: 1, email: "4242 4242 4242 4242", name: "1", admin: true, created_at: 1, updated_at: 1>

The output of User ought to scare you! An attacker has managed to populate the email field with a credit
card number.

For our attacker this is where the party really starts. They have a toe hold into your system, and it’s a matter of
time and a simple script to dump all your database.

allthethings

How to fix SQL Injection Vulnerabilities

By now it should be crystal clear why you must fix SQL Injection vulnerabilities. In order to fix your SQL
queries you’ll need to use parameterization. Parameterization, in a nutshell, is the safest way to
handle unsafe user input. And whether you’re using ActiveRecord, Sequel, ROM, or some other ORM they’re all
going to have facilities for parameterizing queries.

Let’s look at some common unsafe queries that frequently occur and how to fix them (these examples are
ActiveRecord based.)

Single Parameter Queries

The most common use case for Ruby queries is a single parameter.

1
2
3
4
5
6
7
8
# Unsafe
User.where("email = '#{email}'")
User.where("email = '%{email}'" % { email: email })

# Safe
User.where(email: email)
User.where("email = ?", email)
User.where("email = :email", email: email)

While line 3 above looks very similar to line 8, they are different in that line 3 uses
string formatting instead of parameterization
which is unsafe for protecting against SQL injection.

Looking at the Unsafe vs Safe examples above you can extrapolate a rule of thumb: If you have to add surrounding
quotes to your query, you’re vulnerable to SQL Injection
.

Compounding Queries

Sometimes you need to chain together a series of queries, usually that’s with an AND statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Unsafe
def unsafe_query
  sql = []
  sql << "email = #{email}" if condition1?
  sql << "name = #{name}"   if condition2?
  # ... etc

  User.where(sql.join(' and '))
end

# Safe
def safe_query
  User.all.tap do |query|
    query.where(email: email) if condition1?
    query.where(name: name)   if condition2?
    # ... etc
  end
end

ActiveRecord is great because it allows you to easily chain together multiple pieces of a query and because they’re
evaluated lazily.

One of the real tricky places I’ve seen people struggle with is OR statements. This is
in the process of changing
but right now the common pattern is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Unsafe
def unsafe_query
  sql = []
  sql << "email = #{email}" if condition1?
  sql << "name = #{name}"   if condition2?
  # ... etc

  User.where(sql.join(' OR '))
end

# Safe
def safe_query
  sql   = []
  param = []

  if condition1?
    sql << "email = ?"
    param << email
  end

  if condition2?
    sql << "name = ?"
    param << name
  end

  User.where(sql.join(' OR '), *param)
end

While not very pretty, notice that the parameters are passed in separately from the query. This way
parameterization can still occur keeping you safe from SQL Injection.
There are ways to pretty this up which I’d encourage you to
use if this type of code is in your code base.

LIKE Query

Another common scenario is doing a starts with/ends with filter
using LIKE. This query is more apt to introduce SQL injection because many people don’t understand how
it works!

1
2
3
4
5
# Unsafe
User.where("email LIKE '%#{partial_email}%'")

# Safe
User.where("email LIKE ?", "%#{partial_email}%")

Notice that with both queries, you’re going to have to do some string interpolation to insert the % signs.
You’ll want to make sure that this occurs inside the value that will be parameterized.

Raw Queries

The final common scenario is raw queries. These are queries where you need to get right into the SQL itself
without using ActiveRecord or any other type of framework.

1
2
3
4
5
6
7
8
9
10
11
# Unsafe
st = ActiveRecord::Base.connection.raw_connection.prepare(
  "select * from users where email = '#{email}'")
results = st.execute
st.close

# Safe
st = ActiveRecord::Base.connection.raw_connection.prepare(
  "select * from users where email = ?")
results = st.execute(email)
st.close

The above query is too simple for a raw query, you’d normally be doing a complex query, but at least now
you can see proper parameterization. Raw queries follow in the same footsteps as previous examples of query
parameterization.

That wraps up this post on SQL Injection. I hope that you learned something new. If there’s a Ruby or Rails
security topic that you’d like me to touch on send me a tweet or an
email.

Добавить комментарий