Перейти к содержимому

SQL-инъекция второго порядка: ручная эксплуатация через Burp Repeater

SQL-инъекция второго порядка: ручная эксплуатация через Burp Repeater
Время чтения: 14 мин.

На пентесте веб-приложения с функцией экспорта отчётов sqlmap просканировал все параметры и вернул ноль находок. Через два дня ручного ковыряния в Burp Repeater нашлась SQL-инъекция второго порядка: payload из поля с датой сохранялся при создании задания на отчёт, а срабатывал при скачивании готового Excel через отдельный API-эндпоинт. Два HTTP-запроса, один уязвимый параметр — автоматический сканер промолчал, потому что не умеет связывать точку ввода с точкой срабатывания. Подобный сценарий подробно описан в исследовании NetSPI: отложенный SQL payload в механизме Excel-экспорта, обнаруженный только ручным тестированием.

Что такое SQL-инъекция второго порядка и чем она отличается от обычной

SQL-инъекция (SQLi) — атака, при которой вредоносный SQL-код попадает в запрос к базе данных через пользовательский ввод. Классическая инъекция работает мгновенно: отправили payload (полезная нагрузка — фрагмент SQL-кода, внедрённый в параметр) → получили результат в том же HTTP-ответе. Корневая причина описана в CWE-89 (Improper Neutralization of Special Elements used in an SQL Command): продукт конструирует SQL-запрос из внешних данных, не нейтрализуя спецсимволы. По классификации OWASP это A03:2021 — Injection.

SQL-инъекция второго порядка (second order SQL injection, она же stored SQL injection) устроена иначе. Атака разбита на два этапа:

  1. Сохранение. Приложение принимает пользовательский ввод, возможно экранирует его и кладёт в базу. Payload на этом этапе безвреден — лежит в таблице как обычная строка.

  2. Срабатывание. Другая часть приложения — часто совсем другой эндпоинт — достаёт это значение из базы и подставляет в новый SQL-запрос без повторной санитизации (очистки опасных символов). Здесь payload исполняется.

Аналогия из жизни: вы оставляете записку с текстом '; DROP TABLE users;-- в ящике входящей корреспонденции. Секретарь аккуратно подшивает её в архив — ничего не происходит. Через неделю бухгалтер берёт текст из архива и вставляет в финансовый отчёт через конкатенацию строк. «Бомба» срабатывает не в момент закладки, а при повторном использовании.

PortSwigger формулирует это так: «second-order SQL injection arises when user-supplied data is stored by the application and later incorporated into SQL queries in an unsafe way». Именно двухэтапность делает отложенный SQL payload невидимым для большинства автоматических сканеров.

Если вы только начинаете разбираться в классификациях атак — вот контекст. MITRE ATT&CK — это открытая база тактик и техник, которыми пользуются атакующие. Каждой технике присвоен T-код. SQL-инъекция через публичное приложение — это T1190 (Exploit Public-Facing Application, тактика Initial Access). Грубо говоря, это «как злоумышленник попадает внутрь». При эскалации через хранимые процедуры подключается T1505.001 (SQL Stored Procedures, тактика Persistence) — атакующий закрепляется в системе. Чтение данных из БД — T1213.006 (Databases, тактика Collection). Ещё один связанный CWE — CWE-116 (Improper Encoding or Escaping of Output): данные, корректно закодированные при записи, при повторном чтении из базы возвращаются в исходную форму, и экранирование перестаёт работать.

Где встречается двухэтапная SQL-инъекция

Stored SQL injection обнаруживается в функциях, где данные сохраняются в одном контексте и используются в другом:

  • Регистрация → смена пароля. Вредоносный username сохраняется при регистрации и попадает в UPDATE users SET password='new' WHERE username='<значение из БД>'.
  • Экспорт отчётов. Параметр из формы сохраняется как задание, при формировании файла подставляется в SELECT без санитизации — этот сценарий описан в исследовании NetSPI с функцией Excel-экспорта.
  • Комментарии и отзывы. Текст вставляется через параметризованный INSERT, но на административной панели выводится через динамический SELECT с конкатенацией строк.
  • Корзина → счёт-фактура. Название товара, изменённое через уязвимость в CMS, попадает в запрос генерации счёта.

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

Stored SQL injection — не изолированная техника, а звено в цепочке. Типичная последовательность:

Разведка (определение функций с отложенной обработкой данных) → Initial Access через T1190 (Exploit Public-Facing Application) → Collection через T1213.006 (чтение таблиц, паролей, ключей) → при доступе к хранимым процедурам SQL Server — Persistence через T1505.001 (SQL Stored Procedures, например xp_cmdshell, xp_dirtree).

Перед эксплуатацией нужно найти две точки: где данные сохраняются и где используются повторно. Это ключевое отличие от inline-SQLi — тут недостаточно перебирать параметры сканером, нужно понимать бизнес-логику приложения.

Контекст применимости: техника актуальна и для внешнего пентеста (публичные веб-приложения), и для внутреннего (корпоративные порталы, ERP). Чаще встречается в legacy-приложениях на PHP/ASP.NET с ручной конкатенацией SQL и в проектах, где разные команды разработчиков отвечают за разные модули. Именно на стыке модулей — когда один разработчик аккуратно экранирует данные при записи, а другой достаёт их из базы и считает «доверенными» — и рождается second order SQLi.

Ограничения sqlmap при эксплуатации stored SQL injection

sqlmap — инструмент автоматизации обнаружения и эксплуатации SQL-инъекций (открытый код, Python, активно поддерживается). Его архитектура заточена под inline-инъекции: отправить payload → проанализировать ответ. Ключ --second-url позволяет указать URL для проверки результата, но на практике ограничений больше, чем возможностей.

Критерий sqlmap (--second-url) Ручная эксплуатация (Burp Repeater)
Преимущества Автоматический перебор техник и СУБД Полный контроль над каждым запросом, адаптация к любой логике
Ограничения Не обрабатывает JSON API; не перехватывает UUID/токены между запросами; не работает с многошаговой аутентификацией Медленнее; требует знания SQL и HTTP
Когда использовать Подтверждение уже найденной вручную инъекции; простой сценарий с двумя фиксированными URL Сложные REST API; многошаговые workflows; нестандартные триггеры
Когда НЕ использовать Отложенная обработка через очереди или cron; auth с ротирующимися CSRF-токенами Массовое сканирование десятков параметров

В сценарии из исследования NetSPI payload отправлялся через POST /api/report в JSON-теле, результат проверялся через GET /api/report/ExportToExcel?reportId=<UUID>. Между запросами — уникальный UUID из ответа первого запроса. sqlmap не умеет подхватить этот UUID и подставить во второй запрос. В Burp Repeater вы копируете идентификатор из ответа первой вкладки в запрос второй — вручную, зато с полным контролем над цепочкой.

Ручная эксплуатация SQL-инъекции второго порядка через Burp Suite Repeater

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

  • Burp Suite Community или Pro, версия 2024.x или новее. Community хватит для всех описанных техник; Pro добавляет полноценную интеграцию Burp Collaborator Client для OOB-каналов (эксфильтрация через DNS; в Community доступ к collaborator.net ограничен и без интеграции в инструменты).
  • Браузер с настроенным прокси через Burp (127.0.0.1:8080).
  • Целевое приложение с second order SQLi. Для лабораторного окружения: bWAPP в Docker (минимум 2 ГБ RAM) или самописный стенд PHP + MySQL (4 ГБ RAM для Docker Compose).
  • Доступ к функциям с отложенной обработкой пользовательского ввода: регистрация + профиль, создание задания + экспорт результата.

Обнаружение и подтверждение SQL-инъекции второго порядка

Главная задача — найти пару «точка сохранения → точка срабатывания». Это ключевая сложность stored SQLi: нужно проследить, как данные перемещаются внутри приложения. Никакой автоматикой это пока не решается — только руками и головой.

Шаг 1. Пройдитесь по функциям приложения через браузер с включённым Burp Proxy. Ищите пары: форма регистрации → страница профиля; создание задания → просмотр результата; поле настроек → сгенерированный отчёт. Откройте Target → Site Map в Burp и найдите связанные эндпоинты. Отправьте оба запроса в Repeater (правый клик → Send to Repeater). Получится две вкладки: одна для сохранения payload, вторая для проверки срабатывания.

Шаг 2. Во вкладке «сохранение» добавьте одинарную кавычку ' к значению параметра. Если данные отправляются в JSON-теле:

{
  "ReportParams": "{\"76\":{\"Value\":\"2024-08-15'\"}}",
  "ReportTypeId": 36
}

Отправьте запрос (Send). Ответ, скорее всего, будет 200 OK — приложение приняло данные и сохранило. Теперь перейдите во вторую вкладку и отправьте запрос на срабатывание. Ожидаемый результат: 500 Internal Server Error или SQL-ошибка в теле ответа. Получили 500 вместо обычного 200? Одинарная кавычка сломала SQL-запрос при повторном использовании данных из базы.

Шаг 3. Сбалансируйте SQL-запрос. Вместо одинокой кавычки отправьте ';-- (кавычка закрывает строковый литерал, точка с запятой завершает оператор, -- превращает остаток запроса в комментарий). Повторите цикл: первый запрос с ';-- → второй запрос. Ожидаемый результат: 200 OK с корректным ответом. Несбалансированный payload давал 500, сбалансированный даёт 200 — SQL-инъекция второго порядка подтверждена.

Как понять, что получилось: смотрите на код ответа и размер тела (столбцы Status и Length в Repeater). Разница между 200 и 500 — ваш маркер. Если разницы нет, попробуйте time-based подход: вместо ';-- отправьте '; WAITFOR DELAY '0:0:5'-- (SQL Server) или '; SELECT SLEEP(5)-- (MySQL). Задержка в 5 секунд во втором запросе подтвердит инъекцию.

Чтение схемы базы данных через information_schema

После подтверждения инъекции следующая цель — извлечь структуру БД. Таблица information_schema (системная база метаданных, доступная в MySQL, PostgreSQL, SQL Server) содержит имена всех таблиц и столбцов. Выбор техники извлечения зависит от того, что видно в ответе:

  • Ответ содержит данные запроса (таблицу, Excel, HTML) → UNION-based
  • Ответ содержит SQL-ошибки → Error-based через CAST()
  • Ответ меняется (200 vs 500, есть/нет элемент) → Boolean blind
  • Ответ одинаковый → Time-based blind
  • Обработка асинхронная → OOB (DNS exfiltration)

Выбор техники — не вопрос предпочтений, а вопрос того, что приложение вам «отдаёт». Идёте сверху вниз по списку: первое, что сработало, то и используете.

Error-based через CAST. Функция CAST() преобразует тип данных. Если заставить СУБД привести строку к числу — она вернёт ошибку с содержимым строки. Payload для сохранения: ' AND 1=CAST((SELECT table_name FROM information_schema.tables LIMIT 1) AS int)--. При срабатывании PostgreSQL ответит: ERROR: invalid input syntax for type integer: "users". Имя таблицы — прямо в тексте ошибки. PortSwigger отмечает: «this effectively turns an otherwise blind SQL injection vulnerability into a visible one».

UNION-based. Если приложение выводит данные запроса, используйте UNION SELECT. Пошагово:

  1. Определите количество столбцов. Отправляйте payload ' ORDER BY 1--, ' ORDER BY 2-- и далее через цикл сохранение → срабатывание. Когда ответ станет ошибочным — предыдущее число и есть количество столбцов.

  2. Найдите выводимые позиции: ' UNION SELECT 'aaa','bbb','ccc'-- - (число строк = числу столбцов; -- - с пробелом и символом после — для совместимости с MySQL). Посмотрите, какие маркеры появились в ответе.

  3. Извлеките имена таблиц:

' UNION SELECT table_name,NULL,NULL
  FROM information_schema.tables
  WHERE table_schema=database()
  LIMIT 0,1--

Меняйте LIMIT 0,1 на LIMIT 1,1, LIMIT 2,1 для перебора. Каждая итерация — два запроса в Repeater.

  1. Извлеките столбцы нужной таблицы: замените table_name на column_name, таблицу на information_schema.columns, добавьте условие WHERE table_name='users'.

  2. Извлеките данные: ' UNION SELECT username,password,NULL FROM users LIMIT 0,1--.

Blind second order SQLi: извлечение данных без вывода

Если приложение не выводит ни содержимого запроса, ни SQL-ошибок — это blind second order SQL injection. Данные придётся извлекать посимвольно через анализ поведения. Да, это медленно. Но это работает.

Boolean-based. Функция SUBSTRING() (в SQLite — SUBSTR()) проверяет один символ за раз. Payload для сохранения: ' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)>'m'--. При срабатывании: если первый символ пароля администратора больше m — приложение ведёт себя нормально (200 OK, корректный контент). Если нет — ошибка или пустой ответ.

Бинарный поиск ускоряет процесс: >'m'>'t'>'q' — это сокращает число проверок на символ с 62 (буквы + цифры) до примерно шести. PortSwigger подробно описывает эту технику на примере извлечения пароля Administrator: сначала >'m' (верно), затем >'t' (неверно), затем ='s' (верно) — первый символ найден.

Time-based. Если и поведение не меняется — используйте задержку. Для MySQL: ' AND IF(SUBSTRING(database(),1,1)='a',SLEEP(5),0)--. Для SQL Server: '; IF SUBSTRING(DB_NAME(),1,1)='a' WAITFOR DELAY '0:0:5'--. Время ответа второго запроса измеряется в столбце Time внизу панели Repeater. Прибавка 5 секунд = условие истинно.

Когда time-based не спасёт: если между сохранением и срабатыванием стоит асинхронная очередь (RabbitMQ, cron). Задержка в SQL-запросе не пробрасывается до HTTP-ответа — нужны OOB-каналы (DNS-эксфильтрация через xp_dirtree в SQL Server или LOAD_FILE() в MySQL, если есть привилегии). Для OOB в Burp нужна Pro-версия с Collaborator Client.

Различия СУБД при эксплуатации stored SQL injection

При ручной эксплуатации SQL-инъекции второго порядка нужно знать целевую СУБД — от этого зависит синтаксис payload. Определить СУБД можно по формату ошибок или через специфичные функции задержки.

СУБД Комментарий Подстрока Задержка Особенности second order
MySQL -- (пробел обязателен) или # SUBSTRING() SLEEP(N) Кавычки, удвоенные при INSERT, возвращаются к одинарной форме при SELECT
PostgreSQL -- SUBSTRING() pg_sleep(N) Строгая типизация — CAST часто выдаёт информативные ошибки с содержимым
SQL Server -- SUBSTRING() WAITFOR DELAY xp_dirtree для DNS-эксфильтрации; xp_cmdshell для RCE
SQLite -- SUBSTR() Нет встроенной функции Встречается в мобильных и встраиваемых приложениях

Обратите внимание на MySQL в контексте second order — это ловушка, на которой спотыкаются даже опытные разработчики. PortSwigger прямо указывает на механизм: «data that has been safely escaped when initially inserted into the database is subsequently read from the database and then passed back to it again. Quotation marks that have been doubled up initially will return to their original form when the data is reused, allowing the defense to be bypassed». Проще говоря: кавычки, удвоенные при INSERT (O''Brien), в самой базе хранятся как O'Brien. При повторном SELECT они возвращаются к одинарной форме и ломают SQL. Экранирование на входе не защищает от повторного использования.

Ограничения техники и когда она не работает

Ручная эксплуатация SQL-инъекции второго порядка имеет объективные границы. Знать их не менее важно, чем знать саму технику:

  • Prepared statements на обоих этапах. Если приложение использует параметризованные запросы (prepared statements — способ передачи данных отдельно от SQL-кода) и при INSERT, и при повторном SELECT/UPDATE — stored SQLi невозможна. Это основная рекомендация PortSwigger и OWASP. Ключевое слово — «на обоих этапах». Prepared statement на INSERT, но конкатенация на SELECT — и вы снова уязвимы.
  • WAF с поведенческим анализом. Современные WAF (Web Application Firewall — сетевой фильтр HTTP-трафика) могут блокировать payload уже на этапе сохранения, если обнаруживают SQL-синтаксис. Сигнатурные WAF обходятся кодированием, поведенческие — значительно сложнее.
  • ORM-библиотеки. Hibernate, Django ORM, SQLAlchemy автоматически параметризуют запросы, включая повторное использование данных. Stored SQLi при корректном использовании ORM маловероятна, но возможна при raw-запросах внутри ORM-кода (а они встречаются чаще, чем хотелось бы).
  • Асинхронная обработка. Если данные проходят через RabbitMQ, Kafka или cron — момент срабатывания непредсказуем. Time-based и boolean-blind подходы не работают, нужны OOB-каналы.
  • Минимальные привилегии БД. Если учётная запись приложения ограничена SELECT/INSERT на конкретные таблицы — доступ к information_schema может быть закрыт (хотя MySQL по умолчанию разрешает чтение information_schema всем пользователям).

Минилаб для отработки stored SQLi

Требования: Docker (2 ГБ RAM свободно), Burp Suite Community Edition, браузер с настроенным прокси.

Быстрый старт с bWAPP: выполните docker run -d -p 80:80 raesene/bwapp, откройте http://localhost/install.php, установите базу. Выберите задание «SQL Injection — Stored (Blog)» через меню. Введите в форму блога payload с одинарной кавычкой (например, test'), затем откройте страницу всех записей. Если увидите SQL-ошибку на странице вывода — данные из базы подставляются в запрос без параметризации. Повторите с payload test'; SELECT 1-- и убедитесь, что ответ изменился.

Самописный стенд (для тех, кто хочет разобраться глубже): создайте PHP-скрипт register.php, который вставляет username через PDO с prepared statement. Создайте profile.php, который выводит данные пользователя через конкатенацию: "SELECT * FROM users WHERE username='" . $row['username'] . "'". Зарегистрируйте пользователя с именем admin' UNION SELECT 1,2,3#, затем откройте его профиль. Если в профиле появились цифры 1, 2, 3 — second order SQL injection работает. Именно так выглядит разрыв между «безопасным INSERT» и «уязвимым SELECT» в реальных проектах.

Чеклист ручной эксплуатации SQL-инъекции второго порядка

  1. Определить функции с отложенной обработкой: регистрация → профиль, форма → экспорт, настройки → отчёт
  2. Оба запроса (сохранение + срабатывание) отправить в Burp Repeater — по отдельной вкладке
  3. Одинарная кавычка ' → точка сохранения → проверить точку срабатывания → ответ 500?
  4. Сбалансировать: ';-- → повторить цикл → ответ 200? → инъекция подтверждена
  5. Определить СУБД: по формату ошибок, через SLEEP(5) / pg_sleep(5) / WAITFOR DELAY
  6. Выбрать технику: error-based → UNION-based → boolean blind → time-based → OOB
  7. Для UNION: определить число столбцов через ORDER BY N
  8. Извлечь имя базы (database()), таблицы из information_schema.tables, столбцы из information_schema.columns
  9. Извлечь целевые данные из найденных таблиц
  10. Задокументировать цепочку: какой payload, в какой точке сохранён, какой запрос триггерит, какой ответ получен

Большинство пентест-отчётов, которые я читал за последние два года, содержат SQL-инъекции исключительно первого порядка — те, что sqlmap находит за минуты. Stored SQL injection попадает в отчёт в единичных случаях. И не потому что встречается реже, а потому что её целенаправленно не ищут. sqlmap даёт ложное чувство полноты: «прогнал все параметры — чисто». А за фасадом «чисто» может лежать deferred payload, который ждёт момента, когда другой кусок кода достанет его из базы без параметризации.

Проблема глубже инструментария. SQL-инъекция второго порядка требует понимания бизнес-логики — вещи, которую ни один автоматический сканер пока не освоил. Думать нужно не «какой параметр уязвим», а «куда этот параметр попадёт через час, через запрос, через микросервис». Это другой навык — он отделяет специалиста, который ищет уязвимости в архитектуре, от тестировщика, который гоняет чеклисты. Если в голове пока каша из терминов — на IB Basics эту базу разложат по полочкам, включая SQL-инъекции и работу с Burp Suite, без воды и академизма.

Эту тему и смежные навыки разбирают на практике в курсе «Тестирование веб-приложений на проникновение (WAPT)» Codeby Academy.