
Привет, Хабр!
На связи Олег Уланов (aka brain), багхантер и ведущий подкаста «Начинаем в багбаунти». Кстати, по итогам 2025 года стал топ-1 площадки Standoff Bug Bounty.
Этот материал мы подготовили вместе с Дмитрием Прохоровым, пентестером из PT SWARM (в мире багхантинга Дима известен как ratel_xx). Речь пойдёт о поиске уязвимостей через загрузку файлов.
Вы узнаете, как устроен механизм multipart/form-data, какие защитные механизмы ставят разработчики и как их обходить. Я покажу на практике, что можно сделать с расширениями файлов, Content-Type, magic bytes, а заодно затрону эксплойты типа race condition, zip-слайп и нестандартные векторы вроде загрузки .htaccess. Статья подойдёт для начинающих багхантеров, поэтому даже если у вас мало опыта - смело читайте дальше!
Статья носит исключительно информационный и образовательный характер и не является инструкцией или призывом к совершению противоправных действий. Описанные материалы предназначены для повышения осведомленности о возможных уязвимостях и методах их предотвращения. Любое тестирование безопасности допускается только при наличии явного разрешения правообладателя соответствующих ресурсов или в рамках официальной программы багбаунти. Несанкционированные действия могут нарушать законодательство. Автор статьи не поощряет и не поддерживает неправомерное использование представленной информации и не несет ответственности за ее использование в противоправных целях. Помните, что нужно уделять внимание защите своих данных и использовать информацию из статьи исключительно в законных целях.
Если вы делаете первые шаги в багхантинге, советуем посмотреть, как стартовали ребята из сообщества Standoff Bug Bounty.
👾 Для тех, кто уже подбирается к наградам, будет полезна статья про поиск уязвимостей SSRF с кучей ссылок на полезные ресурсы и инструменты.
👾 Отдельно рекомендуем почитать про Broken Access Control ‑ один из самых распространенных типов уязвимостей.
👾 А также не проходите мимо топ-10 инструментов для старта в багбаунти.
Почему file upload — это баунти-магнит
Формы загрузки файлов встречается повсюду. Обратная связь, импорт данных, загрузка финансовых документов, аватары пользователей — все это потенциальные точки входа. Где есть загрузка, там есть и возможности для атакующего.
И дело не только в распространенности, но и в потенциале — file upload часто ведет к критическим последствиям. Удаленное исполнение кода (RCE), path traversal для перезаписи конфигурационных файлов, отказ в обслуживании (DoS) — все это делает загрузку особенно привлекательной для багхантеров.
Вот пример массовой уязвимости в популярном плагине WordPress — одна ошибка ставила под угрозу тысячи сайтов.

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

В топ-отчетах HackerOne можно найти примеры, где обходы были до смешного простыми. Например, достаточно было добавить пробел в расширение файла, и защита падала. О таких техниках мы обязательно поговорим при разборе базовых техник, а пока начнем с теории.
Как устроена загрузка файлов
Чтобы понимать, где искать уязвимости, нужно сначала разобраться, как вообще происходит загрузка файлов с точки зрения протокола и стандартов. Рассмотрим механизм multipart/form-data, на котором она построена, заглянем в RFC 7578 и посмотрим, на что именно обращает внимание сервер, когда получает файл.
Механика multipart/form-data
Когда вы загружаете файл через веб-форму, браузер отправляет на сервер запрос в формате multipart/form-data. Этот механизм позволяет передавать в одном запросе и текстовые поля, и бинарные данные — те самые файлы. В запросе есть скрытое поле с именем формы и сам файл. Клиентскому коду доверять нельзя, поэтому проверять нужно именно запрос, отправляемый на сервер.
Однако разработчики часто полагаются на стандартные библиотеки и не задумываются, как именно парсится этот запрос. А в реализации парсеров бывают расхождения со стандартом. Именно эти расхождения, например, неправильная обработка границ или игнорирование завершающих символов, приводят к уязвимостям. Понимая структуру multipart, вы сможете находить такие ошибки там, где другие видят обычную форму загрузки.

Чтобы увидеть сырой запрос и понять, что именно улетает на сервер, используют прокси-инструменты. Самый популярный в багхантинге — Burp Suite. Он перехватывает трафик между браузером и сервером, показывает структуру запроса и позволяет изменять любые его части прямо на лету.
Стандарт RFC 7578
Формат multipart/form-data для загрузки файлов описан в стандарте RFC 7578. В этом документе прописаны правила формирования границ (boundary), заголовков и тела запроса. Границы — уникальные разделители, которые генерируются на стороне клиента и не должны повторяться внутри содержимого. Сервер ищет эти границы, чтобы понять, где начинается и заканчивается каждая часть формы.
Казалось бы, это чисто техническая деталь, но именно отступления от RFC — благодатная почва для уязвимостей. Один разработчик использует библиотеку, которая строго следует стандарту. Другой пишет свой парсер и режет углы. Третий вообще не подозревает, что его коллега сделал велосипед. В результате разные компоненты системы по-разному интерпретируют один и тот же запрос.
Классический пример — уязвимость в популярной Node.js-библиотеке Multer. Парсер ожидал, что последняя граница в запросе будет завершаться двумя дефисами, как того требует RFC. Если дефисов не было, сервер уходил в бесконечное ожидание следующей части. Одна лишняя или недостающая пара символов — и DoS готов.

В 2025 году вышло большое исследование, где авторы мутировали границы, заголовки и структуру multipart-запросов, не трогая сам пейлоад. В результате они нашли больше тысячи способов обойти AWS, Cloudflare, Google Cloud Armor, Azure и ModSecurity. Проблема глубже, чем кажется: парсеры WAF и веб-приложений расходятся в мелочах, а превратить это в атаку можно почти всегда.
Куда смотрит сервер
Итак, мы разобрались, как устроен multipart-запрос и почему стандарт важен. Теперь посмотрим, на что именно сервер обращает внимание, когда получает файл.
Сервер обращает внимание на три вещи.
Имя файла (filename) — из него сервер берет расширение, чтобы понять, с каким типом файла он имеет дело. Если разработчик проверяет только расширение, это уже повод присмотреться..
Content-Type — MIME-тип, который пришел от клиента. Беда в том, что он генерируется на стороне браузера и легко подменяется через прокси.
Тело файла, а точнее его начало — здесь находятся так называемые magic bytes — уникальные байтовые сигнатуры, которые определяют реальный тип содержимого независимо от расширения. Например, у PNG-файла первые байты всегда 89 50 4E 47 0D 0A 1A 0A. Если разработчик доверяет только заголовкам и расширениям, но не проверяет сигнатуры, это прямой путь к обходу.

Базовые атаки: как обходят простую защиту
Когда мы попадаем на форму загрузки файлов, первое, что делаем, — пытаемся загрузить что-то вредоносное: PHP-шелл, JavaScript для XSS, SVG с внедренным кодом. В ответ разработчики ставят защитные механизмы. Основные приемы, которые помогают их обойти: перебор расширений (.php5, .phtml, .phps), спецсимволы (пробелы, точки, null-байт), подмена регистра (на Linux), особенности нормализации имен (на Windows), подмена Content-Type, внедрение magic bytes, склейка файлов и использование двойных расширений.
Начнем с самого простого сценария, когда на сервере нет никакой защиты — любые файлы принимаются и сохраняются в веб-доступной директории. Подготавливаем простой шелл с вызовом команды id, загружаем обычную картинку, перехватываем запрос в Burp и смотрим его структуру. В теле файла вместо картинки вставляем наш PHP-код, меняем расширение на .php. Сервер отвечает, что файл сохранен. Переходим по ссылке — вместо картинки видим результат выполнения команды.
Загрузка PHP-шелла на незащищенный сервер (начало 21:51):
Конечно, в реальных программах такой простой сценарий встречается редко. Чаще разработчики что-то проверяют: запрещают определенные расширения, смотрят на Content-Type или даже на содержимое файла. И здесь в игру вступают чуть более сложные техники.
Одна из самых интересных — обход через magic bytes. Сервер принимает только картинки и смотрит на первые байты файла. Подмена Content-Type не помогает — нужно, чтобы файл начинался с правильной сигнатуры. Для таких случаев в Burp Suite есть расширение Magic Bytes Selector. Оно позволяет одним кликом подставить в начало файла сигнатуру нужного формата — GIF, PNG, JPEG и других.
Выбираем сигнатуру GIF, плагин добавляет нужные байты в начало запроса. Поверх них вставляем PHP-код. Сервер видит начало файла как GIF и пропускает загрузку. Обращаемся к файлу — PHP-код в конце выполняется.
Внедрение magic bytes через Burp (начало 41:46)
Если хотите увидеть больше реальных примеров обхода — от простых до неочевидных — посмотрите подборку техник с примерами из багбаунти на Briskinfosec.
Ты не пройдешь — продвинутая практика
Мы рассмотрели базовые приемы, но настоящие баги часто прячутся глубже. Чтобы их найти, нужно понимать, как загруженный файл обрабатывается после сохранения — и тут на сцену выходят особенности серверов, языков программирования и сторонних библиотек. Прежде чем перейти к конкретным техникам, давайте добавим немного теории.
Особенности обработки файлов — языки программирования, серверы и дыры в конфигах
Когда мы говорим о file upload, недостаточно думать только о расширениях и magic bytes. Загруженный файл попадает в экосистему — веб-сервер, язык программирования, фреймворк, сторонние библиотеки. Каждый из этих компонентов добавляет свои нюансы, а иногда и готовые векторы для атаки.
Веб-серверы: Apache
Главный риск в Apache связан с файлом .htaccess. Если сервер разрешает его загрузку в папку с пользовательскими файлами, атакующий может загрузить свой .htaccess с директивой AddHandler, которая заставит Apache исполнять любые файлы (например, с расширением .evil) как PHP.
![Запрос в Burp на загрузку .htaccess с содержимым AddType application/x-httpd-php .l33t] Запрос в Burp на загрузку .htaccess с содержимым AddType application/x-httpd-php .l33t]](https://habrastorage.org/r/w1560/getpro/habr/upload_files/2c0/59c/90d/2c059c90dadd03aff1b1467b22e23b85.png)
Кроме того, Apache иногда настраивают на обработку файлов, содержащих в имени подстроку .php, — тогда shell.php.jpg станет исполняемым, даже если расширение формально .jpg.
Веб-серверы: Nginx + PHP-FPM
В связке nginx и PHP-FPM классическая ошибка — директива cgi.fix_pathinfo=1. При такой конфигурации запрос к /uploads/shell.png/shell.php заставит PHP-FPM открыть файл shell.png и выполнить PHP-код внутри него, потому что nginx отбрасывает часть пути после слэша. Это не баг самого PHP, а особенность обработки пути, которую часто забывают учитывать. О том, как далеко можно зайти с этой особенностью и превратить LFI в RCE через временные файлы Nginx, хорошо написано в статье PHP LFI with Nginx Assistance.
PHP
Самый очевидный вектор — просто положить .php-файл в доступную веб-директорию. Но есть и более сложные техники. Wrappers вроде php://filter и phar:// позволяют исполнить код внутри загруженных архивов или даже картинок, если в коде есть уязвимое include. А если разработчик использует include для подгрузки файлов, легальная картинка с внедренным PHP-кодом может превратиться в полноценный шелл — достаточно, чтобы include обработал загруженный файл.
Python и Node.js
Прямое исполнение загруженных файлов здесь встречается реже, но это не значит, что они безопасны. Чаще ищут path traversal, чтобы перезаписать конфигурационные файлы или файлы скриптов самого приложения. А если приложение использует шаблонизаторы (Jinja для Python, Handlebars для Node.js), загрузка файла-шаблона может привести к SSTI — серверной инъекции шаблонов, которая часто дает RCE.
Java
Основные векторы — загрузка WAR-архивов или JSP-файлов в директории, которые обрабатываются сервлет-контейнером. Если удастся загрузить malicious.war в папку деплоя или перезаписать конфигурацию через path traversal, можно получить полный контроль над приложением. Такие атаки сложнее в исполнении, но и последствия от них серьезнее.
Продвинутые техники
Мы разобрали основные стеки и их особенности, теперь перейдем к конкретным атакам, которые используют эти знания на практике. Некоторые из них требуют стечения обстоятельств, другие — нестандартного подхода, но каждая способна привести к полной компрометации сервера.
.htaccess маленький файл с большими последствиями
Если сервер позволяет загружать .htaccess, можно переписать правила обработки. Загружаем файл с директивой AddHandler application/x-httpd-php .evil. Теперь любой файл с расширением .evil будет исполняться как PHP. Загружаем shell.evil — получаем RCE.
Демонстрация (начало 59:54):
Race condition: угнать за ~0 секунд
Иногда разработчик сначала сохраняет файл, а потом проверяет — не вредоносный ли он. Если проверка не пройдена, файл удаляется. Между сохранением и удалением есть микросекунда, когда файл доступен по HTTP. Если успеть обратиться к нему в это окно, код исполнится. На практике это сложно: нужно угадать имя файла, обойти ограничения по частоте запросов, а иногда и арендовать VPS поближе к серверу. Сама техника достойна отдельной статьи.
Демонстрация (начало 1:01:48):
Если хотите попробовать сами, рекомендую лабораторию Web Security Academy на тему загрузки шелла через race condition.
Zip Slip — path traversal через архивы
Сервер принимает ZIP-архивы, распаковывает их и сохраняет содержимое. Если разработчик не проверяет имена файлов внутри архива, мы можем указать путь с обходом директорий. Создаем ZIP, где файл shell.php лежит по пути ../../../../var/www/html/uploads/shell.php. При распаковке система проходит по всем ../ и кладет файл в целевую директорию. Уязвимость применима не только к ZIP, но и к любым архивным форматам: DOCX, XLSX, JAR, APK — везде, где сервер распаковывает контент.
Демонстрация (начало 1:06:52):
Если хотите глубже разобраться в механике Zip Slip и посмотреть примеры кода, начните с подробной статьи на Snyk. Там объясняется, как именно формируется вредоносный архив и почему эта уязвимость до сих пор встречается в реальных проектах.
Обработка метаданных: CVE-2016-3714 (ImageTragick)
Старая, но показательная уязвимость в ImageMagick. Библиотека пыталась выполнить команду curl для загрузки файла по URL, причем команда собиралась через вызов shell. Внедрение двойной кавычки и точки с запятой позволяло выполнить произвольную команду.
Создаем файл, который начинается с PNG-сигнатуры, а затем содержит строку с вызовом команды id и записью результата в файл в веб-директории. Загружаем — ImageMagick пытается выполнить curl, ломается, но команда id отрабатывает, и результат оказывается доступен по HTTP.
Демонстрация (начало 1:10:23):
Что дальше
Мы разобрали основные техники, но file upload на этом не заканчивается. Каждый язык и фреймворк добавляет свои нюансы. Java, Python, Node.js — везде есть специфические векторы, связанные с конфигурацией, обработкой архивов и шаблонизаторов. Общий принцип остаётся тем же: валидация должна быть комплексной. Расширение, Content-Type, magic bytes, структура файла, место сохранения — все это звенья одной цепи. Если хотя бы одно ослабло, это может привести к уязвимости.
Итоги
Может показаться, что уязвимости загрузки файлов — это что-то простое и давно изученное. Но на практике они встречаются повсюду, от небольших сервисов до крупных корпораций, и ведут к самым серьезным последствиям: RCE, утечке данных, полной компрометации сервера.
Немаловажно и то, что искать их выгодно — за один критический баг можно получить приличное вознаграждение, а массовые уязвимости в популярных плагинах приносят баунти даже после публичного раскрытия.
Напоследок делюсь полезными ссылками по теме:
Лаборатории Web Security Academy от PortSwigger — лучший способ набить руку. В разделе про file upload есть несколько практических заданий разного уровня сложности.
Burp Suite с расширениями (Autorize, Turbo Intruder, Magic Bytes Selector).
Списки расширений для перебора — есть в SecLists.
Исследования по файловым уязвимостям — ссылки на них были по ходу статьи.
Всем удачных находок. И помните: даже если форма выглядит просто, в ней может скрываться неожиданный вектор.
