Эдам Тьюлипер
Почти каждый день в СМИ сообщают об очередном взломанном сайте. Эти постоянные нападения знаменитых групп хакеров оставляют разработчиков в недоумении, неужто хакеры пользуются более продвинутыми технологиями для своих гнусных делишек. Хотя некоторые современные атаки могут быть весьма сложными с технологической точки зрения, зачастую самые эффективные поразительно просты - и известны очень давно. К счастью, от этих атак обычно на удивление легко защититься.
Я собираюсь рассмотреть в этой и следующей статье некоторые из наиболее распространенных типов хакерских атак. В первой статье будут рассмотрены встраивание SQL-кода (SQL injection) и модификация параметров (parameter tampering), а во второй - кросс-сайтовые скрипты (cross-site scripting) и подделка кросс-сайтовых запросов (cross-site request forgery).
На случай, если вас интересует, только ли крупные сайты должны быть обеспокоены защитой от взлома, дам простой ответ: все разработчики должны думать о защите своих приложений от попыток взлома. Это наша работа защищать свои приложения; этого ждут от нас пользователи. Даже небольшие приложения в Интернете не гарантированы от прощупывания на прочность из-за наличия огромного количества автоматических программ взлома. Допустим, у вас украли вашу таблицу Users или Customers, а пароли для этих таблиц также используются в других приложениях. Конечно, рекомендуется, чтобы пользователи всегда применяли разные пароли, но реальность такова, что этого не происходит. Кому понравится вынужденное оповещение пользователей, что его приложение стало точкой доступа для кражи информации? Даже крошечный блог одного из моих приятелей, где рассказывалось о его восхождении на Эверест, был взломан и полностью удален безо всяких на то причин. Так что без защиты практически все приложения уязвимы.
Если только ваша сеть физически не отключена ото всех устройств внешних коммуникаций, всегда существует потенциальная возможность того, что кто-то вломится в вашу сеть из-за проблем в конфигурации прокси-сервера, предпримет атаки с использованием Remote Desktop Protocol (RDP) или Virtual Private Network (VPN), воспользуется уязвимостью к выполнению удаленного кода внутренним пользователем, который просто зайдет на некую веб-страницу, или ошибками в правилах брандмауэра, попытается подобрать пароли, проникнет через Wi-Fi (большинство систем защиты Wi-Fi можно взломать, сидя в машине на ближайшей к вам парковке), задействует методики социальной психологии (при этом люди добровольно выдают конфиденциальную информацию) или нащупают другие точки входа. Так что никакую среду нельзя считать безопасной, если она полностью не отключена от внешнего мира.
Теперь, когда я запугал вас (надеюсь) тем, что угроза взлома очень реальна и что все ваши приложения могут быть прощупаны на прочность, давайте разберемся в этих способах взлома и рассмотрим, как их предотвратить!
Встраивание SQL-кода
Чтоэтотакое? Встраивание SQL-кода - это атака, при которой в запрос вставляется одна или более команд с целью формирования нового запроса, не предусмотренного разработчиком. Это делается почти всегда, когда используется динамический SQL-код, т. е. когда вы сцепляете строки в своем коде для формирования SQL-выражений. Встраивание SQL-кода возможно в вашем коде под Microsoft .NET Framework, если вы формируете запрос или вызов процедуры, а также может происходить в вашем коде T-SQL на серверной стороне, как, например, в случае динамического SQL в хранимых процедурах.
Встраивание SQL-кода особенно опасно, потому что может использоваться не только для запроса и редактирования данных, но и для выполнения команд базы данных, ограниченных лишь правами доступа пользователя базы данных или сервисной учетной записи для нее. Если ваш SQL Server сконфигурирован на выполнение под учетной записью администратора и пользователь вашего приложения получает роль sysadmin, вам особенно есть, о чем беспокоиться. Атаки со встраиванием SQL-кода могут выполнять системные команды и достигать следующих целей:
- установить лазейки (backdoors);
- передать всю базу данных через порт 80;
- установить сетевые анализаторы для кражи паролей и других конфиденциальных данных;
- взломать пароли;
- перечислить все хосты в вашей внутренней сети и просканировать порты других компьютеров;
- скачать файлы;
- запустить программы;
- удалить файлы;
- сделать компьютер частью бот-сети (botnet);
- запросить автоматически подставляемые пароли, хранящиеся в системе;
- создать новых пользователей;
- создавать, удалять и модифицировать данные; создавать и удалять таблицы.
Это отнюдь не исчерпывающий список, и степень опасности ограничена только существующими разрешениями - и изобретательностью атакующего.
Встраивание SQL-кода известно уже так давно, что меня нередко спрашивают, а не потеряло ли оно актуальность? Ответ: абсолютно нет и взломщики очень часто пользуются этим методом. По сути, если не считать атаки Denial of Service (DoS), то встраивание SQL-кода является самым распространенным типом атак.
Как эксплуатируется результат этой атаки? Результаты встраивания SQL-кода обычно эксплуатируются через прямой вход на веб-страницу или модификацию параметров, что обычно включает не только формы и URI, но и файлы cookie, заголовки и прочее, если приложение использует эти значения в небезопасном SQL-выражении (об этом мы поговорим позже).
Рассмотрим пример встраивания SQL-кода через модификацию формы. Этот сценарий я много раз видел в производственном коде. Ваш код может немного отличаться, но это распространенный способ проверки разработчиками удостоверений входа.
Вот динамически формируемое SQL-выражение для получения логина пользователя:
string loginSql = string.Format(
"select * from users where loginid= '{0}
' and password= '{1} '"", txtLoginId.Text, txtPassword.Text);
Это дает такое SQL-выражение:
select * from dbo.users where loginid='Administrator' and
password='12345'
Само по себе, это не проблема. Но предположим, что в поле на форме введено нечто вроде показанного на рис. 1.
Рис. 1. Злонамеренный ввод вместо допустимого имени пользователя
Этот ввод приведет к формированию следующего SQL-выражения:
select * from dbo.users where loginid='anything'
union select top 1 *
from users --' and password='12345'
В этом примере вводится неизвестный идентификатор "anything", который сам по себе не вернул бы никаких записей. Однако потом эти результаты объединяются с первой записью в базе данных, а последующий оператор "--" приводит к тому, что остальная часть запроса рассматривается как комментарий и поэтому игнорируется. Взломщик не только получает возможность входа, но и возвращает в вызывающий код запись действительного пользователя, не имея ни малейшего представления о его имени.
Ваш код, возможно, не дублирует этот сценарий, но важнее другое: анализируя свое приложение, продумайте, откуда обычно поступают значения, включаемые в запросы, в том числе из:
- полей форм;
- параметров URL;
- значений, хранимых в базе данных;
- файлов cookie;
- заголовков;
- файлов;
- изолированного хранилища.
Не все из перечисленного очевидно. Например, почему заголовки (headers) являются потенциальной уязвимостью? Если ваше приложение хранит информацию о профилях пользователей в заголовках и эти значения используются в динамических запросах, то вот вам и уязвимость. Все это может стать источником атаки, если вы применяете динамический SQL.
Веб-страницы, включающие поисковую функциональность, могут оказаться исключительно легкой жертвой для взломщиков, потому что они открывают прямую дорогу для попыток встраивания SQL-кода. В уязвимом приложении это может дать атакующему почти готовый редактор запросов.
В последние годы люди гораздо лучше осознают проблемы безопасности, поэтому системы в целом стали более стойкими по умолчанию. Например, системная процедура xp_cmdshell в экземплярах SQL Server 2005 и более поздних версий (в том числе в SQL Express) отключена. Но не думайте, что это может воспрепятствовать атакующему выполнять команды на вашем сервере. Если учетная запись, используемая вашим приложением для доступа к базе данных, имеет достаточно высокий уровень разрешений, атакующий сможет просто встроить следующую команду, чтобы вновь включить упомянутую ранее системную процедуру:
EXECUTE SP_CONFIGURE 'xp_cmdshell', '1'
Как предотвратить встраивание SQL-кода? Давайте сначала обсудим, как постараться не исправить эту проблему. Очень распространенный подход к исправлению традиционных ASP-приложений заключался в простой замене дефисов и кавычек. Увы, этот подход по-прежнему широко используется даже в .NET-приложениях и зачастую является единственным средством защиты:
string safeSql = "select * from users where loginId =
" + userInput.Replace("--", "");
safeSql = safeSql.Replace("'","''");
safeSql = safeSql.Replace("%","");
При таком подходе предполагается, что вы делаете следующее.
Должным образом защищаете каждый запрос такими вызовами. То есть разработчик должен помнить о том, чтобы везде включать эти подставляемые в строку проверки, а не использовать шаблон защиты по умолчанию - даже после кодирования в выходные и после того, как кончатся все запасы кофе.
Проверяете тип каждого параметра. Обычно это лишь вопрос времени, когда именно разработчик забудет проверить параметр с веб-страницы, например действительно ли значением параметра является число, а потом использует это число в каком-нибудь запросе ProductId безо всяких проверок строк, ведь это же число, а не строка. Что будет, если взломщик изменит ProductId и просто прочитает из строки запроса:
URI: http://yoursite/product.aspx?productId=10
Это даст ему:
select * from products where productid=10
А затем взломщик введет такие команды:
URI: http://yoursite/product.aspx?productId=10;select
1 col1 into #temp; drop table #temp;
сформировав в результате запрос:
select * from products where productid=10;select 1 col1 into
#temp; drop table #temp;
О, нет! Вы только что встроили целочисленное значение в поле, которое не было отфильтровано строковой функцией и которое не было проверено на допустимый тип. Такой вариант называют атакой с прямым встраиванием, так как кавычки здесь не нужны и встраиваемая часть напрямую используется в запросе без кавычек. Вы можете сказать, что всегда проверяете все свои данные, но в таком случае разработчик отвечает за проверку каждого параметра вручную и может легко ошибиться. Почему бы не исправить это более правильным способом, задействовав в приложении шаблон получше?
Итак, каков же правильный способ предотвращения попыток встраивания SQL-кода? На самом деле в большинстве случаев он весьма прост. Главное - использовать параметризованные вызовы. Динамический SQL реально может быть безопасным при условии, что вызовы являются параметризованными. Вот базовые правила.
1. Убедитесь, что вы используете только:
- хранимые процедуры (без динамического SQL);
- параметризованные запросы (рис. 2);
- параметризованные вызовы хранимых процедур (рис. 3).
Рис. 2. Параметризованный запрос
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings[1].ConnectionString))
{
using (SqlDataAdapter adapter = new SqlDataAdapter())
{
// Заметьте, что мы используем динамический блок 'like'
string query = @"Select Name, Description,
Keywords From Product
Where Name Like '%' + @ProductName + '%'
Order By Name Asc";
using (SqlCommand command = new SqlCommand(
query, connection))
{
command.Parameters.Add(new SqlParameter(
"@ProductName", searchText));
// Получаем данные
DataSet dataSet = new DataSet();
adapter.SelectCommand = command;
adapter.Fill(dataSet, "ProductResults");
// Заполняем сетку данных
productResults.DataSource = dataSet.Tables[0];
productResults.DataBind();
}
}
}
Рис. 3. Параметризованный вызов хранимой процедуры
// Пример параметризованного вызова хранимой процедуры
string searchText = txtSearch.Text.Trim();
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings[0].ConnectionString))
{
using (SqlDataAdapter adapter = new SqlDataAdapter())
{
// Примечание: НЕ используйте запрос так:
// string query =
// "dbo.Proc_SearchProduct" + productName + ")";
// Он должен быть параметризованным -
// используйте CommandType.StoredProcedure!
string query = "dbo.Proc_SearchProduct";
Trace.Write(string.Format("Query is: {0}", query));
using (SqlCommand command = new SqlCommand(
query, connection))
{
command.Parameters.Add(new SqlParameter(
"@ProductName", searchText));
command.CommandType = CommandType.StoredProcedure;
// Получаем данные
DataSet products = new DataSet();
adapter.SelectCommand = command;
adapter.Fill(products, "ProductResults");
// Заполняем сетку данных
productResults.DataSource = products.Tables[0];
productResults.DataBind();
}
}
}
Динамически формируемый SQL-код в хранимых процедурах должен представлять собой параметризованные вызовы sp_executesql. Избегайте использования exec - эта команда не поддерживает параметризованные вызовы. Избегайте конкатенации строк с пользовательским вводом. Изучите листинг на рис. 4.
Рис. 4. Параметризованный вызов sp_executesql
/*
Это демонстрация применения динамического SQL,
но с использованием безопасного параметризованного запроса
*/
DECLARE @name varchar(20)
DECLARE @sql nvarchar(500)
DECLARE @parameter nvarchar(500)
/* Формируем SQL-строку один раз */
SET @sql= N'SELECT * FROM Customer
WHERE FirstName Like @Name Or LastName Like @Name +''%''';
SET @parameter= N'@Name varchar(20)';
/* Выполняем строку со значением первого параметра.
Примечание: -- ничего не делает, как и должно быть! */
SET @name = 'm%'; --ex. mary, m%, etc.
EXECUTE sp_executesql @sql, @parameter, @Name = @name;
/* Из-за отсутствия поддержки параметров НЕ используйте
exec 'select... ' + @sql */
Не думайте, что простая замена дефисов и кавычек обеспечит безопасность. Выбирайте единообразные методы доступа к данным (как было описано), которые предотвращают встраивание SQL-кода без ручного вмешательства разработчика, и придерживайтесь их. Если вы полагаетесь на escaping-процедуру и случайно забыли вызвать ее в одном месте, вы рискуете. Более того, уязвимость может быть даже в том, как вы реализуете эту процедуру, например в случае атаки с сокращением SQL-кода (SQL truncation attack).
Проверяйте ввод (см. следующий раздел) с помощью проверок типов и преобразования; используйте регулярные выражения для ограничения, например, только до буквенно-цифровых данных или извлекайте важные данные лишь из известных источников; не доверяйте данным, поступающим с веб-страниц.
Выполните аудит разрешений объектов базы данных, чтобы ограничить права пользователя приложения и тем самым уменьшить возможности для атак. Выдавайте права на операции обновления, удаления и вставки, только если у пользователя действительно должна быть возможность их выполнения. У каждого индивидуального приложения должен быть свой логин для доступа к базе данных с ограниченными разрешениями. Моя программа SQL Server Permissions Auditor с открытым исходным кодом (sqlpermissionsaudit.codeplex.com) может помочь вам в этом деле.
Очень важно вести аудит разрешений ваших таблиц, если вы используете параметризованные запросы. Такие запросы требуют, чтобы у пользователя или роли были соответствующие разрешения для доступа к таблице. Ваше приложение может быть защищено от встраивания SQL-кода, но что будет, если другое - незащищенное - приложение обратится к вашей базе данных? Атакующий сможет хозяйничать в вашей базе данных через него, поэтому вы должны быть уверены, что у каждого приложения есть свой уникальный и ограниченный логин. Вы также должны проверить разрешения для таких объектов базы данных, как представления, процедуры и таблицы. Хранимые процедуры требуют разрешения лишь для их вызова, а не применительно к таблицам (как правило, пока внутри хранимой процедуры нет динамического SQL-кода), поэтому управлять их защитой немного легче. И здесь мой SQL Server Permissions Auditor также может помочь.
Очень важно вести аудит разрешений ваших таблиц, если вы используете параметризованные запросы.
Заметьте, что Entity Framework на внутреннем уровне использует параметризованные запросы и благодаря этому обеспечивает защиту от встраивания SQL-кода в обычных сценариях. Некоторые предпочитают сопоставлять свои сущности с хранимыми процедурами, а не открывать разрешения таблиц динамическим параметризованным запросам. Оба подхода имеют право на существование, у них есть свои сильные стороны. Так что решение остается за вами. Заметьте: если вы явным образом используете Entity SQL, то должны знать о некоторых дополнительных соображениях по поводу безопасности своих запросов. Пожалуйста, зайдите на веб-страницу MSDN Library "Security Considerations (Entity Framework)" по ссылке msdn.microsoft.com/library/cc716760.
Модификация параметров
Что это такое? Модификация параметров - атака, при которой параметры изменяются так, чтобы модифицировать ожидаемую функциональность приложения. Параметры могут находится в форме, строке запроса, файлах cookie, базе данных и т. д. Я рассмотрю атаки, включающие параметры, которые поступают через Web.
Как эксплуатируется результат этой атаки? Взломщик изменяет параметры, чтобы заставить приложение выполнить такую операцию, которая не была предусмотрена разработчиком. Допустим, вы сохраняете запись пользователя, считывая его идентификатор из строки запроса. Безопасно ли это? Нет. Атакующий может изменить URL в вашем приложении по аналогии с тем, что показано на рис. 5.
Рис. 5. ИзмененныйURL
Тем самым атакующий мог бы неожиданно для вас загрузить свою учетную запись пользователя. И слишком часто в коде приложений слепо доверяют этому userId:
// Скверная практика!
string userId = Request.QueryString["userId"];
// Загрузка пользователя на основе этого идентификатора
var user = LoadUser(userId);
Есть ли способ получше? Да! Вместо того чтобы доверять форме, вы можете считывать значения из более доверяемого источника, например из сеанса пользователя, или получать от провайдера членства в группах или провайдера профилей.
Различные инструменты позволяют довольно легко модифицировать нечто большее простой строки запроса. Советую изучить некоторые панели инструментов для разработчиков в веб-браузере, которые дают возможность видеть скрытые элементы на ваших страницах. Думаю, вы удивитесь тому, что обнаружите, и тому, насколько легко модифицировать ваши данные. Рассмотрим страницу Edit User на рис. 6. Если вы откроете скрытые поля на этой странице, то заметите, что идентификатор пользователя встроен непосредственно в форму и готов к модификации (рис. 7). Это поле используется как основной ключ для записи данного пользователя, и его модификация изменит запись, которая сохраняется обратно в базе данных.
Рис. 6. Форма Edit User
Рис. 7. Открытие скрытых полей на форме
Как предотвратить модификацию параметров?
Не доверяйте данным, предоставляемым пользователем, и проверяйте их на соответствие, прежде чем принимать на их основе какие-либо решения. В целом, вас не должно волновать, если пользователь изменит свое отчество, хранящееся в его профиле. Но вас точно должно волновать, чтобы он не модифицировал скрытый в форме идентификатор, представляющий ключ записи об этом пользователе. В подобных случаях вы можете извлекать доверяемые данные из известного источника на сервере, а не с веб-страницы. Эту информацию можно было бы хранить в сеансе пользователя или провайдере членства в группах.
Например, гораздо лучше использовать информацию от провайдера членства в группах, чем данные с формы:
// Более правильная практика
int userId = Membership.GetUser().ProviderUserKey.ToString();
// Загрузка пользователя на основе этого идентификатора
var user = LoadUser(userId);
Теперь, когда вы увидели, насколько злонамеренными могут быть данные от браузера, давайте рассмотрим несколько примеров проверки этих данных, чтобы прояснить некоторые вещи. Вот типичный сценарий использования Web Forms:
// 1. Никакой проверки! Особенно проблематично, так как этот
// productId на самом деле является числовым
string productId = Request.QueryString["ProductId"];
// 2. Проверка получше
int productId = int.Parse(Request.QueryString["ProductId"]);
// 3. Еще лучше
int productId = int.Parse(Request.QueryString["ProductId"]);
if (!IsValidProductId(productId))
{
throw new InvalidProductIdException(productId);
}
На рис. 8 показан типичный сценарий для MVC с привязкой модели, когда автоматически выполняются преобразования базовых типов без их явного приведения.
Рис. 8. Использование связывания с моделью в MVC
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Exclude="UserId")] Order order)
{
...
// Все свойства объекта заказа были заполнены автоматически,
// равно как и автоматически преобразованы их типы через
// MVC-механизм привязки модели (из формы в модель)
Trace.Write(order.AddressId);
Trace.Write(order.TotalAmount);
// Мы не хотим доверять идентификатору клиента со страницы
// на случай, если он был модифицирован. Получаем его
// от провайдера профилей или объекта членства в группах.
order.UserId = Profile.UserId;
// Либо берем его отсюда
order.UserId = Membership.GetUser().
ProviderUserKey.ToString();
...
order.Save();}
...
// и т. д.
}
Привязка модели (model binding) - отличный механизм Model-View-Controller (MVC), который помогает в проверках параметров, так как свойства объекта Order будут автоматически заполняться и преобразовываться в определенные типы на основе информации от формы. Вы можете определить атрибуты Data Annotations для своей модели, а также включить множество разнообразных проверок. Просто будьте осторожны и ограничивайте круг свойств, которые можно заполнять данными от формы, и, опять же, не доверяйте данным со страницы, относящимся к важным элементам. Хорошее эмпирическое правило - создавать по одному ViewModel для каждого View, и тогда вы полностью исключите UserId из модели в этом примере с Edit User.
Заметьте, что я использую здесь атрибут [Bind(Exclude)], ограничивая то, что MVC связывает с моей моделью (Model), и это позволяет указывать, чему я доверяю, а чему - нет. Это гарантирует, что UserId не попадет ко мне из данных формы, а значит, он не может быть модифицирован. Обсуждение привязки модели и атрибутов Data Annotations выходит за рамки этой статьи. Я упомянул о них лишь потому, что хотел показать, как может работать типизация параметров в Web Forms и MVC.
Если вы должны включать поле ID с веб-страницы, которой вы "доверяете", пожалуйста, пройдите по ссылке MVC Security Extensions (mvcsecurity.codeplex.com), чтобы узнать об атрибуте, помогающем в таких ситуациях.
Заключение
В этой статье я представил два самых распространенных способа взлома приложений. Но, как видите, эти атаки можно предотвращать или, по крайней мере, ограничивать, внося всего несколько изменений в свои приложения. Конечно, существуют вариации этих атак и другие способы взлома ваших приложений. В следующей статье мы рассмотрим еще два типа атак: кросс-сайтовые скрипты и подделку кросс-сайтовых запросов.
Ссылки по теме