Анализ защищенности

Как мы нашли возможность списания денег с любого счета

Миллионы пользователей и одна ошибка в логике авторизации. Показываем, как из неё собирается рабочий способ увести деньги без пароля.

Image

Финтех

Собрали самое ценное

С чего всё началось

Что насторожило

Как открылось чужое

Где была дыра и как закрыли

С чего всё началось

Мы взялись за платёжный модуль финтех-сервиса. Продукт обрабатывал транзакции, хранил данные карт и работал с банковским API, то есть держал в руках самое чувствительное, что вообще бывает у бизнеса, чужие деньги и чужие платёжные данные. Задача звучала стандартно. Посмотреть на безопасность, найти что найдём, написать отчёт. Никто, включая команду клиента, не ждал ничего серьёзного, продукт работал стабильно много месяцев. Параллельно шла рутинная работа с обучающей выборкой для внутренней модели клиента, мы прогоняли через систему тестовые запросы и складывали ответы, чтобы понять, как она себя ведёт. Именно в этой рутине всё и началось. Самые дорогие находки почти никогда не приходят через парадную дверь, они вылезают сбоку, там, где никто не смотрит.

Что-то не так с данными

Платёжный модуль работал через несколько терминалов, и мы прогоняли одни и те же тестовые запросы через каждый, складывая ответы в таблицу для сравнения. Большинство терминалов отвечали ровно так, как должны. Везде ошибка авторизации и доступа, чужого не отдаём, и это правильное поведение. Но несколько терминалов из общего ряда вели себя иначе. На наши тестовые идентификаторы, которые мы придумали сами и которым не соответствовало ничего реального, они возвращали данные, которых мы не создавали. Чужие суммы. Чужие временные метки. Чужие транзакции. Сначала это выглядело как ошибка с нашей стороны, может, перепутали окружение или взяли не тот идентификатор. Но повторный прогон дал тот же результат. Часть терминалов отвечала так, будто наши выдуманные запросы попадали в реальный поток. Это был первый звоночек, что граница между тем, что мы считали песочницей, и боевой системой где-то протекает.

Тестовые данные не должны так отвечать

Чтобы убедиться, что это не совпадение, мы поставили чистый эксперимент. Отправили заведомо несуществующий идентификатор, такой, которого ни в одной базе просто не может быть, собранный наугад. По всем правилам ответом должна была быть пустота или отказ. Вместо этого терминал вернул ответ с реальной транзакцией. Чтобы исключить случайность, мы подождали двадцать минут и отправили тот же запрос снова. Пришла та же транзакция плюс новые, которых двадцать минут назад ещё не существовало. Это означало одно, и сомнений уже не оставалось. Перед нами был не тестовый стенд, а живая система, обрабатывающая настоящие платежи прямо сейчас, и она спокойно отдавала их по запросу, который не имел права ничего получить. То, что мы считали безопасной игровой площадкой, на деле было прямым окном в боевой контур.

Что оказалось внутри

Дальше мы пошли по тем же терминалам аккуратно, без разрушительных действий, только чтобы понять масштаб. Корень оказался обиднее любой сложной атаки. Одна строка в конфигурации, и граница между тестом и реальными деньгами исчезла. Тестовый и боевой контуры делили общий сервис авторизации, а признак того, тестовый это запрос или настоящий, передавался самим запросом и нигде на сервере не перепроверялся. То есть система верила клиенту на слово о том, в каком она режиме. Сначала мы проверили, что именно можно читать. Транзакции, балансы, привязанные карты. Всё открывалось без дополнительной авторизации, достаточно было знать идентификатор пользователя. А идентификаторы оказались предсказуемыми, порядковые числа с лёгким смещением, которые перебираются за минуты. Сложив это вместе, мы увидели картину целиком. Зная всего один параметр, можно было читать платёжные данные любого клиента, а затем, двигаясь дальше, мы дошли до точки, где система приняла тестовый запрос на списание и вернула статус обработки. Здесь мы остановились, зафиксировали факт и передали всё клиенту, не трогая реальные деньги. Доказать наличие дыры, а не воспользоваться ей, в этом и есть разница между аудитом и атакой.

Где была дыра и как закрыли

Корень был не в одном сбойном терминале, и заменить или перезагрузить его означало бы убрать симптом, а причину оставить. Настоящая проблема состояла в том, что система доверяла данным, которым доверять нельзя. Мы зафиксировали три причины и закрыли каждую вместе с командой клиента. Первая, общий сервис авторизации на два контура. Развели тестовый и боевой так, чтобы они физически не могли обращаться к одной точке принятия решений. Вторая, признак среды приходил от клиента. Перенесли определение режима и проверку прав полностью на сервер, убрав всякое доверие к тому, что присылает запрос. Третья, предсказуемые идентификаторы и отсутствие проверки владельца. Заменили их на непредсказуемые и добавили проверку прав на каждое обращение к объекту. После внедрения мы прогнали те же сценарии заново. Терминалы перестали отвечать на чужие и несуществующие идентификаторы, окно в боевой контур закрылось.

Related Blogs

Путь к защите

Что увидит атакующий, если начнёт изучать вашу инфраструктуру сегодня?

Аудит покажет реальный путь к вашим данным

Shape

Путь к защите

Что увидит атакующий, если начнёт изучать вашу инфраструктуру сегодня?

Аудит покажет реальный путь к вашим данным

Shape

Путь к защите

Что увидит атакующий, если начнёт изучать вашу инфраструктуру сегодня?

Аудит покажет реальный путь к вашим данным

Shape