Один французский «исследователь безопасности» этим летом опубликовал невиданно много найденных им уязвимостей типа arbitrary file upload в разных «написанных на коленке», но популярных CMS и плагинах к ним. Удивительно, как беспечны бывают создатели и администраторы небольших форумов, блогов и интернет-магазинчиков. Как правило, в каталоге, куда загружаются аватары, резюме, смайлики и прочие ресурсы, которые пользователь может загружать на сайт — разрешено выполнение кода PHP; а значит, загрузка PHP-скрипта под видом картинки позволит злоумышленнику выполнять на сервере произвольный код.
Выполнение кода с правами apache — это, конечно, не полный контроль над сервером, но не стоит недооценивать открывающиеся злоумышленнику возможности: он получает полный доступ ко всем скриптам и конфигурационным файлам сайта и через них — к используемым БД; он может рассылать от вашего имени спам, захостить у вас какой-нибудь незаконный контент, тем подставив вас под абузы; может, найдя параметры привязки к платёжной системе, отрефандить все заказы и оставить вас без дохода за весь последний месяц. Обидно, правда?
Как ему это удастся?
Клинический случай: вы скопировали из мануала по PHP строчку
и так её и оставили в продакшн-коде.
Так я могу заливать вообще любые файлы, причём не только в каталог upload, а куда угодно — могу указать в качестве имени файла ../index.html и заменить вашу главную страницу своей. Обидно, правда?
Добавьте хотя бы вызов basename() для
Перед тем, как сохранить загруженный файл в upload/, вы вызываете getimagesize(), чтоб убедиться, что загружена именно картинка. К превеликому удовольствию «исследователей безопасности», PHP позволяет вставлять выполнимый код в любое место любого файла, игнорируя всё, что не заключено в теги <? ?>. Я могу взять фотографию своего любимого котика, дописать в её конец что-нибудь навроде
Почему нельзя доверять полю «mime», возвращаемому getimagesize(), понятно: оно берётся на основании заголовка файла. Тем более нельзя доверять
В другом самоуверенном проекте я только что видел проверку
Особенность Apache — что когда он встречает файлы с незнакомыми расширениями, он эти расширения «откусывает» и ищет знакомые расширения перед ними. Поэтому если я залью файл pwn.php.omgif, то он пройдёт проверку, но Apache увидит в нём PHP-скрипт, потому что для расширения omgif не зарегистрирован ни mime-type, ни модуль-обработчик.
В третьем самоуверенном проекте я видел проверку
Предположим, вы убедились, что имя файла заканчивается на .jpg (вместе с точкой!) Можно ли быть уверенным, что PHP-код в нём не будет выполняться? Учитывая предыдущий пункт — скорее всего можно, но гарантии нет. Может случиться, что злоумышленник (или криворукий админ) как-то повредил /etc/mime.types, так что расширение .jpg окажется незарегистрированным, и Apache станет анализировать расширение перед ним. Может случиться, что /etc/mime.types стал недоступен из-за chroot. Может случиться, что злоумышленник (или криворукий админ) забросил в каталог upload/ такой .htaccess, который позволяет выполнять PHP-код во всех файлах подряд.
Кроме того, до недавних версий PHP можно было загрузить файл с именем «pwn.php\0.jpg», который при сохранении на диск превращался бы в «pwn.php», потому что для системных вызовов строка с именем файла заканчивается на \0, а для функций PHP это обычный допустимый в строке символ. В таком случае проверка расширения тоже ничего не гарантировала бы.
Исчерпывающий чеклист попытались составить на stackoverflow. Если коротко: а) запретите явно выполнение PHP в каталоге, куда сохраняются пользовательские файлы; б) переименовывайте пользовательские файлы в неподконтрольные пользователям имена; в) если возможно, пересохраняйте картинки при помощи GD, чтобы удалить ненужные метаданные и непрошенные приписки в конце файла.
Выполнение кода с правами apache — это, конечно, не полный контроль над сервером, но не стоит недооценивать открывающиеся злоумышленнику возможности: он получает полный доступ ко всем скриптам и конфигурационным файлам сайта и через них — к используемым БД; он может рассылать от вашего имени спам, захостить у вас какой-нибудь незаконный контент, тем подставив вас под абузы; может, найдя параметры привязки к платёжной системе, отрефандить все заказы и оставить вас без дохода за весь последний месяц. Обидно, правда?
Как ему это удастся?
Вы не проверяете загружаемые файлы вообще?
Клинический случай: вы скопировали из мануала по PHP строчку
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);
и так её и оставили в продакшн-коде.
Так я могу заливать вообще любые файлы, причём не только в каталог upload, а куда угодно — могу указать в качестве имени файла ../index.html и заменить вашу главную страницу своей. Обидно, правда?
Добавьте хотя бы вызов basename() для
$_FILES["file"]["name"]
. В последней версии PHP, судя по моим экспериментам, $_FILES["file"]["name"]
и так возвращает только basename от переданного в HTTP-запросе значения filename; но осторожность тут лишней не будет.Вы проверяете, является ли файл картинкой?
Перед тем, как сохранить загруженный файл в upload/, вы вызываете getimagesize(), чтоб убедиться, что загружена именно картинка. К превеликому удовольствию «исследователей безопасности», PHP позволяет вставлять выполнимый код в любое место любого файла, игнорируя всё, что не заключено в теги <? ?>. Я могу взять фотографию своего любимого котика, дописать в её конец что-нибудь навроде
<?php passthru($_GET['c']); ?>
и залить под именем pwn.php. Тогда getimagesize() подтвердит вам, что в файле JPEG-картинка, потому что анализирует только заголовки; и моя «фотография с припиской» пройдёт проверку.Вы проверяете mime-type?
Почему нельзя доверять полю «mime», возвращаемому getimagesize(), понятно: оно берётся на основании заголовка файла. Тем более нельзя доверять
$_FILES["file"]["type"]
— ничто не мешает мне передать в HTTP-запросе «Content-Type: image/jpeg» перед PHP-скриптом. Это кажется банальным, но люди действительно на это полагаются. Вот я только что видел проверку if(substr($_FILES["file"]["type"], 0, 6)=="image/") {/* сохранить файл */}
в одном самоуверенном проекте. Они, поди, считают изящным и хитроумным то, что смогли одной проверкой покрыть все возможные типы картинок! Но не стоит у них заимствовать этот «передовой опыт».Вы проверяете расширение?
В другом самоуверенном проекте я только что видел проверку
if(substr($_FILES["file"]["name"], -3)=="jpg" || substr($_FILES["file"]["name"], -3)=="gif" || substr($_FILES["file"]["name"], -3)=="png") {/* сохранить файл */}
Особенность Apache — что когда он встречает файлы с незнакомыми расширениями, он эти расширения «откусывает» и ищет знакомые расширения перед ними. Поэтому если я залью файл pwn.php.omgif, то он пройдёт проверку, но Apache увидит в нём PHP-скрипт, потому что для расширения omgif не зарегистрирован ни mime-type, ни модуль-обработчик.
Вы проверяете «нехорошие» расширения?
В третьем самоуверенном проекте я видел проверку
if(strpos($_FILES["file"]["name"], ".php")===FALSE) {/* сохранить файл */}
— мол, если в имени файла хоть где-нибудь встретилось расширение .php, значит файл не годится. Но не надо забывать, что по умолчанию интерпретатор PHP вызывается ещё и для расширений .phtml и .php3, а некоторые хостеры включают его и для других расширений — порой даже для .html, .js, .jpg и так далее, чтобы вставлять в статические ресурсы какой-нибудь SEO-код, или отслеживать статистику хитов. (Можете сами проверить, сколько гугл находит вопросов «как мне сделать, чтоб выполнялся PHP-код в JPEG-файлах?») Кроме того, если злоумышленнику удастся загрузить свой .htaccess, то он сможет исполнять PHP-код в файлах с любыми расширениями — даже в самом этом файле .htaccess. В общем, будьте бдительны.Вы проверяете расширение правильно?
Предположим, вы убедились, что имя файла заканчивается на .jpg (вместе с точкой!) Можно ли быть уверенным, что PHP-код в нём не будет выполняться? Учитывая предыдущий пункт — скорее всего можно, но гарантии нет. Может случиться, что злоумышленник (или криворукий админ) как-то повредил /etc/mime.types, так что расширение .jpg окажется незарегистрированным, и Apache станет анализировать расширение перед ним. Может случиться, что /etc/mime.types стал недоступен из-за chroot. Может случиться, что злоумышленник (или криворукий админ) забросил в каталог upload/ такой .htaccess, который позволяет выполнять PHP-код во всех файлах подряд.
Кроме того, до недавних версий PHP можно было загрузить файл с именем «pwn.php\0.jpg», который при сохранении на диск превращался бы в «pwn.php», потому что для системных вызовов строка с именем файла заканчивается на \0, а для функций PHP это обычный допустимый в строке символ. В таком случае проверка расширения тоже ничего не гарантировала бы.
И что же делать?
Исчерпывающий чеклист попытались составить на stackoverflow. Если коротко: а) запретите явно выполнение PHP в каталоге, куда сохраняются пользовательские файлы; б) переименовывайте пользовательские файлы в неподконтрольные пользователям имена; в) если возможно, пересохраняйте картинки при помощи GD, чтобы удалить ненужные метаданные и непрошенные приписки в конце файла.