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

Sql-инъекция в java-приложении: анализ Jar-файла и подтверждение уязвимости

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-файла позволяет быстро перейти от внешнего симптома к конкретному методу и строке, а контролируемая динамическая проверка подтверждает, что найденный дефект действительно проявляется во время работы приложения.

Прокрутить вверх