SQL Injection — обход WAF blacklist
WAF с blacklist-фильтром блокирует SQL-ключевые слова, но обходится через mixed case.
hardphpPro
Задача
# SQL Injection — обход WAF blacklist
## Сценарий
Перед вами PHP-приложение интернет-магазина SecureShop. Это нетипичная задача для SQLi: разработчик уже знает про класс проблемы и в поиске по каталогу добавил **дополнительный слой защиты** — самописный «WAF» на основе чёрного списка SQL-ключевых слов. Перед выполнением запроса параметр `q` проверяется на наличие `SELECT`, `UNION`, `DROP` и других опасных конструкций; если что-то из списка найдено, запрос отвергается с сообщением «Suspicious input blocked by WAF».
Идея «защитить уязвимый код фильтром опасных слов» — типичная для разработчиков, начинающих знакомиться с SQLi. На практике она не работает: количество способов выразить ту же SQL-конструкцию в обход конкретного blacklist-фильтра практически бесконечно — другой регистр, разделители-комментарии внутри ключевого слова, hex/url-кодирование, эквивалентные операторы. Любая фильтрация по чёрному списку проигрывает: достаточно одной щели — и payload проходит. И **сама исходная уязвимость никуда не исчезает** — за фильтром по-прежнему стоит обработчик, формирующий SQL-запрос через строковую конкатенацию; «WAF» лишь создаёт ложное чувство защищённости.
В таблице пользователей есть привилегированная учётная запись `admin` с балансом, достаточным для покупки товара «CTF Flag» в каталоге; флаг отображается на странице профиля владельца такого заказа после оформления покупки. Обычный пользователь `demo` с балансом 500 купить флаг не может — его задача обойти WAF и эксплуатировать SQLi так, чтобы получить заказ «CTF Flag», не имея денег.
## Класс уязвимости
SQL Injection через UNION-based extraction в сочетании со stacked-query insertion. SQL — регистронезависимый язык: ключевое слово в `SeLeCt`, `sElEcT` или `select` выполняется так же, как и `SELECT`. Если blacklist-фильтр сравнивает по точному совпадению с UPPERCASE-вариантом, любая смена регистра пропускает payload. Помимо смены регистра, классические техники обхода blacklist-фильтров:
- **Mixed case** — `uNiOn SeLeCt` вместо `UNION SELECT`
- **Inline-комментарии** — `UN/**/ION SE/**/LECT` (SQLite принимает `/* */` внутри идентификаторов)
- **Hex/URL-кодирование** — `%55NION` декодируется веб-сервером в `UNION` после фильтра
- **Альтернативные операторы** — `||` вместо `OR`, `LIKE` вместо `=`
Любая комбинация из вышеперечисленного делает атаку возможной, поскольку код за фильтром формирует SQL через конкатенацию.
## Уязвимый паттерн
```php
// «WAF» — регистрозависимая проверка по списку UPPERCASE-ключевых слов:
$sqlBlacklist = ['SELECT', 'UNION', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'EXEC'];
foreach ($sqlBlacklist as $keyword) {
if (strpos($input, $keyword) !== false) {
return false; // запрос блокирован, страница покажет «Suspicious input blocked by WAF»
}
}
// ...
// За фильтром — обычная конкатенация значения q в текст SQL-запроса:
$query = "SELECT id, name, description, price, image FROM products WHERE name LIKE '%" . $search . "%'";
$stmt = $db->query($query);
```
Здесь видны обе проблемы сразу. Первая — функция `strpos` сравнивает подстроку **с учётом регистра**, а SQL регистр игнорирует — значит варианты `uNiOn`, `union`, `UnIoN` проходят фильтр и при этом распознаются БД как ключевое слово `UNION`. Вторая — даже если бы фильтр был идеальным, за ним всё равно работает классическая конкатенация: значение `$search` склеивается с текстом SQL, и любой пропущенный фильтром payload автоматически получает SQL-инъекцию в чистом виде.
## Цели
1. Подтвердите наличие WAF: стандартный payload вида `' UNION SELECT 1,2,3,4,5 --` должен возвращать ошибку «Suspicious input blocked by WAF».
2. Найдите способ обхода фильтра. Внимательно посмотрите, **как именно** значения сравниваются с чёрным списком: учитывается ли регистр? Сравнивается ли подстрока или слово целиком? Любая обнаруженная щель открывает доступ к SQLi за фильтром.
3. Через UNION-инъекцию извлеките данные из таблицы `users` (имена пользователей, хеши паролей).
4. Используя более продвинутую технику (stacked queries), создайте для текущего пользователя `demo` запись в таблице `orders` с `product_name = 'CTF Flag'` — это сделает флаг видимым на странице профиля.
5. Заберите CTF-флаг со страницы `/profile`.
## Данные
| Параметр | Значение |
|----------|----------|
| Обычный пользователь | `demo` / `demo` |
| Точка входа | `GET /catalog?q=` |
| Endpoint флага | `GET /profile` (только при наличии заказа с `product_name = 'CTF Flag'`) |