SQL Injection в форме входа
Обход аутентификации через SQL-инъекцию в форме логина.
easyphpPro
Задача
# SQL Injection: обход авторизации через форму входа
## Сценарий
Перед вами PHP-приложение интернет-магазина SecureShop. Стек — нативный PHP 8.3 на Apache, для хранения учётных записей и каталога используется SQLite. На странице входа стандартная форма с полями username/password — типовая авторизация, ничего необычного на первый взгляд.
Реализация выглядит безобидно, но имеет фундаментальную ошибку, которую часто допускают разработчики, привыкшие к PHP-стилю «склеить строку и отправить»: значения из формы попадают в SQL-запрос **через конкатенацию текста запроса**, без разделения данных и кода. Это даёт атакующему возможность управлять структурой запроса через содержимое полей формы — классический SQL Injection в auth-эндпоинте, входящий в OWASP Top 10 (A03:2021 — Injection) и описанный в CWE-89.
В таблице пользователей есть привилегированная учётная запись `admin` с балансом, достаточным для покупки товара «CTF Flag» в каталоге; флаг отображается на странице профиля владельца такого заказа после оформления покупки. Обычный пользователь `demo` с балансом 0 такой покупки совершить не может — только администратор.
## Цели
1. Подтвердите, что в форме входа значения полей попадают в SQL-запрос **как часть SQL-синтаксиса**, а не как данные. Простейший детектор — символ одинарной кавычки в username, ожидаемая реакция — серверная ошибка SQL или нестабильное поведение страницы входа.
2. Обойдите проверку пароля и войдите как `admin`, **не зная** его пароля. Используйте контролируемое вами значение поля username, чтобы изменить структуру SQL-запроса так, чтобы условие проверки пароля было отброшено парсером БД.
3. После входа из-под admin'а добавьте товар «CTF Flag» в корзину, оформите покупку, и заберите CTF-флаг со страницы профиля. Корзина и оформление заказа требуют CSRF-токена — он есть в HTML страницы каталога.
## Почему это типовая ошибка
PHP исторически популяризировал «строковые» интерфейсы к БД (старое расширение `mysql_*`, прямой `query()` в PDO), и поколения разработчиков привыкли формировать SQL через конкатенацию переменных в текст запроса. Параметризованные запросы появились в PHP относительно поздно (PDO с PHP 5.1), и до сих пор в legacy-проектах встречается смешение стилей: часть запросов параметризована, часть — нет. SecureShop здесь — реалистичный кейс: основная масса запросов в проекте уже использует prepared statements, и только функция логина была написана «по-быстрому» и осталась уязвимой.
## Уязвимый паттерн
Обработчик POST-запроса на форму входа собирает SQL-запрос **строковой конкатенацией**: значения `username` и `password` из тела формы оборачиваются в одинарные кавычки и склеиваются с шаблоном `SELECT ... FROM users WHERE username = '<...>' AND password = '<...>'`. Получившаяся строка отправляется в БД через простой `query()` — без подготовленного шаблона и без отдельной передачи значений как параметров.
Парсер БД получает уже готовую строку запроса и не отличает данные пользователя от управляющих конструкций SQL. Достаточно поместить в значение поля специальный символ (одинарную кавычку, комментарий, точку с запятой) — и атакующий перехватывает структуру запроса.
## Точка входа атаки
* **Эндпоинт:** `POST /login` с полями `username` и `password`.
* **Где появится флаг:** на странице `/profile` после покупки товара «CTF Flag», в виде строки `flag{...}` из переменной окружения `LAB_FLAG`.
## Данные
| Параметр | Значение |
|----------|----------|
| Обычный пользователь | `demo` / `demo` |
| Целевая учётная запись | `admin` (пароль не известен — его не нужно угадывать) |
| СУБД | SQLite (стандартный SQL-диалект, поддерживает `--` для однострочных комментариев) |
| Стоимость товара «CTF Flag» | 9999 — доступно только администратору с соответствующим балансом |