Timing-атака на форму входа (PHP)
Обработчик входа раскрывает существование пользователей через разницу во времени ответа — bcrypt вызывается только для существующих аккаунтов.
mediumphpPro
Задача
# Timing Attack: перечисление пользователей через различия во времени ответа
## Сценарий
Вы аудируете интернет-магазин **SecureShop**. У приложения есть форма логина с корректным сообщением об ошибке при неверных данных: «Invalid credentials» — одинаковый текст и для несуществующего пользователя, и для существующего с неверным паролем. Команда уверена, что защита от user enumeration реализована: атакующий не может по сообщению понять, существует ли учётка. По данным внутреннего аудита, в админ-панели хранится конфиденциальная метка, доступная пользователю с привилегиями admin; её требуется получить.
## Цель
1. Изучите поведение эндпоинта логина для разных сценариев: несуществующий пользователь, существующий с неверным паролем, существующий с верным паролем. Сравните не только текст ответа, но и поведение по другим осям (время, заголовки, статус).
2. Найдите боковой канал утечки информации, через который можно различить «несуществующий пользователь» от «существующий с неверным паролем» — даже при идентичном текстовом ответе.
3. Используя этот канал, подтвердите наличие admin-учётки и получите доступ к админ-панели; извлеките флаг.
## Теория
User enumeration через **timing side-channel** — класс уязвимостей, при которых атакующий определяет существование учётной записи по различиям в **физическом времени обработки** запроса, даже если все наблюдаемые ответы (статус, заголовки, тело) идентичны. Корень проблемы — в том, что путь обработки запроса проходит через дорогостоящие криптографические операции (проверка bcrypt/argon2-хеша пароля) **только когда пользователь существует**; если строки в БД нет — обработка завершается мгновенно.
**Уязвимый паттерн (PHP):**
```php
// Дорогостоящая операция (password_verify) выполняется только при найденном пользователе
$stmt = $pdo->prepare('SELECT id, password FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user) {
// ВЕТКА A: пользователь не найден — мгновенный возврат (~1 мс)
return $this->render('login.html', ['error' => 'Invalid credentials']);
}
// ВЕТКА B: password_verify тратит ~60-100 мс на bcrypt-сравнение
if (!password_verify($password, $user['password'])) {
return $this->render('login.html', ['error' => 'Invalid credentials']);
}
```
Функции семейства `password_verify` (bcrypt, Argon2) по дизайну вычислительно дороги — это нужно для замедления offline-перебора хешей. Но в timing-side-channel это становится явным сигналом: время ответа на «существующий с неверным паролем» отличается от «несуществующий» на порядки. Атакующий через серию замеров (`curl --write-out '%{time_total}'`, Python с `time.perf_counter()`) с разными username получает чёткий сигнал, какие учётки реально есть в системе. Дальше работают другие техники (брут-форс подобранных учёток, password-spray по полученному списку, social engineering на найденные email-адреса).
## Точка входа атаки
| Параметр | Значение |
|----------|----------|
| Учётные данные | `demo` / `demo` |
| Эндпоинт логина | `POST /login` (timing-side-channel в обработке) |
| Целевой пользователь | `admin` (имя из публичных конвенций) |
| Привилегированный эндпоинт | `GET /admin` (флаг отображается на странице) |