Second-Order SQL Injection через профиль пользователя
Имя из БД подставляется в SQL через конкатенацию — Second-Order SQLi.
hardphpPro
Задача
# Second-Order SQL Injection через профиль пользователя
## Сценарий
Вы исследуете интернет-магазин SecureShop. Магазин поддерживает регистрацию, авторизацию, корзину и каталог товаров. На странице профиля пользователя приложение отображает дополнительную секцию «Similar Users» — список пользователей с похожим полным именем, чтобы создать ощущение сообщества и помочь найти друзей.
Команда разработки выполнила базовое ревью безопасности. Регистрация и логин используют параметризованные запросы — поиск инъекций на этих формах ничего не даст: специальные символы корректно сохраняются как обычная строка, INSERT защищён, а проверка пароля идёт через `password_verify()`. На первый взгляд приложение защищено.
Однако в коде есть ошибка, которая является классическим примером Second-Order SQL Injection. Разработчик исходит из предположения, что данные, однажды прошедшие через параметризованный запрос при записи, становятся «доверенными» и могут безопасно использоваться в других местах. Это опасное заблуждение.
## Что такое Second-Order SQL Injection
Обычная (first-order) SQL-инъекция выполняется сразу — пользователь отправляет вредоносный ввод, и тот немедленно интерпретируется как SQL. Прямые формы поиска, login bypass через одинарную кавычку с тавтологическим условием — это first-order.
Second-Order SQL Injection (известна также как **stored** или **persisted SQLi**) работает в два этапа:
1. **Сохранение payload.** Атакующий отправляет SQL-payload через любое поле, которое корректно сохраняется в базу через параметризованный запрос. На этом этапе атака «спит» — никакой инъекции ещё не происходит, payload лежит в БД как обычная строка.
2. **Активация payload.** В другом месте приложения данные читаются из БД и подставляются в новый SQL-запрос через конкатенацию. Разработчик считает, что раз данные пришли «из своей БД», они безопасны, и пропускает параметризацию. На этом этапе сохранённый payload интерпретируется как SQL и инъекция срабатывает.
Этот паттерн опасен тем, что:
- автоматические сканеры часто пропускают такие уязвимости — они не срабатывают на месте ввода;
- code review при беглом просмотре функции записи (`register`) не находит ничего подозрительного;
- даже опытные разработчики ошибочно делят данные на «доверенные» (из БД) и «недоверенные» (от пользователя).
## Уязвимый паттерн
В лабе живут одновременно две функции: безопасная (запись fullname через prepared statement) и уязвимая (чтение fullname и подстановка в новый SQL через конкатенацию).
```php
// БЕЗОПАСНО: INSERT через prepared statement — payload сохраняется как обычная строка
$stmt = $db->prepare(
"INSERT INTO users (username, password, role, fullname, balance, avatar_url)
VALUES (?, ?, 'user', ?, 0, '/static/img/avatar-default.svg')"
);
$stmt->execute([$username, $hash, $fullname]);
```
```php
// УЯЗВИМО: чтение fullname из БД и подстановка в новый SQL-запрос через конкатенацию.
// Разработчик ошибочно считает данные «из своей БД» доверенными и пропускает параметризацию.
$fullname = $user['fullname'];
$sql = "SELECT username, fullname FROM users WHERE fullname LIKE '%" . $fullname . "%' AND id != " . (int)$user['id'];
$similarUsers = $db->query($sql)->fetchAll();
```
Payload, переданный через форму регистрации в поле fullname, **сохраняется** в таблице `users` как обычная строка. На этапе записи атака «спит». Но когда обработчик профиля читает этот же fullname обратно и подставляет его в SQL через конкатенацию — парсер БД интерпретирует кавычки и ключевые слова в значении как SQL-синтаксис. Получается двухтактная атака: store → trigger.
## Цели
1. Зарегистрировать нового пользователя с SQL-payload, спрятанным в поле «Full Name» формы регистрации.
2. Войти под этим пользователем и открыть страницу профиля.
3. Спровоцировать срабатывание payload в секции «Similar Users» страницы профиля.
4. Извлечь чувствительные данные (логин и bcrypt-хэш пароля администратора), используя их далее для авторизации как admin.
5. Получить флаг через cabinet админа.
## Условия
| Параметр | Значение |
|----------|----------|
| Точка входа | Регистрация — поле Full Name (`/register`) |
| Точка срабатывания | Страница профиля, секция Similar Users (`/profile`) |
| Обычный пользователь | `demo` / `demo` (опционально для разведки) |
| Тип флага | Содержится в env-переменной `LAB_FLAG`, выдаётся на странице профиля админа после покупки товара «CTF Flag» |
| Стек | PHP 8.3 + Apache + SQLite (PDO) |
## Что важно понять
- INSERT в `register` использует prepared statement и **не** уязвим — нет смысла пытаться сломать регистрацию.
- Login через `password_verify()` тоже безопасен — bcrypt-сравнение нельзя обмануть SQL-инъекцией.
- Уязвимость находится **только** в чтении данных из БД и подстановке их в новый SQL-запрос без параметризации.
- Разведка: посмотрите, какие данные отображаются на странице профиля кроме базовой информации о пользователе. Что нового появляется на этой странице по сравнению с другими?