File Upload: Content-Type Bypass
Загрузка файлов проверяет только расширение, но не содержимое. Атакующий может загрузить HTML/JS в файле с расширением .jpg.
mediumphpPro
Задача
# File Upload: обход проверки типа через MIME sniffing
## Сценарий
Интернет-магазин SecureShop позволяет авторизованным пользователям загружать в профиль изображения — стандартная функция «добавить фото для аватара». Команда разработки **добавила проверку формата** загружаемых файлов: явно перечислила, какие расширения изображений разрешены (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`), и сохранила контент в директорию `/uploads/`, доступную всем авторизованным пользователям по прямой ссылке.
Разработчики уверены, что фильтр расширений достаточен — «если расширение похоже на картинку, значит это картинка». Проверка реализована «дешёво»: смотрит только на имя файла, реальное содержимое в момент загрузки не валидируется. В дополнение к этому, при отдаче файлов из `/uploads/` сервер не выставляет заголовок, запрещающий браузеру самостоятельно определять тип содержимого, — рассчитывая, что `Content-Type` из ответа браузер примет на веру.
## Цель
1. Авторизуйтесь и найдите форму загрузки изображения в профиле.
2. Изучите проверки сервера: что именно валидируется — имя или содержимое? Какие подсказки даёт ответ при загрузке файла с «правильным» расширением, но необычным содержимым?
3. Сформируйте файл, который пройдёт фильтр, но при последующем обращении к нему браузер интерпретирует **не как изображение**.
4. Используйте полученный канал stored XSS на доверенном домене для извлечения CTF-флага.
## Теория
В PHP-приложениях контроль загруженных файлов опирается на два независимых свойства:
- **Расширение в имени файла** — строка, которая полностью контролируется клиентом. `pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION)` извлекает её из заголовка `Content-Disposition`, который атакующий может задать вручную.
- **Магические байты содержимого** — сигнатура в первых байтах файла (например, `FF D8 FF` для JPEG, `89 50 4E 47` для PNG). Эти байты определяет источник файла; клиент-фильтр в браузере на них не смотрит, но сервер может — через `finfo` или `mime_content_type`.
**Уязвимый PHP-паттерн (проверка только расширения):**
```php
// VULNERABILITY: фильтр опирается на клиентское имя файла
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
// отклоняем
}
// сохраняем как есть, содержимое не валидируется
move_uploaded_file($_FILES['file']['tmp_name'], $uploadDir . $storedName);
```
Когда сервер ещё и **не отправляет** заголовок `X-Content-Type-Options: nosniff` при отдаче файла обратно, браузер применяет механизм **MIME sniffing** — анализирует начальные байты тела ответа, замечает разметку `<html>`/`<script>` и переключается из режима «картинка» в режим «HTML-страница», игнорируя присланный `Content-Type: image/jpeg`. Полезная нагрузка, лежащая внутри «изображения», исполняется в контексте домена приложения как обычный HTML — это и есть stored XSS на канале загрузки.
Класс уязвимости — Unrestricted File Upload (CWE-434), частный случай Content-Type Bypass / MIME Type Confusion.
### Таблица типичных обходов фильтра расширений
| Техника обхода | Идея | Когда сработает |
|----------------|------|-----------------|
| Спуфинг расширения | HTML-файл сохранён с именем `evil.jpg` | Сервер проверяет только расширение |
| Magic bytes spoofing | Перед HTML вставлен валидный заголовок JPEG (`FFD8FF`) | Сервер использует `getimagesize` (которая смотрит только заголовок) |
| Polyglot файл | Файл одновременно валидный JPEG и валидный HTML | Содержимое не пересохраняется, валидация только верхнего слоя |
| SVG с `<script>` | Векторное изображение с JS-кодом | Сервер принимает `image/svg+xml` и отдаёт inline |
| Двойное расширение | `evil.php.jpg` | Apache `AddHandler` обрабатывает по любому совпадению в имени |
| Null-byte в имени | `evil.php%00.jpg` | Старые PHP-версии до 5.3.4 (для контекста) |
В этой лабе срабатывает первый сценарий — спуфинг расширения с MIME sniffing'ом на стороне браузера.
## Точка входа атаки
| Параметр | Значение |
|----------|----------|
| Учётные данные обычного пользователя | `demo` / `demo` |
| Учётные данные привилегированного пользователя (моделирование результата cookie theft) | `admin` / `SuperSecret` |
| Форма загрузки | страница `/profile`, поле `file`, multipart/form-data |
| Прямая ссылка на загруженный файл | путь вида `/uploads/{hash}.{ext}` |
| Источник флага | переменная окружения `LAB_FLAG`, выводится в карточке «CTF Flag Acquired!» в профиле после покупки CTF Flag в каталоге |