Перейти к содержимому
← Каталог php CSRF

CSRF: JSON API без кастомного заголовка (PHP)

JSON-endpoint /api/profile принимает state-changing запрос без проверки CSRF-токена и кастомного заголовка X-Requested-With. Простой fetch с Content-Type:text/plain даёт CSRF.

mediumphpPro
Задача
# CSRF: атака на JSON API ## Сценарий Перед вами интернет-магазин **SecureShop**. Команда разработки недавно мигрировала страницу настроек профиля с классических HTML-форм на современный JSON API: фронтенд отправляет POST-запросы с JSON-телом на `/api/profile`, бэкенд возвращает JSON-ответ. Разработчик уверен, что новый API **автоматически защищён от CSRF**, потому что HTML-формы физически не могут отправить `Content-Type: application/json` — а для произвольных типов содержимого браузер обязан выполнить CORS preflight, на который сервер атакующему не ответит разрешением. Это распространённое заблуждение, и ваша задача — продемонстрировать, в чём оно ошибочно. В системе есть административная учётная запись с CTF-флагом. Используя CSRF-уязвимость в JSON API смены пароля, заставьте браузер админа сменить пароль на известное вам значение — и войдите в админ-панель. ## Теория **CSRF (Cross-Site Request Forgery)** — атака, при которой злоумышленник заставляет браузер жертвы выполнить запрос на целевой сайт от её имени. Браузер автоматически прикрепляет сессионную cookie к любому исходящему запросу на тот же домен, **даже если запрос инициирован сторонней страницей**. Если сервер не требует никакого дополнительного «доказательства намерения» — CSRF-токена, проверки `Origin`/`Referer`, обязательного кастомного заголовка — любая открытая в браузере жертвы страница в интернете может выполнить от её имени state-changing операцию. Заблуждение разработчиков о «JSON-иммунитете» опирается на правила CORS. Действительно: для запроса с `Content-Type: application/json` браузер обязан выполнить preflight (`OPTIONS`), и без явного разрешения сервером cross-origin-вызов не пойдёт. Но это верно **только для `application/json`** в категории «non-simple». На практике обходится несколькими способами: * **HTML-форма с `enctype="text/plain"`** — собирает тело как `name=value` без URL-кодирования. При правильном подборе имени поля получается валидный JSON. Type `text/plain` входит в категорию «simple», preflight не запрашивается. * **`fetch(..., {mode: 'no-cors'})` с `Content-Type: text/plain`** — не вызывает CORS preflight, потому что считается «простым» запросом. * **`navigator.sendBeacon()` с `Blob`** — позволяет указать произвольный `Content-Type` (включая `text/plain`), при этом браузер прикрепляет cookies жертвы. Любой современный backend парсит тело как JSON независимо от заявленного `Content-Type`, если код вызывает `json_decode(file_get_contents('php://input'))` напрямую — что в этой лабе и происходит. **Уязвимый паттерн:** ```php function apiUpdateProfile(PDO $db): void { $user = getUser($db); if (!$user) { /* 401 */ return; } // VULN: нет проверки CSRF-токена, кастомного заголовка, Origin/Referer $raw = file_get_contents('php://input'); $data = json_decode($raw, true); // парсит JSON независимо от Content-Type if (!empty($data['password'])) { $hash = password_hash((string)$data['password'], PASSWORD_BCRYPT); $stmt = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?'); $stmt->execute([$hash, $user['id']]); } } ``` ## Цели 1. Авторизуйтесь как `demo` / `demo` и изучите страницу настроек профиля. Откройте DevTools, вкладка «Сеть» — посмотрите, как фронтенд общается с серверным API при сохранении изменений. 2. Найдите способ выполнить тот же запрос **с другого домена** — в обход «защиты», на которую полагается разработчик. 3. Доставьте подготовленную страницу-эксплойт администратору через предусмотренный для этого механизм отчётов. 4. После того как пароль администратора изменится, войдите от его имени и заберите CTF-флаг из админ-панели. ## Точка входа атаки | Параметр | Значение | |----------|----------| | Ваш аккаунт | `demo` / `demo` | | Уязвимый эндпоинт | `POST /api/profile` (JSON-тело) | | Поле атаки | `password` в JSON-теле | | Доставка боту | `POST /report` (бот залогинен как admin) | | Где появится флаг | `GET /admin` после входа под `admin` | | Exploit Server | внешний хостинг для HTML-payload (см. панель лабы) |
🚧 Сайт в разработке. Полный функционал пока недоступен. Все вопросы — support@hackandfix.ru