Наблюдая за работой сервера, я вижу, как он выполняет ровно ту бизнес-логику, которую в него заложили. Система, доверяющая данным от клиента без проверки их целостности, добровольно отдает управление процессами любому, кто умеет перехватывать HTTP-запросы.
Механика подделки JWT токенов и обход криптографической защиты
JSON Web Token состоит из трех частей, разделенных точками. Заголовок указывает алгоритм шифрования. Полезная нагрузка содержит данные пользователя. Подпись гарантирует целостность этих данных. Атакующий может изменить алгоритм в заголовке на значение none. Сервер без строгой проверки алгоритма проигнорирует отсутствие подписи и примет измененную полезную нагрузку. Злоумышленник меняет роль пользователя на администратора и получает полный доступ к системе.
Подобная уязвимость возникает из-за того, что многие библиотеки по умолчанию доверяют алгоритму, указанному в самом токене. Разработчик забывает явно указать серверу, какие алгоритмы допустимы для данного приложения. В результате сервер слепо выполняет инструкцию из заголовка и отключает проверку подписи. Пример модифицированного заголовка выглядит следующим образом: eyJhbGciOiJub25lIn0. Полезная нагрузка в таком случае может содержать {"role":"admin"}, а третья часть подписи остается пустой.
Брутфорс слабых секретных ключей представляет еще один распространенный вектор атаки. Алгоритм HS256 использует симметричное шифрование. Один и тот же секретный ключ применяется для создания и проверки подписи. Если разработчик выбирает короткую или словарную фразу в качестве секрета, злоумышленник может восстановить этот ключ методом перебора. Инструменты вроде Hashcat позволяют проверить миллионы кандидатов в секунду. После восстановления секрета атакующий получает возможность подписывать любые токены от имени сервера. Команда для запуска атаки выглядит так: hashcat -m 16500 jwt.txt wordlist.txt.
Применение асимметричной криптографии полностью исключает возможность подбора ключа. Алгоритмы семейства RS256 используют пару ключей. Приватный ключ хранится только на сервере и используется для подписи. Публичный ключ доступен всем и служит исключительно для проверки подписи. Даже при компрометации публичного ключа создание новых токенов остается невозможным.
Существует еще одна техника, известная как подмена алгоритма с RS256 на HS256. В некоторых реализациях сервер использует один и тот же ключ для обоих алгоритмов. Атакующий меняет алгоритм в заголовке токена с RS256 на HS256. Затем он подписывает токен, используя публичный ключ сервера в качестве симметричного секрета. Сервер, ожидая проверку по алгоритму HS256, успешно верифицирует подпись публичным ключом, который у него есть в открытом доступе. Подобная архитектурная ошибка полностью ломает модель доверия асимметричного шифрования.
Дополнительным вектором атаки является манипуляция заголовком kid (Key ID). Этот параметр указывает серверу, какой именно ключ использовать для проверки подписи. Если сервер не валидирует значение kid, атакующий может указать путь к локальному файлу, например /dev/null. Сервер попытается прочитать этот файл как ключ, получит пустую строку и успешно проверит токен, подписанный пустым секретом.

Ошибки проектирования бизнес-логики и атаки на аутентификацию
Автоматизированные сканеры уязвимостей часто пропускают ошибки бизнес-логики. Такие дефекты возникают из-за неверного проектирования процессов, а не из-за синтаксических ошибок в коде. Запрос выглядит абсолютно валидным с технической точки зрения. Сервер успешно его обрабатывает. Проблема кроется в том, что разрешенное действие нарушает бизнес-правила приложения.
Классическим примером служит механика колеса скидок. Приложение разрешает прокрутить колесо один раз в день. Злоумышленник перехватывает запрос на сервер и многократно отправляет его через прокси. Сервер обрабатывает каждый запрос как новый и начисляет множество призов. Отсутствие привязки действия к уникальному идентификатору сессии или состоянию аккаунта позволяет бесконечно повторять операцию.
Финансовые операции часто уязвимы к манипуляциям с отрицательными значениями. Пользователь инициирует перевод отрицательной суммы на свой счет. Сервер вычитает отрицательное значение из баланса получателя и прибавляет его к балансу отправителя. В результате баланс отправителя увеличивается. Разработчики забывают добавить проверку на то, что сумма перевода должна быть строго больше нуля. Пример полезной нагрузки для такого запроса: {"amount": -5000, "target_account": "user_123"}.
Манипуляции с курсами валют создают еще один вектор атаки. Приложение предлагает льготный курс при обмене крупной суммы. Атакующий формирует запрос на обмен большой суммы для получения льготного курса. Затем он перехватывает этот запрос в прокси и меняет сумму на минимально возможную. Сервер применяет льготный курс к малой сумме из-за отсутствия проверки последовательности действий. Система проверяет условия только в момент инициации запроса и не перепроверяет их в момент финального подтверждения транзакции.
Состояние гонки позволяет эксплуатировать временные задержки между проверкой условия и выполнением действия. Пользователь отправляет несколько одновременных запросов на списание средств. Сервер проверяет баланс для каждого запроса параллельно, видит достаточные средства и выполняет все транзакции до обновления итогового баланса в базе данных. Подобная уязвимость требует высокой степени синхронизации запросов, но приводит к катастрофическим последствиям для финансовых систем.
Небезопасная прямая ссылка на объект также относится к ошибкам бизнес-логики. Приложение использует предсказуемые идентификаторы ресурсов, такие как order_id или user_id, в URL или параметрах запроса. Атакующий изменяет эти идентификаторы на значения, принадлежащие другим пользователям. Сервер не проверяет, имеет ли текущий аутентифицированный пользователь права на доступ к запрашиваемому ресурсу. В результате злоумышленник получает доступ к чужим заказам, личным данным или функциям администрирования.
Мини-кейс: цепочка обхода аутентификации и манипуляции балансом
Реальный сценарий компрометации часто объединяет несколько уязвимостей в одну цепочку. Приложение использует JWT токены с алгоритмом HS256 и слабым секретным ключом. Атакующий перехватывает токен при авторизации и отправляет его в Hashcat. Словарная атака восстанавливает секретный ключ за несколько минут.
Злоумышленник модифицирует полезную нагрузку токена, изменяя идентификатор пользователя на идентификатор администратора. Он подписывает измененный токен восстановленным ключом и отправляет его серверу. Сервер принимает токен как валидный, предоставляя доступ к административной панели.
Далее атакующий обращается к функции перевода средств между счетами, доступной в панели. Он формирует запрос на перевод отрицательной суммы со своего счета на счет жертвы. Сервер обрабатывает транзакцию, так как проверка на отрицательные значения отсутствует в бизнес-логике. Баланс атакующего увеличивается, а баланс жертвы уменьшается. Цепочка из слабого шифрования и ошибки валидации приводит к полной компрометации финансовой логики приложения без необходимости эксплуатации классических инъекций.
Методология тестирования уязвимостей логики и токенов
Ручное тестирование остается единственным надежным способом поиска логических уязвимостей. Сканеры не понимают контекст приложения. Они не знают, что колесо можно крутить только раз в день или что курс валют зависит от суммы.
Burp Suite предоставляет необходимые возможности для глубокого анализа. Модуль Repeater позволяет вручную изменять параметры запроса и наблюдать за реакцией сервера. Модуль Intruder автоматизирует перебор значений, например для проверки реакции системы на отрицательные числа или огромные суммы.
Для тестирования состояний гонки используется расширение Turbo Intruder. Оно позволяет отправлять десятки идентичных запросов в одном TCP-соединении с минимальной задержкой. Это имитирует реальную атаку на параллельную обработку транзакций. Скрипт для Turbo Intruder выглядит следующим образом:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=100,
pipeline=False)
for i in range(30):
engine.queue(target.req, target.baseInput)
def handleResponse(req, interesting):
table.add(req)
Анализ JWT токенов начинается с декодирования заголовка и полезной нагрузки. Проверка алгоритма подписи и длины секретного ключа выполняется через утилиты вроде john the ripper или hashcat со стандартными словарями. Тестировщик также проверяет возможность удаления подписи или изменения алгоритма на none непосредственно в модуле Repeater, наблюдая за кодом ответа сервера.
Для поиска небезопасных прямых ссылок на объекты тестировщик создает два тестовых аккаунта. Он выполняет целевое действие от имени первого аккаунта, перехватывает запрос и заменяет идентификаторы ресурсов на идентификаторы, принадлежащие второму аккаунту. Успешное выполнение запроса подтверждает наличие уязвимости.
Практические рекомендации для разработчиков по защите
Защита от подобных атак требует строгой валидации на стороне сервера. Приложение должно проверять алгоритм подписи JWT и жестко отвергать значение none. Конфигурация библиотеки должна явно разрешать только конкретные алгоритмы, такие как RS256. Использование слабых секретных ключей для алгоритма HS256 недопустимо. Секрет должен представлять собой криптографически стойкую случайную строку достаточной длины.
Финансовые операции требуют внедрения идемпотентности. Сервер должен генерировать уникальные ключи для каждой транзакции и отвергать повторные запросы с тем же ключом. Такой подход предотвращает атаки повторного воспроизведения и множественное выполнение одного действия.
Проверка бизнес-правил должна происходить после применения всех изменений к данным. Сервер обязан убедиться, что итоговое состояние системы соответствует допустимым пределам. Перед выполнением перевода система должна проверить, что сумма положительна, что баланс отправителя достаточен и что получатель существует. Все эти проверки выполняются в одной транзакции базы данных. Любое нарушение условий приводит к полному откату операции.
Пример защиты на уровне кода требует явной валидации входных данных и блокировки строк на уровне базы данных.
def process_transfer(sender_id, receiver_id, amount, idempotency_key):
if amount <= 0:
raise ValueError("Сумма перевода должна быть положительной")
if not is_idempotency_key_valid(idempotency_key):
raise ValueError("Запрос уже был обработан")
with transaction.atomic():
sender = Account.objects.select_for_update().get(id=sender_id)
if sender.balance < amount:
raise ValueError("Недостаточно средств")
sender.balance -= amount
receiver = Account.objects.get(id=receiver_id)
receiver.balance += amount
sender.save()
receiver.save()
mark_idempotency_key_as_used(idempotency_key)
Использование select_for_update блокирует строку в базе данных на время транзакции, предотвращая состояние гонки при параллельных запросах. Явная проверка на положительное значение суммы устраняет риск манипуляций с отрицательными числами. Проверка ключа идемпотентности гарантирует, что один и тот же запрос не будет выполнен дважды.