SQL Injection howto
Не секрет, что баги в скриптах всегда были ахиллесовой пятой многих веб-ресурсов. Просканишь хост, посмотришь, какие сервисы и каких версий там крутятся, и опускаешь руки - просто неоткуда подступиться. Все лишнее закрыто фаерволом, а то, что открыто - пропатчено до последних версий. Засекурено по максимуму. И тут исход взлома решает какая-то байда в скрипте, заставляющая прыгать от радости.
Итак, сегодня на повестке дня - техника взлома скриптов, почти по-наркомански называемая SQL injection. Для понимания всех прелестей этой фишки неплохо бы немного знать, что такое SQL, или, по-нашему, скуль. Об этом очень кратко читай в следующих абзацах.
Ликбез по SQL
Язык SQL (Structured Query Language) на сегодняшний день является стандартом де-факто практически для всех распространенных серьезных СУБД. Он предназначен для составления запросов к базе данных. Запросы могут иметь цель добавить некоторые данные в базу, их модификацию, поиск и т.д. Работает это следующим образом. На языке SQL составляется некоторое выражение (запрос). Затем оно передается серверу базы данных, который, в свою очередь, обрабатывает его и возвращает результат этой обработки программе, сделавшей запрос.
Язык SQL достаточно удобен и очень прост для изучения, благодаря чему он получил такое широкое распространение - большая часть мало-мальски серьезных cgi-скриптов работают с базами данных, используя SQL.
Любая база данных - это набор таблиц, предназначенных для хранения однотипной информации. Каждая таблица имеет имя и состоит из записей. Запись - это своего рода единица информации, хранимая в базе данных. Информация в записи поделена на поля. Вот пример таблицы:
cc_type, cc_number, cc_holder - названия полей.
Вторая строка - это запись.
Visa, 123, Agent Smith - значения полей записи.
При помощи языка SQL можно производить различные операции с базой данных, таблицами и записями. Основные команды языка:
SELECT - извлечение инфы из таблицы.
INSERT - добавление записи в таблицу.
UPDATE - изменение записи.
DELETE - удаление записи.
Простейшее SQL-выражение может выглядеть так:
SELECT cc_number, cc_holder FROM cc_table WHERE cc_type='visa'
Ключевое слово FROM указывает таблицу, к которой будет применяться это выражение. После необязательного WHERE идет условие, определяющее, по каким параметрам будут отфильтровываться записи таблицы. Результатом данного запроса станет извлечение из таблицы cc_table записей, в которых поле cc_type имеет значение 'visa'. Программе, передавшей данный запрос базе данных, будут возращены не полные записи, а только поля cc_number и cc_holder.
После WHERE может содержаться несколько условий, разделенных логическими операторами:
WHERE cc_type='visa' AND cc_number=1234 OR cc_number=4321
Как было сказано, добавление записей производится командой INSERT. Вот пример запроса:
INSERT INTO cc_table VALUES ('amex', 12345, 'Neo')
Результатом обработки запроса станет добавление в таблицу cc_table новой записи, в которой поле cc_type примет значение 'amex', cc_number - 12345, cc_holder - 'Neo'.
Вот пример с UPDATE:
UPDATE cc_table SET cc_number=12345 WHERE cc_holder='Agent Smith'
Здесь произойдет просмотр таблицы cc_table. Если в соответствующих записях значение поля cc_holder будет равно 'Agent Smith', то значение cc_number сменится на 12345.
А это запрос DELETE:
DELETE FROM cc_table WHERE cc_type='visa'
Удалятся все записи из таблицы, где поле cc_type равно значению 'visa'.
На этом небольшой ликбез по SQL закончился. Теперь можно приступить к самому вкусному. Тому, ради чего поднялся весь сыр-бор: SQL injection.
SQL injection
Суть ошибок класса SQL injection состоит в том, что из-за некорректной обработки данных, передаваемых скрипту, потенциальный хакер может изменить составляемый скриптом SQL-запрос со всеми вытекающими отсюда последствиями. К примеру, в случае с инет-магазином, он может таким образом изменить запрос, что SQL-сервер после его обработки выдаст все содержимое таблицы, в которой содержится инфа о предыдущих клиентах магазина, включая номера их кредитных карточек и т.д. и т.п.
Это может выглядеть следующим образом. Предположим, что в составе интернет-магазина присутствует скрипт, принимающий от юзера логин и пароль и выдающий инфу о его предыдущих покупках, номерах карточек и т.д. Логин и пароль загоняются скриптом в следующее SQL-выражение, которое передается серверу базы данных:
SELECT * FROM clients WHERE login='$login' AND password='$password'
В результате обработки данного выражения сервер возвращает скрипту все записи в таблице, соответствующие логину юзверя.
Данные, введенные юзверем ($login и $password), берутся из web-формы и напрямую подставляются в это SQL-выражение. Отсюда появляется возможность хитрым образом так задать логин и пароль, что логика SQL-выражения немного изменится, в результате чего сервер базы данных возвратит записи из таблицы clients, соответствующие всем клиентам магазина со всеми данными о них. К примеру, это можно сделать так: в качестве логина задать такую строку: "nobody' OR ''='", а в качестве пароля "nopassword' OR ''='". С использованием этих данных скрипт сформирует такое выражение:
SELECT * FROM clients WHERE login='nobody' OR ''='' AND password='nopassword' OR ''=''
И под это SQL-выражение будут попадать все записи таблицы clients, т.к. login='nobody' OR ''='' и password='nopassword' OR ''='' всегда будут истинными. Так хакеры и ломают многие базы данных.
Собственно говоря, мы сделали то, что принято называть SQL injection. Дальше же будут приведены некоторые особо часто используемые трюки, которые проворачивают, если есть хоть какая-то возможность повлиять на SQL-выражение.
UNION
В предыдущем примере мы предположили, что вся инфа о юзверях содержится в таблице clients. Немного изменим условия. Допустим, в таблице clients находятся просто записи о клиентах - имена, фамилии и т.д. Номера же кредитных карточек располагаются в другой таблице, допустим, cards. Соответственно, теперь стоит задача вытянуть инфу из этой таблицы, смодифицировав уже указанный выше запрос:
SELECT * FROM clients WHERE login='$login' AND password='$password'
Здесь на помощь приходит такой элемент языка SQL, как UNION. UNION обычно используется в случае необходимости объединить результаты обработки двух запросов в один:
SELECT smth FROM table1 UNION SELECT smth FROM table2
Заюзав UNION, можно добавить еще один SELECT, который будет извлекать инфу из таблицы cards. Для этого в качестве пароля в форме может быть задано что-то вроде этого:
' UNION SELECT * FROM cards WHERE ''='
В итоге получается:
SELECT * FROM clients WHERE login='nologin' AND password='nopassword' UNION SELECT * FROM cards WHERE ''=''
После приема такого запроса произойдет вывод всего содержимого таблицы cards.
При использовании UNION необходимо учитывать следующий момент. Оба SELECT'а должны выдавать одинаковое количество столбцов, иначе произойдет глюк. Грубо говоря, если в таблице clients - всего 4 столбца, а в cards - 5, вышеуказанный пример работать не будет. Чтобы он все-таки заработал, надо задать не "SELECT *", а, например, "SELECT type, number, holder, address".
Использование разделителей SQL-выражений
Если sql-сервер позволяет задавать в одном запросе несколько SQL-выражений, разделенных некоторым символом, то это открывает перед потенциальным атакующим более широкие возможности. Следует отметить, что фичи такого рода поддерживаются далеко не всеми серверами баз данных. К примеру, Microsoft SQL Server позволяет это делать, интерпретируя в качестве разделителя символ ';', а MySQL - нет.
Опять небольшой пример с использованием того же SQL-выражения, что и выше. Допустим, веб-магазин использует базу данных, крутящуюся на скульном сервере от Дяди Билли. Допустим также, что атакующему надо почистить логи, которые скрипт сохраняет в таблицу logs, содержащую поле с именем remote_ip. Ему надо удалить все записи, содержащие в поле remote_ip его IP-адрес. Удаление записей производится при помощи SQL-команды DELETE. Соответственно, удаление логов может выполняться командой:
DELETE FROM logs WHERE remote_ip='IP address'
Без использования разделителей совместить оригинальное выражение
SELECT * FROM clients WHERE login='$login' AND password='$password'
с DELETE затруднительно. А с их использованием все делается на раз-два. Достаточно в качестве пароля забить в форму, например, "nopassword'; DELETE FROM logs WHERE remote_ip='10.0.0.2", и все становится на свои места:
SELECT * FROM clients WHERE login='nologin' AND password='nopassword'; DELETE FROM logs WHERE remote_ip='10.0.0.2'
Теперь логи почищены.
Естественно, что помимо DELETE после разделителя можно забить ЛЮБУЮ SQL-конструкцию, поддерживаемую сервером: SELECT, INSERT, UPDATE и т.д.
Вывод в файл
Некоторые SQL-сервера позволяют выводить результаты обработки SQL-выражений во внешний файл. Когда это может быть полезным? Самое первое, что может прийти на ум хакеру - это собрать какой-нить скрипт, облегчающий дальнейшее юзание сервера. К примеру, php-шелл.
В сервере MySQL вывод в файл происходит посредством команды INTO OUTFILE. В простейшем случае это делается так:
INSERT '<? system($cmd) ?>' INTO OUTFILE /www/inetshop/htdocs/shell.php
Если наш вебшоп с багой "крутится" на MySQL, то втупую проинжектить select этим запросом не получится - MySQL, к сожалению, не поддерживает разделителей.
Ситуацию спасает то, что "INTO OUTFILE" может использоваться вместе с SELECT'ом, выдавая результат обработки запроса (т.е. содержимое таблицы) не скрипту, сделавшему запрос, а напрямую в файл. Но тут возникает еще одна проблема. Нужная строка, которую надо записать в файл, должна уже присутствовать в базе. Если в магазине присутствует скрипт регистрации, то что мешает зарегистрироваться и в качестве имени юзера, пароля или еще чего-нибудь задать нужную строку? После регистрации надо сделать так, чтобы оригинальный запрос принял, скажем, такую форму:
SELECT * FROM clients WHERE login='<? system($cmd) ?>' AND password='our_password' INTO OUTFILE '/www/inetshop/htdocs/shell.php'
Как нетрудно догадаться, для успешного "инжектирования" логин должен быть - "<? system($cmd) ?>", а пароль - "ourpassword' INTO OUTFILE '/www/inetshop/htdocs/filename".
В MS SQL вывод в файл происходит несколько иначе. В поставке с ним идет большое количество модулей, содержащих различные процедуры, которые можно вызывать непосредственно из SQL-выражения. Одна из них - master.dbo.sp_makewebtask - как раз предназначена для вывода результатов выполнения скульных выражений в файл:
EXEC sp_makewebtask 'c:\inetpub\wwwroot\shell.php', "скульное выражение"
Ключевое слово EXEC предназначено для выполнения внешних процедур, какой и является sp_makewebtask. Используя разделители, эту строчку можно вогнать в поле веб-формы, предназначенное для пароля, и получить на выходе отличный скриптец.
Исполнение команд шелла
Иногда можно избежать процесса сборки скриптов, т.е. не использовать средства вывода в файл, а сделать выполнение команд шелла прямо из SQL-выражения. Эта фича, естественно, не входит в спецификацию языка SQL, поэтому, как и вывод в файл, ее наличие или отсутствие полностью лежит на совести разработчика сервера.
Например, исполнение шелл-команд возможно в MS SQL Server путем использования процедуры master.dbo.xp_cmdshell. Юзается это довольно просто. Вот пример SQL-выражения:
EXEC master.dbo.xp_cmdshell 'cmd.exe dir'
Вкупе с тем, что MS SQL поддерживает разделители (';'), вызов внешних процедур при проведении атаки проходит на ура. С тем самым магазином атакующему достаточно в качестве пароля ввести всего лишь "' EXEC master.dbo.xp_cmdshell 'cmd.exe dir".
Some tricks
Во многих приведенных выше примерах предполагалось, что потенциальный атакующий знает названия таблиц и полей в них. В реальной жизни это не всегда так - зачастую приходится иметь дело с самописными скриптами, исходники которых попросту недоступны. Но, как говорится, на каждую хитрую...
В подавляющем большинстве серверов инфа обо всех базах, поддерживаемых сервером, хранится в системных таблицах, имена которых имеют дефолтовые названия. То же самое относится и к названиям полей в них. В этих таблицах можно найти исчерпывающую информацию, касающуюся структуры любой базы данных на сервере. Далее приведена небольшая подборка названий таблиц для разных серверов. Для получения подробной информации о содержимом этих таблиц было бы неплохо глянуть в документацию по конкретному серверу, хотя до смысла большинства полей можно логически дойти, просто заселектив все их содержимое:
1) MS SQL
sysobjects
syscolumns
2) MySQL
mysql.user
mysql.host
mysql.db
3) Oracle
SYS.USER_OBJECTS
SYS.USER_TABLES
SYS.USER_VIEWS
SYS.USER_TAB_COLUMNS
SYS.TAB
SYS.ALL_TABLES
Защита от подобного
Чтобы защитить свои скрипты от подобной напасти, необходимо тщательно проверять всю входящую информацию. Нельзя доверять пользователям. Минимум, что надо делать, это удалять следующие символы:
1) Кавычки.
И двойные, и одинарные. С их помощью можно добавлять левые параметры в запрос.
2) Точка с запятой.
Она разделяет запросы (не во всех sql-серверах; mysql, например, этого не поддерживает). Ее наличие может привести к добавлению левых команд в SQL-запрос.
Также стоит проверять и другие символы, например, подчеркивание (_), знак процента (%), звездочка (*). Все они могут привести к нежелательным последствиям, поэтому очень важно отфильтровывать лишние данные.
Заключение
Нельзя сказать, что тема атак класса SQL Injection полностью рассмотрена. Каждая реализация SQL-сервера имеет свои особенности, которые потенциальный взломщик может использовать себе на благо. Естественно, для этого надо хорошо разбираться в том или ином диалекте языка SQL. Но самых популярных серверов на сегодняшний день всего несколько: MySQL, Postgres, MS SQL, Oracle. Для их защиты, особенно в случае с MySQL и Postgres, не требуется больших усилий. Главное грамотно проверять входящие данные. Это как минимум спасет твой сайт от утечки данных через скрипты. Так что помни об этом. Удачи!