
За последний год на PortSwigger Academy, HackTheBox и TryHackMe я прошёл больше 30 тасков категории web, связанных с unrestricted file upload. Примерно 8 из 10 сводятся к трём базовым техникам обхода: подмена расширения, манипуляция Content-Type и модификация magic bytes. Каждая техника — отдельный слой валидации, который разработчик мог прикрутить. Задача CTF-игрока — определить, какие слои стоят, и сломать именно их. Здесь — пошаговая методология с Burp Suite, которая работает от простых тасков до skill assessment на HTB.
Загрузка веб-шелла — не самоцель, а звено в цепочке от initial access до выполнения команд на сервере. По классификации MITRE ATT&CK, атака через file upload проходит через несколько тактик: Подробнее — в нашем статье о пентест веб-приложений.
В CTF обычно достаточно дойти до шага 4 и выполнить cat /flag.txt или whoami. В реальном пентесте после получения шелла начинается lateral movement. Но логика обхода фильтров — одна и та же.
По классификации OWASP, file upload напрямую связан с A05:2021 — Security Misconfiguration: выполнение файлов в директории загрузки, отсутствие серверной валидации содержимого, доверие клиентским заголовкам.
Прежде чем открывать Burp, убедитесь что стенд готов.
exiftool (предустановлен в Kali), xxd или hexedit для работы с magic bytes, Python 3 для генерации полиглот-файловПервый шаг при встрече с формой загрузки — разведка. Нужно понять, какие слои защиты стоят на сервере, прежде чем подбирать конкретную технику обхода.
Шаг 1: определить blacklist или whitelist. Загрузите файл с произвольным несуществующим расширением, например test.codeby. Файл загрузился — сервер использует чёрный список (блокирует конкретные расширения, остальное пропускает). Отклонил — скорее всего, белый список (разрешает только определённые расширения). Эту технику описывает Intigriti в своём гайде по file upload: загрузка файла с рандомным расширением мгновенно показывает тип фильтрации.
Шаг 2: проверить, где хранится файл. После успешной загрузки легитимного изображения найдите его URL. Типичные директории: /uploads/, /media/, /files/, /static/. Если URL содержит оригинальное имя файла — путь к эксплуатации короче. Если имя заменено на хеш или UUID — перезапись .htaccess и path traversal становятся сложнее.
Шаг 3: проверить исполнение. Загрузите легитимный .txt файл с содержимым test и обратитесь к нему по URL. Сервер отдаёт содержимое как текст? Отлично — файлы из директории загрузки доступны напрямую.
| Ситуация | Первая техника | Запасная техника | Инструмент |
|---|---|---|---|
| Чёрный список расширений, case-sensitive фильтр | Вариация регистра: .pHp, .PhP |
Альтернативные расширения: .phtml, .phar |
Burp Repeater |
| Чёрный список, нестрогий парсинг | Двойное расширение: shell.php.jpg |
Null byte (legacy): shell.php%00.jpg |
Burp Repeater / Intruder |
| Белый список расширений, проверка Content-Type | Подмена MIME: Content-Type: image/jpeg |
Полиглот-файл с magic bytes | Burp Repeater |
| Белый список, проверка magic bytes | Magic bytes + PHP код | Внедрение кода в метаданные через exiftool | exiftool + Burp |
| Все слои одновременно | Комбинация: magic bytes + whitelist extension + MIME | Перезапись конфигурации .htaccess (если доступно) |
exiftool + Burp Repeater |
| Файл загружается, но не исполняется | Проверить конфигурацию сервера, path traversal ../ |
Загрузка .htaccess с AddType application/x-httpd-php .jpg |
Burp Repeater |
Фильтрация по расширению — первый и самый распространённый барьер в CTF. Сервер смотрит на имя файла и решает, пропускать или нет. Здесь важно понимать разницу между подходами.
Чёрный список блокирует известные опасные расширения: .php, .jsp, .asp. Но PHP-интерпретатор обрабатывает не только .php.
Альтернативные расширения. Apache с mod_php может исполнять файлы с расширениями .php3, .php4, .php5, .phtml, .phar, .pht — зависит от конфигурации. Если чёрный список содержит только .php, загрузка shell.phtml даст RCE. Для фаззинга в Burp Intruder берём wordlist из этих расширений и смотрим на разницу в ответах сервера (размер или код ответа).
Вариация регистра. Если фильтр проверяет расширение без приведения к нижнему регистру, файл shell.pHp или shell.PHP пройдёт проверку. На Linux-системах файловая система чувствительна к регистру, но Apache передаёт файл PHP-обработчику по конфигурации AddHandler, которая может быть case-insensitive.
Двойные расширения. Файл shell.php.jpg — классика CTF. Логика зависит от того, как сервер определяет расширение. Некоторые парсеры берут последнее расширение (.jpg — проходит фильтр), а Apache с mod_mime может обработать файл по первому распознаваемому расширению (.php — исполняется). Это техника Double File Extension (T1036.007, Defense Evasion) по классификации MITRE ATT&CK.
Добавление точки или пробела. Имена файлов shell.php. или shell.php%20 на некоторых файловых системах теряют завершающий символ после сохранения, превращаясь в shell.php. Фильтр видит расширение .php. и пропускает, а файловая система сохраняет как .php.
Null byte injection. На старых системах (PHP < 5.3.4) нулевой байт %00 обрезает строку. Файл shell.php%00.jpg фильтром воспринимается как .jpg, но сохраняется как shell.php. Техника древняя, но в CTF встречается на задачах с пометкой «legacy».
Белый список строже: сервер разрешает только .jpg, .png, .gif. Прямая подмена расширения здесь не прокатит.
Двойное расширение в обратную сторону. Файл shell.jpg.php может пройти фильтр, который проверяет наличие .jpg в имени файла (а не позицию расширения). Зависит от реализации: слабое регулярное выражение вроде /\.jpg/ пропустит такое имя.
Загрузка .htaccess. Если сервер — Apache, а белый список не блокирует служебные файлы, можно загрузить .htaccess с директивой AddType application/x-httpd-php .jpg. После этого любой .jpg файл в директории загрузки будет интерпретироваться как PHP. Затем загружаем shell.jpg с PHP-кодом внутри. Эту технику подробно разбирает PortSwigger в лабораторной работе «Web shell upload via extension blacklist bypass».
Работает если: сервер — Apache с включённой поддержкой .htaccess (AllowOverride All), директория загрузки не имеет собственного <Directory> с ограничениями.
Не работает если: сервер — Nginx (не поддерживает .htaccess), или AllowOverride None в конфигурации Apache.
Когда расширение прошло, следующий барьер — проверка заголовка Content-Type в multipart-запросе. Сервер читает MIME-тип, указанный клиентом, и сравнивает с разрешёнными значениями (image/jpeg, image/png, image/gif).
Вся штука в том, что Content-Type — клиентский заголовок. Браузер ставит его автоматически на основе расширения файла, но атакующий контролирует каждый байт запроса через Burp.
Включите перехват в Burp Proxy, загрузите легитимное изображение через форму. Burp перехватит POST-запрос с multipart/form-data.
Отправьте запрос в Repeater (Ctrl+R). Обратите внимание на структуру: внутри boundary каждая часть имеет свой Content-Disposition и Content-Type.
Меняем три элемента: имя файла на shell.php, содержимое тела на PHP-код, но Content-Type оставляем image/jpeg:
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--
Отправляем запрос. Если сервер проверяет только Content-Type и не анализирует содержимое — файл загрузится.
Обращаемся к загруженному файлу: http://target/uploads/shell.php?cmd=id. Если в ответе видите вывод команды id — RCE получен.
Почему это работает: сервер доверяет клиентскому заголовку Content-Type и не проверяет фактическое содержимое файла. Техника Masquerade File Type (T1036.008) — файл маскируется под изображение на уровне метаданных, оставаясь исполняемым кодом.
Если первая попытка не прошла, стоит определить, какие именно MIME-типы принимает сервер. Отправляем запрос в Intruder, отмечаем значение Content-Type как payload position и загружаем список: image/jpeg, image/png, image/gif, application/octet-stream, text/plain. По разнице в размере ответа или HTTP-коде определяем принимаемые значения. В Community Edition скорость режется, но для 5-10 значений хватает.
Предусловия: техника работает только если сервер определяет тип файла по заголовку Content-Type, а не по содержимому (magic bytes) или расширению. Если при отправке shell.php с Content-Type: image/jpeg сервер отвечает ошибкой валидации — значит, проверяется расширение или содержимое, и одного MIME-обхода недостаточно.
Продвинутые реализации проверяют первые байты файла — сигнатуру формата. JPEG начинается с FF D8 FF, PNG — с 89 50 4E 47 0D 0A 1A 0A, GIF — с 47 49 46 38 (или ASCII-строки GIF89a). Функция getimagesize() в PHP анализирует именно эти байты.
Обход: добавить magic bytes в начало файла перед PHP-кодом. Интерпретатор PHP проигнорирует бинарный мусор до тега <?php и выполнит код.
Самый простой вариант — GIF-сигнатура, потому что она текстовая:
GIF89a<?php system($_GET['cmd']); ?>
Сохраняем это в файл shell.php.gif (или shell.gif, если планируете комбинировать с .htaccess). При проверке getimagesize() сервер увидит GIF-заголовок. При вызове через PHP-обработчик — код выполнится.
Для JPEG magic bytes: echo -e '\xFF\xD8\xFF\xE0<?php system($_GET["cmd"]); ?>' > shell.php. Файл начинается с валидной JPEG-сигнатуры.
Ещё один подход — записать PHP-код в метаданные легитимного изображения:
exiftool -Comment='<?php system($_GET["cmd"]); ?>' photo.jpg -o polyglot.php.jpg
На выходе — файл, который проходит проверку как валидный JPEG (структура сохранена, magic bytes на месте, getimagesize() возвращает размеры), но содержит PHP-код в поле Comment. Если сервер обрабатывает этот файл через PHP-интерпретатор, код из комментария выполнится.
| Техника | Преимущества | Ограничения | Когда использовать |
|---|---|---|---|
| Подмена Content-Type | Тривиально, одна правка в Burp | Не работает при проверке содержимого файла | Content-Type — единственная серверная проверка |
| Альтернативные расширения | Широкий выбор вариантов | Зависит от конфигурации Apache/Nginx | Чёрный список, Apache с mod_php |
| Двойное расширение | Работает на многих конфигурациях | Нестабильно: результат зависит от парсера | Apache с mod_mime, слабый парсер расширений |
| GIF magic bytes | Просто: одна текстовая строка перед кодом | Строгая валидация (PIL, ImageMagick) отклонит | getimagesize() без дополнительных проверок |
| exiftool injection | Файл остаётся валидным изображением | Не все серверы отдают файл через PHP-интерпретатор | Комбинированная валидация: magic bytes + размеры |
| Перезапись .htaccess | Полный контроль над исполнением в директории | Только Apache с AllowOverride All | Белый список без блокировки служебных файлов |
В реальных CTF-тасках среднего и высокого уровня валидация многоуровневая. Типичный сценарий с HackTheBox forum: игрок загружает файл с обходом Content-Type, видит «file uploaded successfully», но при обращении к файлу получает ошибку — белый список расширений блокирует исполнение.
Алгоритм комбинированного обхода:
Определить все слои. Загрузите shell.php с Content-Type: image/jpeg — если ошибка «extension not allowed», активен фильтр расширений. Загрузите shell.jpg с PHP-кодом — если ошибка «invalid image», проверяются magic bytes.
Собрать payload. Начните с magic bytes (GIF89a), добавьте PHP-код, установите расширение по белому списку (.gif), Content-Type — image/gif.
Обеспечить исполнение. Если файл загружен как .gif, сервер не передаст его PHP-интерпретатору. Здесь нужен дополнительный вектор: загрузка .htaccess (если доступна), path traversal в имени файла (../shell.php), или эксплуатация серверной логики обработки.
Проверить path traversal. В поле filename в Burp попробуйте значения ../shell.php, ..%2fshell.php, ....//shell.php. Если файл сохраняется на уровень выше директории загрузки — он может оказаться в месте, где PHP-исполнение не заблокировано.
Конкретный пример из обсуждения на HackTheBox forum: пользователи фиксировали, что комбинация Content-Type: image/gif + magic bytes GIF89a + расширение .phar.gif давала успешную загрузку, но для исполнения требовалось правильное позиционирование расширения — .gif в конце для обхода whitelist, .phar для запуска через PHP. Решение нашлось после анализа исходного кода серверной валидации (доступного через отдельную уязвимость LFI).
Ни одна техника обхода не универсальна. Вот конкретные сценарии, где описанные методы бесполезны:
Серверная пересборка изображения. Если сервер прогоняет загруженное изображение через ImageMagick convert или PHP GD (imagecreatefromjpeg() + imagejpeg()), весь внедрённый PHP-код удаляется. Файл пересоздаётся пиксель-за-пикселем, метаданные и комментарии не сохраняются. Встречается на зрелых платформах и в CTF-тасках уровня hard/insane.
Хранение вне web root. По рекомендациям OWASP (Security Misconfiguration, A05:2021), загруженные файлы хранятся в /var/uploads/, а отдаются через отдельный endpoint с принудительным Content-Type: application/octet-stream. Даже если файл содержит PHP-код, он никогда не попадает в PHP-обработчик.
CDN и облачное хранение. Файлы, загруженные в AWS S3 или Google Cloud Storage, не исполняются на стороне хранилища. S3 отдаёт файлы as-is, без интерпретации серверного кода. В CTF такие конфигурации встречаются в задачах на cloud misconfiguration, но не на classic file upload.
Nginx без PHP-FPM для директории загрузки. Nginx не имеет встроенного PHP-модуля и не поддерживает .htaccess. Если location /uploads/ не настроен на проксирование в PHP-FPM, никакой загруженный PHP-файл не выполнится — Nginx отдаст его как статический текст. Тут можно хоть десять шеллов залить — толку ноль.
Рандомизация имени файла. Если сервер сохраняет файл как a3f8c1d2.jpg (UUID/хеш вместо оригинального имени), расширение всегда фиксировано. Двойные расширения, case variation и path traversal в имени файла теряют смысл.
Строгая валидация PIL/Pillow. Python-библиотека Pillow при вызове Image.open() полностью парсит структуру изображения. Файл с GIF89a в начале и PHP-кодом после — не пройдёт, потому что Pillow попытается прочитать GIF-фреймы и упадёт с ошибкой. Против Pillow фокус с текстовой сигнатурой не работает.
Этот чеклист — алгоритм при встрече с любым file upload таском. Распечатайте, повесьте рядом с монитором.
.test123) — определить blacklist или whitelist.phtml, .phar, .php5, .pht, .phps.pHp, .PhP, .PHPshell.php.jpg, shell.jpg.php, shell.php.test.jpgimage/jpeg или image/gifGIF89a для GIF, \xFF\xD8\xFF для JPEG../shell.php, ..%2fshell.php.htaccess с AddType application/x-httpd-php .jpg (только Apache)По моему опыту, шаги 2-6 закрывают большинство тасков уровня easy/medium. Шаги 7-11 — medium/hard. Шаг 12 — hard/insane, где file upload комбинируется с другими уязвимостями.
Файловая загрузка в CTF — та категория, где каждая следующая задача учит больше предыдущей. Но я заметил одну закономерность: большинство игроков застревают не на технике обхода, а на этапе разведки. Они сразу кидают shell.php и начинают перебирать расширения, вместо того чтобы потратить пять минут на анализ поведения сервера. Загрузил .test123 — получил ответ. Загрузил .jpg с мусором в теле — получил другой ответ. Два запроса — и уже понятно, какие слои валидации стоят. А дальше работа по таблице, не гадание.
Второй момент, который я вижу на форумах HTB снова и снова: люди не читают ответы сервера. Ошибка «only images allowed» — не то же самое, что «extension not allowed». Первая говорит о проверке содержимого или MIME, вторая — о фильтре расширений. Разные ошибки → разные техники обхода. Кажется очевидным, но половина тредов с просьбами о помощи начинается с «я попробовал всё, ничего не работает», а потом выясняется, что человек четыре часа фаззил расширения при проблеме на уровне magic bytes.
File upload — не rocket science. Это методичная работа: определи фильтры, подбери обход для каждого слоя, собери финальный payload. Кто это понял — решает такие таски за 15 минут. На WAPT эту цепочку разбирают в модулях по веб-эксплуатации с лабами на каждый тип фильтрации, и оттуда до OSCP-уровня остаётся меньше, чем кажется.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...