Инъекционные атаки
Инъекция — это один баг со множеством лиц (SQL, OS-команды, NoSQL, LDAP), и все они — одна и та же ошибка: данные атакующего пересекают границу в интерпретатор как код. Как об этом рассуждает атакующий и почему параметризация — единственный фикс, закрывающий весь класс.
В рамках авторизованного задания тестировщик вводит одну-единственную апострофу в поле поиска товаров и отправляет форму. Страница возвращает 500 с обрывком ошибки драйвера базы данных. Для разработчика эта ошибка — досадная мелочь, которую надо погасить. Для тестировщика — это признание: введённая им апостроф дошла до базы и изменила структуру запроса, а значит, это вовсе не поле поиска, а программный интерфейс к бэкенду. Он не вламывался ни инструментом, ни через CVE. Он ввёл один символ, который код приложения послушно вклеил в SQL-строку и передал движку. Всё, что следует дальше — чтение чужих строк, выгрузка колонки с хешами паролей, иногда выполнение OS-команд на хосте, — лишь развитие первого факта: приложение не смогло отличить данные от кода. Это и есть инъекция — самый старый и самый живучий класс веб-уязвимостей, одна ошибка в десятке разных костюмов.
К концу урока ты сможешь объяснить, как атакующий распознаёт точку инъекции и рассуждает о ней, и почему именно параметризация — а не экранирование или фильтрация — это контроль, который реально закрывает весь класс.
Авторизация прежде всего: это вскрытие, которое делает защитник
Всё ниже предполагает подписанное задание с явным охватом (scope) — намеренно уязвимый стенд, CTF, твоё собственное приложение или программу багбаунти с целью в списке активов. Прощупывание живого приложения, которым ты не владеешь, на инъекции — это несанкционированный доступ в большинстве юрисдикций, точка; один неправильно сформированный запрос к чужой базе может повредить или уничтожить данные, так что «я просто тестировал» — не оправдание. Мы изучаем рассуждения атакующего по одной причине: нельзя надёжно закрыть класс уязвимостей, который понимаешь лишь как пункт чек-листа. Читай это как вскрытие, которое защитник проводит над собственным кодом, учась видеть запрос так, как его видит атакующий, чтобы сломать атаку до того, как она появится. Здесь нет готовых пейлоадов для копирования — только механизм, чтобы ты узнал его на код-ревью и убил в дизайне.
Один механизм за каждой «инъекцией»
SQL-инъекция, OS-command-инъекция, NoSQL-инъекция, LDAP-инъекция, XPath-инъекция, даже server-side template injection — безопасники называют их по-разному, но это один и тот же баг в разных интерпретаторах. Механизм всегда такой: приложение строит инструкцию (SQL-выражение, shell-команду, LDAP-фильтр), конкатенируя доверенный код с недоверенным вводом как текст, и затем отдаёт получившуюся строку интерпретатору, который заново её разбирает. Интерпретатор понятия не имеет, какие байты написал разработчик, а какие подставил пользователь, — он видит одну строку и разбирает её по своей грамматике. Если байты пользователя содержат символы, синтаксически значимые в этой грамматике (кавычку, завершающую строковый литерал; точку с запятой, завершающую выражение; обратный апостроф, открывающий подоболочку), пользователь только что написал код, и интерпретатор его выполняет.
Эта переформулировка и есть весь урок. Уязвимость не в том, что «пользователь ввёл что-то плохое». Она в том, что система смешала два уровня доверия в один канал и затем разобрала этот канал по грамматике. Вся работа атакующего — найти место, где его ввод попадает внутрь интерпретируемой строки, и подать ввод, который вырывается из слота данных в слот кода. Апостроф в поле поиска — ровно это: в SQL ' завершает строковый литерал, поэтому ввод с апострофом может закрыть задуманный разработчиком литерал и продолжить выбранным атакующим SQL.
Как атакующий рассуждает о точке инъекции
Атакующий не начинает с уверенности, что баг есть, — он прощупывает границу между данными и кодом. Это небольшой дисциплинированный цикл, и понимание его — то, что позволяет защитнику воспроизвести и починить проблему.
- Найти сток (sink). Локализовать, где пользовательский ввод правдоподобно достигает интерпретатора — фильтр поиска, параметр сортировки,
idв URL, форма логина, имя файла экспорта, заголовок. Всё, что приложение может подать в запрос или команду. - Возмутить синтаксис. Отправить ввод с символом, специальным для предполагаемой грамматики (для SQL — кавычку; для shell — метасимвол), и смотреть, что изменится. Ошибка 500, иное число результатов или сдвиг по времени — всё это сигналы, что ввод дошёл до парсера и изменил его.
- Подтвердить и охарактеризовать. Отличить реальную инъекцию от случайной ошибки, переключаясь между вводом, который должен оставить синтаксис валидным, и вводом, который должен его сломать. Если «валидный» грузится нормально, а «сломанный» падает — ввод разбирается, и это подтверждённая точка инъекции.
- Определить контекст и канал. Понять, какой интерпретатор (SQL? shell? LDAP?) и приходят ли результаты в ответе (in-band) или их надо выводить косвенно (слепая, blind).
Контексты: один баг, разный интерпретатор и радиус поражения
Причина, по которой инъекция держится в топе любого списка рисков, — она обобщается на каждый интерпретатор, с которым говорит бэкенд, а радиус поражения масштабируется с тем, что этот интерпретатор умеет.
| Контекст | Интерпретатор / сток | Что получает атакующий | Структурный фикс |
|---|---|---|---|
| SQL-инъекция | SQL-движок через строку-запрос | Чтение/правка любой строки; выгрузка хешей паролей; иногда доступ к файлам/ОС | Параметризованные запросы (связанные значения) |
| OS-command-инъекция | Shell, вызванный со строкой-командой | Команды от имени приложения — потенциально полная компрометация хоста | Без shell: exec бинарника с массивом аргументов |
| NoSQL-инъекция | Запрос документной БД из объектов запроса | Обход аутентификации через инъекцию операторов; эксфильтрация данных | Типизированные билдеры; отклонять ввод формы оператора |
| LDAP / XPath | Фильтр каталога или XML через конкатенацию | Обход аутентификации; перечисление записей каталога | Параметризованные/экранирующие API фильтров |
OS-command-инъекция заслуживает отдельной заметки, потому что её радиус поражения — худший в списке. SQL-инъекция компрометирует базу; command-инъекция компрометирует хост, выполняясь с привилегиями процесса приложения, — что на плохо изолированной машине может означать всю машину и всё, до чего она дотягивается. Корневая причина та же — ошибка конкатенации строк, просто интерпретатором служит shell, а не база. И структурный фикс зеркалит SQL-овский: не отдавай shell строку, которую он заново разбирает на метасимволы, — вызывай целевую программу напрямую, передав её аргументы отдельным списком, чтобы ОС никогда не запускала shell, способный истолковать точку с запятой или пайп как новую команду.
Слепая инъекция: когда ответ не возвращается
Новички полагают, что инъекция требует, чтобы база выводила данные на страницу. Это не так. Слепая инъекция (blind) — случай, когда приложение не возвращает атакующему результаты запроса или ошибки, но атакующий всё равно может извлекать данные по биту за раз, задавая вопросы да/нет и наблюдая побочный эффект. Два классических канала:
- Булева (boolean-based): сформировать ввод так, чтобы запрос был истинным в одном случае и ложным в другом, и смотреть, как различается ответ — другая страница, другая длина, наличие или отсутствие элемента. Каждый запрос утекает один бит («первый символ хеша админа — это „a“?»).
- Временная (time-based): заставить запрос спать, когда условие истинно. Данные нигде не появляются; атакующий читает их чисто по тому, сколько занимает ответ. Это медленно — часто автоматизируется, выгружая хеш символ за символом за тысячи запросов, — но полно: слепой временной канал способен извлечь целую базу при достаточном терпении.
Защитный вывод из слепой инъекции груб и проясняющ: подавление сообщений об ошибках не чинит инъекцию. Команды регулярно «исправляют» баг, спрятав ошибку базы, которая его впервые выдала. Точка инъекции на месте; атакующий просто переключается на слепую технику и продолжает — теперь без удобной ошибки, которая подсказала бы твоему мониторингу. Сокрытие симптома убирает твой собственный сигнал и оставляет уязвимость полностью эксплуатируемой.
▸Почему это работает
Почему фильтрация ввода или экранирование не закрывают инъекцию надёжно, а параметризация закрывает? Фильтрация («вырезать кавычки и точки с запятой») — это блок-лист, а блок-листы проигрывают: у каждого интерпретатора больше опасного синтаксиса, чем ты помнишь (последовательности комментариев, кодировки, Unicode-двойники, альтернативные операторы), и один пропущенный случай вновь открывает дыру. Ручное экранирование может быть корректным, но возлагает на каждого разработчика обязанность экранировать идеально для точного контекста каждый раз, и один забытый запрос — это пробой. Параметризация выигрывает, потому что меняет архитектуру, а не ввод: структура запроса отправляется движку первой и компилируется, затем значения связываются с плейсхолдерами по отдельному каналу, который никогда не разбирается как синтаксис. Нет строки, из которой атакующему вырываться, — слот данных и слот кода физически разные каналы. Это безопасно по построению, а не по бдительности, поэтому масштабируется на весь кодовую базу, а блок-лист — никогда.
Почему параметризация — единственный фикс, закрывающий класс
Свяжем воедино. Инъекция существует, потому что код и данные делят один канал, который интерпретатор заново разбирает. Каждый устойчивый фикс работает через разделение этих каналов на уровне архитектуры, так что недоверенный ввод доставляется как значение, структурно неспособное быть истолкованным как синтаксис. Для SQL это параметризованные запросы (они же подготовленные выражения / связанные параметры): база получает шаблон запроса, планирует его и только потом принимает значения — которые трактует как непрозрачные данные, никогда как часть структуры выражения. Для OS-команд — вызов бинарника с вектором аргументов вместо shell-строки. Для NoSQL — типизированные билдеры запросов, отклоняющие ввод формы оператора. Эшелонированная защита по-прежнему важна — учётки БД с минимальными правами, чтобы успешная инъекция читала меньше; валидация ввода как allowlist, чтобы рано отбросить некорректное; структурированное логирование, чтобы засечь прощупывание, — но это снижает ущерб, а не закрывает дыру. Закрывает её только разделение кода и данных, и поэтому параметризованный запрос, а не WAF-правило, — первый и последний ответ зрелого инженера на инъекцию.
Эндпоинт поиска строит SQL, конкатенируя строку запроса пользователя в условие WHERE, и тестировщик подтвердил SQL-инъекцию. Выбери фикс, который реально закрывает класс.
Какова единая корневая причина, общая для SQL-инъекции, OS-command-инъекции и LDAP-инъекции?
Команда «чинит» SQL-инъекцию, спрятав сообщение об ошибке базы, которое её выдало. Почему приложение всё ещё эксплуатируемо?
Упорядочь, как авторизованный тестировщик рассуждает от подозрительного поля ввода к подтверждённой инъекции, а затем к устойчивому фиксу:
- 1 Найти сток, где ввод правдоподобно достигает интерпретатора (поиск, id, логин)
- 2 Возмутить синтаксис символом, специальным для предполагаемой грамматики, и следить за изменением
- 3 Подтвердить, переключая синтаксически-валидный и сломанный ввод, и охарактеризовать контекст/канал
- 4 Закрыть класс структурно: параметризовать запрос, чтобы ввод связывался как данные, а не разбирался как код
- 01Объясни единый механизм за каждым классом инъекции и как атакующий рассуждениями приходит к подтверждённой точке инъекции.
- 02Почему параметризация закрывает инъекцию, а фильтрация, экранирование и WAF-правила — нет, и что в слепой инъекции делает сокрытие ошибок бесполезным?
Инъекция — это одна уязвимость во множестве костюмов: SQL, OS-команды, NoSQL, LDAP, XPath, шаблоны, — и каждая из них — одна и та же ошибка: приложение конкатенирует недоверенный ввод с доверенным кодом в единую строку, затем отдаёт её интерпретатору, который заново разбирает данные и код вместе по своей грамматике. Когда ввод содержит синтаксически значимые символы (кавычку, точку с запятой, shell-метасимвол), он перестаёт быть данными и становится кодом, который интерпретатор выполняет. Авторизованный тестировщик находит эти точки дисциплинированным циклом — локализовать сток, возмутить синтаксис, подтвердить переключением валидного и сломанного ввода, охарактеризовать контекст и канал, — и радиус поражения масштабируется с интерпретатором: SQL-инъекция компрометирует базу, OS-command-инъекция компрометирует хост. Слепая инъекция (булева и временная) показывает, почему поверхностные фиксы проваливаются: когда не возвращается ни результат, ни ошибка, атакующий всё равно извлекает данные по биту за раз из различий ответа или тайминга, так что фильтрация символов, сокрытие ошибок и навешенное WAF-правило оставляют дыру открытой, заодно убирая твой собственный сигнал. Единственный фикс, закрывающий класс, — структурный: отделить код от данных, чтобы недоверенный ввод доставлялся как значение, неспособное быть разобранным как синтаксис: параметризованные запросы, exec с массивом аргументов, типизированные билдеры. Поэтому в следующий раз, читая обработчик, который строит запрос склейкой строки, твой рефлекс — вопрос атакующего наизнанку: может ли хоть один байт этого ввода изменить структуру того, что выполняется, или только его значения?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.