File Upload: Path Traversal через имя файла
Загрузка аватара использует оригинальное имя файла без санитизации. Подмени filename в multipart-запросе для перезаписи шаблона и получения RCE.
easyphpPro
Задача
# File Upload: подмена имени файла как канал записи произвольного пути
## Сценарий
Интернет-магазин SecureShop позволяет авторизованным пользователям загружать собственный аватар на странице профиля. Загруженные файлы кладутся в директорию `/static/uploads/` внутри корня приложения, ссылка на аватар сохраняется в базе данных и подставляется в шаблон профиля.
Команда разработки реализовала загрузку аватаров «в лоб»: серверный код получает файл из multipart-запроса и сохраняет его на диск, используя имя, пришедшее в заголовке `Content-Disposition`. Никакой защиты от путевых компонент в имени файла нет — разработчики посчитали, что браузер «всё равно отправит только базовое имя». По данным внутреннего аудита, на сервере хранится конфиденциальная метка в переменной окружения `LAB_FLAG`, которую можно прочитать из PHP-кода через `getenv("LAB_FLAG")`; ваша задача — её извлечь.
## Цель
1. Авторизуйтесь и откройте страницу профиля. Изучите multipart-поток, отправляемый формой загрузки аватара — какие части запроса полностью контролируются клиентом?
2. Найдите способ направить сохранение файла за пределы директории статики — в место, где PHP-интерпретатор подхватит загруженный контент при HTTP-запросе или при рендеринге следующих запросов.
3. Используйте этот канал, чтобы получить значение `LAB_FLAG` через серверное исполнение PHP.
## Теория
Multipart-запрос включает заголовки для каждой части — в том числе `Content-Disposition` с полем `filename`, в которое браузер по умолчанию помещает оригинальное имя файла, выбранного пользователем. Это полностью **клиентский** ввод: атакующий, отправляющий запрос вручную (через curl, Burp, Python `requests`), может задать там произвольную строку — включая последовательности path traversal (`../`, абсолютные пути, кодированные эквиваленты).
Начиная с PHP 8.1, помимо `$_FILES['avatar']['name']` (которое в исторических версиях обрезалось через `basename` на стороне интерпретатора) появилось дополнительное поле `$_FILES['avatar']['full_path']`. Оно хранит **полный путь**, переданный клиентом в `Content-Disposition`, **со всеми компонентами обхода каталога** — без какой-либо нормализации. Это поле было добавлено для поддержки сценария «загрузка папки» через `<input type="file" webkitdirectory>`, но в неосторожном коде превращается в прямой канал записи произвольного пути.
**Уязвимый PHP-паттерн:**
```php
// VULNERABILITY: full_path берётся напрямую из заголовка Content-Disposition,
// сохраняя любые ../ из multipart-запроса
$filename = $_FILES['avatar']['full_path'] ?? $_FILES['avatar']['name'];
$uploadDir = __DIR__ . '/../../static/uploads/';
$destPath = $uploadDir . $filename;
// Дополнительно: код самостоятельно создаёт промежуточные директории,
// снимая последнее препятствие к записи в произвольное место
$destDir = dirname($destPath);
if (!is_dir($destDir)) {
mkdir($destDir, 0775, true);
}
move_uploaded_file($_FILES['avatar']['tmp_name'], $destPath);
```
Самый «вкусный» класс целей для записи — файлы, которые приложение **читает и интерпретирует** при последующих запросах: PHP-шаблоны (`templates/*.php`, рендерятся обработчиками страниц), скрипты, лежащие в директориях, которые Apache отдаёт через PHP-интерпретатор, конфиги (если читаются на лету). PHP не кэширует исходники по умолчанию (opcache в dev-окружении отключён), поэтому перезаписанный файл подхватывается на следующем же HTTP-запросе.
### Таблица типичных целей для записи
| Целевой путь | Триггер исполнения | Эффект |
|--------------|-------------------|--------|
| `templates/404.php` | любой запрос на несуществующий URL → `handleNotFound` рендерит шаблон | гарантированный |
| `templates/profile.php` | `GET /profile` (страница профиля авторизованного пользователя) | RCE при логине |
| `cmd/shell.php` | прямой `GET /cmd/shell.php` если директория отдаётся через Apache | зависит от DocumentRoot |
| `static/uploads/shell.php` | прямой `GET /static/uploads/shell.php` | работает если PHP включён в /static |
| `internal/handlers/handlers.php` | следующий любой запрос (обработчик берётся из этого файла) | полный takeover |
В этой лабе типичная цель — перезапись `templates/404.php` или `templates/profile.php`: триггер исполнения легко спровоцировать без знания внутренней маршрутизации, а сами шаблоны гарантированно идут через PHP-интерпретатор.
## Точка входа атаки
| Параметр | Значение |
|----------|----------|
| Учётные данные | `demo` / `demo` |
| Форма загрузки аватара | страница профиля, поле `avatar`, multipart/form-data |
| Уязвимый канал | заголовок `Content-Disposition: ...; filename="..."` части `avatar` (PHP 8.1+: `full_path`) |
| Источник флага | переменная окружения `LAB_FLAG`, доступная через `getenv("LAB_FLAG")` из любого PHP-кода |
| Финальная цель | вывести значение `LAB_FLAG` в HTTP-ответ через серверное исполнение загруженного PHP-файла |