Уязвимость PHP regex

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

if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $var) && preg_match('/\.(asp|jsp|php)$/i', $var) == false) 
{
    echo "No way you have extension .php or .jsp or .asp after this check.";
}

Как бы я ни старался и обыскал сеть, я не смог найти недостаток, который мог бы сделать такое. Могу ли я что-то забыть? Учитывая, что проблема с "нулевым байтом" устранена, что еще может быть проблемой?

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

EDIT - действительный код:

if (isset($_FILES["image"]) && $_FILES["image"]["name"] && preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $_FILES["image"]["name"]) && preg_match('/\.(asp|jsp|php)$/i', $_FILES["image"]["name"]) == false) {
        $time = time();
        $imgname = $time . "_" . $_FILES["image"]["name"];
        $dest = "../uploads/images/";

        if (file_exists($dest) == false) {
            mkdir($dest);
        }

        copy($_FILES['image']['tmp_name'], $dest . $imgname);

    }else{
        echo "Invalid image file";
    }

Версия PHP: 5.3.29

EDIT: эпилог

Оказалось, что "уязвимость" присутствует только в Windows. Тем не менее, он сделал именно то, что сказал мне мой коллега, - проверил проверку регулярного выражения и сохранил файл с исполняемым расширением. Следующее было проверено на WampServer 2.2 с PHP 5.3.13:

Передача следующей строки в проверку регулярного выражения выше test.php:.jpg (обратите внимание на символ двоеточия ":" в конце желаемого расширения) проверит его, и функция copy(), кажется, опускает все после символа двоеточия, включая символ сам. Опять же, это верно только для окон. В linux файл будет написан точно с тем же именем, который передается функции.

Ответ 1

Существует не один шаг или полный прямой способ использовать ваш код, но вот некоторые мысли.

Вы передаете его в copy() в этом примере, но вы упомянули, что используете этот метод для проверки файла ext awhile now, поэтому я предполагаю, что у вас были другие случаи, которые могли использовать эту процедуру для других функций тоже для разных PHP-версии.

Рассмотрим это как тестовую процедуру (Exploiting include, require):

$name = "test.php#.txt";
if (preg_match('/\.(xml|csv|txt)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
    echo "in!!!!";
    include $name;
} else {
    echo "Invalid data file";
}

Это закончится печатью "in!!!!" и выполнение "test.php", даже если оно будет загружено, оно будет включать его из папки tmp - конечно, что в этом случае вы уже являетесь владельцем злоумышленника, но также рассмотрите эти варианты. Это не общий сценарий для процедуры загрузки, но это концепция, которая может быть использована путем объединения нескольких методов:

Перемещение вперед - если вы выполните:

//$_FILES['image']['name'] === "test.php#.jpg";
$name = $_FILES['image']['name'];
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
    echo "in!!!!";
    copy($_FILES['image']['tmp_name'], "../uploads/".$name);
} else {
    echo "Invalid image file";
}

Снова отлично. Файл скопирован в папку "uploads" - вы не можете получить к нему доступ напрямую (поскольку веб-сервер будет вырезать правую сторону #), но вы ввели файл, и злоумышленник может найти способ или другую слабую точку для вызова это позже.

Пример такого сценария выполнения распространен среди сайтов совместного доступа и хостинга, где файлы обслуживаются PHP script, который (в некоторых небезопасных случаях) может загрузить файл, включив его с неправильным типом функций, таких как require, include, file_get_contents, которые все уязвимы и могут выполнять файл.

NULL-байт Атаки с нулевым байтом были большой слабостью в php < 5.3, но был повторно введен регрессией в версиях 5.4+ в некоторых функциях, включая все связанные с файлом функции и многое другое в расширениях. Он был исправлен несколько раз, но он все еще там, и многие старые версии все еще используются. Если вы работаете со старой версией php, вы определенно Exposed:

//$_FILES['image']['name'] === "test.php\0.jpg";
$name = $_FILES['image']['name'];
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $name) && preg_match('/\.(asp|jsp|php)$/i', $name) == false) {
    echo "in!!!!";
    copy($_FILES['image']['tmp_name'], "../uploads/".$name);
} else {
    echo "Invalid image file";
}

Будет напечатан "in!!!!" и скопируйте файл с именем "test.php".

Как исправлено php, проверяя длину строки до и после передачи ее более глубокой процедуре C, которая создает фактический массив char, и тем самым, если строка усечена нулевым байтом (что указывает на конец строки в C) длина не будет соответствовать. читать больше

Как ни странно, даже в исправленных и современных версиях PHP он все еще там:

$input = "foo.php\0.gif";
include ($input); // Will load foo.php :)

Мой вывод: Ваш метод проверки расширений файлов может быть значительно улучшен - ваш код позволяет пропустить PHP файл с именем test.php#.jpg, пока он не должен. Успешные атаки в основном выполняются путем объединения нескольких уязвимостей даже младших - вы должны рассматривать любые неожиданные результаты и поведение как один.

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

Ответ 2

Попробуйте этот код.

$allowedExtension = array('jpeg','png','bmp'); // make list of all allowed extension

if(isset($_FILES["image"]["name"])){
     $filenameArray = explode('.',$_FILES["image"]["name"]);
     $extension = end($filenameArray);
     if(in_array($extension,$allowedExtension)){
        echo "allowed extension";
     }else{
          echo "not allowed extension";
     }
}

Ответ 3

preg_match() возвращает 1, если шаблон соответствует заданному объекту, 0, если это не так, или FALSE, если произошла ошибка.

$var = "test.php";
if (preg_match('/\.(jpeg|jpg|gif|png|bmp|jpe)$/i', $var) === 1 
    && preg_match('/\.(asp|jsp|php)$/i', $var) !== 1) 
{
    echo "No way you have extension .php or .jsp or .asp after this check.";
} else{
    echo "Invalid file";
}

Итак, когда вы собираетесь проверить свой код, используйте === 1.

В идеале вы должны использовать.

function isImageFile($file) {
    $info = pathinfo($file);
    return in_array(strtolower($info['extension']), 
                    array("jpg", "jpeg", "gif", "png", "bmp"));
}

Ответ 4

Я помню, что в версии certains в PHP < 5.3.X, PHP позволяет содержать строки 0x00, этот char рассматривается как конец строки
Итак, например, если ваша строка содержит: myfile.exe\0.jpg, поэтому preg_match() будет соответствовать jpg, но другие PHP функции будут останавливаться в myfile.exe, как функции include() или copy()