SQL-инъекция при анализе Java-приложения: от JAR-файла до подтверждения уязвимости
Материал посвящён безопасному анализу корпоративного Java-приложения в рамках авторизованного пентеста. Все действия выполняются в изолированной среде и только на системах, которыми специалист владеет либо на проверку которых получил письменное разрешение. Названия продукта, классов, пакетов и таблиц изменены.
Рассмотрим типичный Java-монолит: веб-интерфейс работает через HTTPS на встроенном Jetty, приложение слушает порт 8443, а данные хранятся в PostgreSQL. Исходный код в распоряжении отсутствует, однако доступны собранные артефакты приложения - JAR-файлы из каталога `WEB-INF/lib`.
Проблема заключается в SQL-запросе, сформированном конкатенацией строк:
```java
"... WHERE ID=" + parameter
```
Такой подход позволяет пользовательскому значению изменить структуру SQL-команды. Если параметр передаётся без аутентификации, риск особенно высок: атакующему может быть достаточно одного HTTP-запроса, чтобы проверить поведение базы данных и получить несанкционированный доступ к данным.
Зачем анализировать байткод
Динамическое тестирование показывает, что приложение реагирует подозрительно, но не всегда объясняет причину. Декомпиляция помогает быстро восстановить логику:
- становится видна конкретная уязвимая строка;
- можно определить все параметры запроса и условия перехода;
- проще понять, какая СУБД используется;
- становится ясно, как исправить дефект;
- сокращается число экспериментальных запросов.
Декомпиляция не заменяет динамический анализ, но существенно ускоряет расследование. Для Java-приложений этот метод особенно эффективен: байткод обычно сохраняет имена методов, классов и общую структуру программы. Теряются главным образом комментарии и названия локальных переменных, однако для понимания потока данных этого обычно достаточно.
Подготовка инструментов
Для работы подойдут:
- декомпилятор JD-GUI;
- CFR или Procyon для сложных либо частично обфусцированных сборок;
- ripgrep, Notepad++ или VS Code для поиска по исходникам;
- Python 3 с библиотекой `requests`;
- `curl` для ручной проверки;
- тестовый экземпляр приложения и журналы сервера.
В каталоге `WEB-INF/lib` обычно находятся как собственные JAR-файлы продукта, так и сторонние зависимости: драйвер PostgreSQL, Apache Commons, SLF4J и другие компоненты. Ориентироваться только на размер архива не стоит. Собственная библиотека может оказаться меньше любой внешней зависимости.
После открытия нужного JAR в JD-GUI достаточно сохранить все исходники через пункт `File → Save All Sources`. В результате появляется дерево Java-файлов, по которому можно выполнять локальный поиск.
Поиск SQL и HTTP-обработчиков
Первый этап - найти места, где формируются запросы:
```text
select
from
query
prepareStatement
createStatement
executeQuery
```
В крупном проекте такой поиск даст сотни или тысячи совпадений. Поэтому полезно сразу сузить область до классов, которые принимают HTTP-запросы.
Сервлеты обрабатывают обращения клиента через методы:
```text
doGet
doPost
doPut
doDelete
```
Искать их можно регулярным выражением:
```text
do(Get|Post|Put|Delete)
```
Особое внимание стоит уделять сервлетам, отвечающим за экспорт, фильтрацию, поиск и отчётность. Подобные функции почти всегда передают пользовательские параметры в слой доступа к данным.
В рассматриваемом примере подозрительным оказывается `ReportExportServlet`. Его назначение - формирование отчёта по складским остаткам.
Разбор логики сервлета
Сервлет получает несколько параметров:
- `fullExport`;
- `minQty`;
- `warehouseId`;
- `includeArchived`.
Далее выполняется последовательность проверок. При `fullExport=true` вызывается отдельная процедура полного экспорта, после чего обработка завершается. Если установлен `includeArchived=true`, приложение переводит запрос в ветку с проверкой прав.
Нужная логика срабатывает только тогда, когда одновременно выполнены два условия:
- `minQty` передан и содержит корректное число;
- `warehouseId` существует и не является пустым.
Таким образом, для анализа требуется попасть именно в эту ветку, не включая остальные флаги. Параметр количества проходит через `Integer.parseInt`, поэтому произвольное текстовое значение вызовет ошибку преобразования. Это ограничивает один из входов, но не защищает второй.
Дальше вызывается метод наподобие:
```java
exportStockByWarehouse(warehouseId, minQty, out);
```
Прослеживая значение `warehouseId`, можно увидеть его передачу в DAO-класс, где и формируется SQL-команда.
Причина уязвимости
В небезопасном варианте код выглядит примерно так:
```java
String sql =
"SELECT item_id, quantity FROM stock " +
"WHERE warehouse_id=" + warehouseId +
" AND quantity >= " + minQty;
```
Здесь `minQty` ограничивается преобразованием к целому числу, а `warehouseId` используется напрямую. Если параметр попадает в SQL без параметризации и дополнительной проверки, пользовательское значение может изменить смысл запроса.
Важно учитывать контекст. Один и тот же символ может обрабатываться по-разному в зависимости от того, находится ли значение внутри кавычек, является ли оно числом и добавляются ли после него дополнительные условия. Поэтому сначала необходимо восстановить точную строку запроса, а не полагаться на догадки.
Восстановление маршрута
Чтобы проверить уязвимость корректно, нужно восстановить полный путь запроса:
1. найти объявление сервлета;
2. определить его отображение в `web.xml` или аннотациях;
3. установить HTTP-метод;
4. проверить параметры, которые читаются через `getParameter`;
5. проследить ветвление;
6. дойти до DAO или вспомогательного класса;
7. собрать итоговый URL в тестовой среде.
Устаревшие приложения часто используют явные записи в `web.xml`, однако современные сборки могут применять аннотации вроде `@WebServlet`. Иногда маршрут задаётся через собственный диспетчер, поэтому поиск необходимо продолжать по имени класса и ключевым словам из конфигурации.
Безопасное подтверждение
Подтверждать проблему следует минимально агрессивным способом. Цель - установить факт влияния на SQL-логику, а не извлечь реальные данные или изменить базу.
В тестовой среде можно применять безвредные временные задержки, характерные для PostgreSQL, например функцию `pg_sleep` с очень коротким интервалом. Если время ответа предсказуемо увеличивается только при изменённом параметре, это указывает на выполнение сервером фрагмента SQL.
Проверка должна включать:
- обычный корректный запрос;
- заведомо некорректное значение;
- контрольный запрос без задержки;
- тестовый запрос с минимальной задержкой;
- несколько повторов для исключения сетевых колебаний.
Измерять нужно не один ответ, а серию запросов. На результат могут влиять нагрузка, пул соединений, кэширование, прокси и тайм-ауты.
Минимальный PoC
Вместо автоматического извлечения информации достаточно продемонстрировать различие во времени ответа:
```python
import time
import requests
url = "https://test-host:8443/report"
cases = {
"normal": {
"warehouseId": "1",
"minQty": "10",
},
"control": {
"warehouseId": "1",
"minQty": "10",
},
}
for name, params in cases.items():
started = time.perf_counter()
response = requests.get(
url,
params=params,
verify=False,
timeout=10
)
elapsed = time.perf_counter() - started
print(name, response.status_code, round(elapsed, 3))
```
В реальном отчёте значения для подтверждения должны использоваться только в согласованном тестовом контуре. Не следует превращать PoC в инструмент дампа базы данных: для доказательства достаточно показать контролируемое влияние на выполнение запроса.
Проверка по журналам
Одного наблюдения за временем ответа недостаточно. Журналы помогают подтвердить цепочку событий:
- входящий URL и параметры;
- факт обращения к нужному сервлету;
- исключения JDBC;
- сообщения PostgreSQL;
- время выполнения запроса;
- идентификатор соединения или транзакции.
Если приложение пишет только общий статус ответа, полезно временно включить расширенное логирование на тестовом стенде. При этом нельзя оставлять подробные SQL-журналы включёнными в рабочей среде: они могут содержать пароли, токены и персональные данные.
Почему blind-уязвимость опасна
Даже если приложение не выводит результат SQL-команды в HTTP-ответе, проблема остаётся критичной. При blind SQL-инъекции данные могут извлекаться по косвенным признакам:
- времени ответа;
- различиям в статусах;
- размеру страницы;
- появлению или исчезновению элементов интерфейса;
- особенностям сообщений об ошибках.
Если запрос выполняется без аутентификации, злоумышленник может автоматизировать проверку и постепенно восстанавливать сведения о структуре базы, учётных записях и конфигурации. В случае доступа к чувствительным таблицам это может привести к компрометации пользовательских данных и дальнейшему захвату аккаунтов.
Возможная цепочка до захвата пароля
SQL-инъекция не всегда означает мгновенное получение пароля. Обычно атака развивается поэтапно:
1. определяется СУБД;
2. уточняется структура таблиц;
3. находятся таблицы пользователей;
4. извлекаются логины и хэши паролей;
5. оценивается стойкость хэширования;
6. проверяется повторное использование паролей;
7. подбираются слабые или утёкшие комбинации;
8. проверяется доступ к административным функциям.
Если система хранит пароли в открытом виде либо использует слабое хэширование без соли, последствия особенно серьёзны. Но даже стойкий хэш не устраняет риск: он может быть использован для офлайн-подбора или сопоставлен с данными из других утечек.
Как исправить дефект
Основной способ устранения - параметризованные запросы:
```java
String sql =
"SELECT item_id, quantity FROM stock " +
"WHERE warehouse_id = ? AND quantity >= ?";
try (PreparedStatement statement =
connection.prepareStatement(sql)) {
statement.setInt(1, warehouseId);
statement.setInt(2, minQty);
try (ResultSet result = statement.executeQuery()) {
// обработка результата
}
}
```
Одной проверки формата недостаточно. Нужны сразу несколько защитных мер:
- применять `PreparedStatement`;
- использовать строгую типизацию параметров;
- проверять диапазоны значений;
- отклонять неожиданные дополнительные параметры;
- ограничивать права учётной записи базы;
- не возвращать пользователю подробности JDBC-ошибок;
- добавлять автоматические тесты на инъекции;
- проводить повторное сканирование после исправления.
Если параметр является идентификатором, безопаснее преобразовать его в числовой тип до построения запроса. Если это имя поля или направление сортировки, применять белый список допустимых значений, поскольку такие элементы нельзя передавать через обычный параметр запроса.
Что включить в отчёт
Качественный отчёт должен содержать:
- затронутый маршрут;
- HTTP-метод;
- имя параметра;
- условия, необходимые для попадания в уязвимую ветку;
- фрагмент опасной логики;
- влияние на конфиденциальность, целостность и доступность;
- безопасный PoC без извлечения реальных данных;
- оценку риска;
- конкретный вариант исправления;
- рекомендации по повторной проверке.
Такой подход показывает не только наличие проблемы, но и её первопричину. Декомпиляция JAR-файла позволяет быстро перейти от внешнего симптома к конкретному методу и строке, а контролируемая динамическая проверка подтверждает, что найденный дефект действительно проявляется во время работы приложения.
