20 лет проблем приема платежей
За логотип спасибо yarbabin
Электронные системы расчетов существуют в интернете уже давно, а баги на них встречаются двадцатилетней давности. Мы находили критические уязвимости, позволяющие угнать деньги и накрутить баланс. Сегодня мы разберем типовые реализации приема платежей и связанные с ними проблемы безопасности.
Обзор платежных систем и типовых реализаций API
Мало кто знает, но первой (анонимной!) платежной системой был DigiCash, который появился аж в 1989 году, за ним, в 1996 году, последовала уже более известная (преимущественно среди кардеров) система E-gold.
Но вернемся в настоящее и перечислим основные современные крупные платежные системы/сервисы электронных платежей, которые позволяют принимать платежи на собственном веб-сайте:
- PayPal
- WebMoney
- ЮMoney (бывшие Яндекс.Деньги)
- Qiwi
- Alipay
- и т.д.
А также десятки менее известных систем, названия которых вам ничего не скажут, не говоря уже о появлении сотен новых, специализирующихся на криптовалютах.
Несмотря на кажущуюся простоту, процесс приема платежей, с точки зрения создания безопасной программной реализации, представляет собой комплексный процесс, который до сих пор приводит к проблемам как у крупных торговых площадок, так и у новых электронных систем расчетов, которые периодически выходят на рынок с «новым и удобным» API и прочими способами интеграции. Как же выглядит типичный процесс приема платежа? Для начала давайте рассмотрим текущую реализацию, которую описывает PayPal, так называемый PayPal Express Checkout.
Данную реализацию можно считать относительно безопасной, и вот почему:
- Параметры платежа не передаются явным образом, вместо этого используется Token
- Сервер платежной системы не отправляет результаты на некий URL самостоятельно, вместо этого ваш веб-сайт должен самостоятельно их запросить и обработать ответ
- В целом схема взаимодействия реализована так, что у потенциального разработчика существует минимум возможностей «выстрелить себе в ногу»
А теперь посмотрим на схему, которую нам предлагает WebMoney:
Схема ни хрена не понятная. Также схема не отражает ряд нюансов, вроде подписи запроса. Или информацию о том, что URL, который принимает на себя технические параметры платежа от платежной системы, и URL, куда пользователь будет перенаправлен для просмотра деталей об оплате, стоит делать разными. Архитектура, которую использует WebMoney, часто всплывает в той или иной форме и в других платежных системах, которые были созданы в СНГ.
Типовые проблемы
Излишнее усложнение схемы приема платежей ведет к финансовым потерям. Например, 10 лет назад я публиковал заметку о проблеме интеграции с WebMoney системы Global Collect Services, что приводило к возможности подтверждать платежи без оплаты в Steam, Battle.net и некоторых других.
В чем же состояла проблема? Ранее я упоминал URL на стороне продавца, которые должны принимать информацию о платеже. Согласно документации, у WebMoney существуют три сущности:
- Success URL — URL (на веб-сайте продавца), на который будет переведен интернет-браузер покупателя в случае успешного выполнения платежа в сервисе Web Merchant Interface. URL имеет префикс «http://» или «https://».
- Fail URL — URL (на веб-сайте продавца), на который будет переведен интернет-браузер покупателя в том случае, если платеж в сервисе Web Merchant Interface не был выполнен по каким-то причинам. URL имеет префикс «http://» или «https://».
- Result URL — URL (на веб-сайте продавца), на который сервис Web Merchant Interface посылает HTTP POST или SMTP-оповещение о совершении платежа с его детальными реквизитами. URL должен начинаться с префикса «http://», «https://» или «mailto:».
Что делают некоторые разработчики, которые читают эту документацию:
- Обрабатывают информацию по одному URL, что приводит к возможности узнать адрес обработчика (также обработчик, в том числе Result URL, может отображаться в платежной форме на сайте WebMoney, но такое происходит не всегда и, вероятно, зависит от настроек).
- Некорректно реализуют проверку подписи для запроса, который приходит на Result URL. Это позволяет клиенту подменить данные о платеже.
- Проверяют подпись, но не проверяют сумму, которая пришла на Result URL. Это позволяет получить товар за 100$, заплатив, например, 0.01$.
- Проверяют подпись, сумму, но не нотацию передаваемых сумм. Помните, я упоминал о передаче параметров платежа через браузер клиента? Так вот, WebMoney абсолютно нормально воспринимает сумму платежа со значением 1e1 или 0xFF, а сравнение подобных чисел, еще и на старых версиях PHP, еще и с учетом нюансов сравнения в языке PHP, приводило к самым неожиданным последствиям.
- Не совсем проблема платежной системы, НО, как насчет «состояния гонки» (race condition) и одинаковых внутренних идентификаторов платежей на ресурсе продавца? Привет, мультипликация баланса.
- …
Подпись запросов
Как это работало:
- При оплате через WebMoney, пользователя, в соответствии со спецификацией платежной системы, перенаправляло на сайт WebMoney, где он мог видеть сумму платежа, номер счета и прочие параметры.
- После нажатия кнопки «Далее» и аутентификации в системе становилась доступна информация об URL, который отвечал за обработку результата платежа (Result URL).
- Пользователь мог сформировать запрос к целевому URL, который, в соответствии со спецификацией WebMoney (ну почти), информировал платежную систему о том, что платеж успешно проведен.
- Profit!
Система приема платежей Global Collect успешно споткнулась о несколько проблем, которые упоминались выше:
- Известный единый обработчик результатов платежа.
- Отсутствие проверки подписи (да и отсутствие подписи в запросе как таковой).
- Использование данных, передаваемых через браузер пользователя, в качестве источника информации о платеже (хотя, согласно спецификации WebMoney, это можно было делать через коллбэк, приходящий от серверов WebMoney).
Все это привело к возможности совершать фиктивные транзакции и покупать все, что использовало процессинг Global Collect, без ограничений. Проблему устранили только через ~2 недели массовой эксплуатации.
Другой вариант похожей проблемы, но чуть сложнее, был не так давно в Smart2Pay.
Еще одна проблема, связанная с подписью запросов — Length Extension Attack.
Или атака удлинением сообщения. Согласно Wikipedia — это тип атаки на хеш-функцию, заключающейся в добавлении новой информации в конец исходного сообщения. При этом новое значение хэша может быть вычислено, даже если содержимое исходного сообщения остаётся неизвестным. Чуть подробнее можно изучить здесь. Проблема встречалась всего пару раз, когда разработчики решили реализовать свою «классную» подпись запросов в стиле VK (которые в общем-то тоже не сами придумали алгоритм), но получилось как обычно.
Ниже небольшая иллюстрация на тему, как допустимо генерировать подпись в таком вот стиле и как «выстрелить себе в ногу».
Для эксплуатации же можно воспользоваться одним из следующих инструментов:
Раскрытие «Result URL»
На сайте, где было доступно пополнение с помощью WooPay (через SMS), отображался полный URL с параметрами (включая подпись), по которому платежная система уведомляет сайт, если платеж успешно зачислен.Логика достаточно проста, нужно вызвать исключительную ситуацию, чтобы тестируемое веб-приложение вывело ошибку.
Если выбрать оплату через SMS и ввести случайный недействительный номер, то получаем:
Повторяем запрос сотню-другую раз. Отправив нас в бан, сайт начинал выводить exception, в тексте которого содержался тот самый секретный URL, при переходе по которому на счет зачисляются деньги.
Атрибуты платежа
Перейдем к проблеме проверки атрибутов платежа.
Один из вариантов интеграции с ЮMoney (ex Яндекс.Деньги), это форма для перевода или ее старая реализация. Опознать ее можно по наличию запросов к следующим адресам:
https://yoomoney.ru/eshop.xml
https://yoomoney.ru/quickpay/confirm.xml
Прямо при отправке запроса необходимо подменить число, которое запрашивает тестируемое веб-приложение:
Высокая вероятность, что платеж пройдет. А дальше сайт либо проверяет сумму, либо нет. Так как в комментарии к платежу передается идентификатор пользователя и/или идентификатор платежа, этого достаточно, чтобы оформить подписку на какой-то сервис.
Небольшой пример:
На скриншоте происходит оплата подписки на бота голосового ассистента в Telegram, но сумма платежа не проверяется, что дает возможность приобрести продукт за произвольную цену. Подменяем сумму 1990 на 19, получаем нужную подписку. Этот тип проблем часто встречается (например, много сервисов-ботов в Telegram, о которых многие слышали, и где эта проблема до сих пор присутствует), в том числе на зарубежных ресурсах (пример из старых — покупка лицензии Minecraft), в 2022 году, хотя казалось бы…
Еще один релевантный пример, но связанный не с суммой платежа, а с валютой, присутствовал у QIWI. В кошельке была опция пополнения путем отправки SMS на короткий номер, при чем валюта передавалась через браузер клиента на нескольких этапах (выбор валюты и суммы, отправка SMS), где сервер доверял данным клиента. В итоге на счет зачислялось 100$, а оплата на 100р.
А что там в 2022?
Смотрим в чат армянского банка https://t.me/Inecobank_forum/6333
Значит, проблема до сих пор актуальна.
Нотации и сравнение типов
Занимательная проблема присутствовала на широко известном в узких кругах сервисе Антикапча (сервис по разгадыванию капчи за деньги с API интерфейсом). Личный кабинет пользователя позволял совершать ряд операций, в т.ч. выводить неиспользованный баланс на WebMoney. WebMoney нормально воспринимает сумму платежа в различных нотациях (например, со значением 1e1 или 0xFF), а сравнение подобных чисел, еще и на старых версиях PHP, еще и с учетом нюансов сравнения в языке PHP, еще и с порцией «качественного кода» приводило к самым неожиданным последствиям.
В шестнадцатеричной нотации сравнение текущего баланса с запрашиваемой на вывод суммой работало некорректно, что позволяло уводить баланс аккаунта в минус. Пример возможной логики перевода денег:
Если на вход подать 1e9
, имея на балансе 20
долларов, механизм проверки удостоверится, что 19>20
, вырезав все кроме цифр, а процессинг обработает 1e9
как 1000000000
.
Другая ошибка — это особенности приведения типов.
NodeJS — пример языка с динамической типизацией. Когда прибавляешь к числу строку, то произойдет конкатенация 1+"1" = "11"
. Но стоит из строки вычесть число, то уже строка приводится к числу "11"-1 = 10
. Самый популярный формат обмена данными — JSON:
{"amount":100}
Справедливо, что JSON будет корректным: параметр amount с числом 100. Но и этот вариант будет корректным JSON«ом:
{"amount": "100"}
Только содержимое параметра amount будет строкой, но из-за особенностей обработки подобного запроса, возможно, кто-то приплюсует к числу 1337 значение этого параметра, и получится 1337100, а не то, что задумывалось изначально.
Ошибки бизнес-логики
В ДБО создается платеж, чтобы его подтвердить, необходимо ввести код из SMS. Платеж сохраняется как неисполненный и доступен для редактирования в мобильном приложении. Редактируем платеж, после этого в браузере вводим код подтверждения, и итоговый перевод совершается с одной суммой, а со счета списывается другая.Другой пример:
- Вызываем механизм пополнения баланса (на балансе $1000, пополняем на $100).
- Веб-приложение запоминает текущий баланс.
- В это время тратим (отправляем на второй аккаунт) деньги.
- После выполнения транзакции баланс окажется $1100.
Отдельного упоминания заслуживает работа корзины на ресурсах, где используется несколько валют. Уязвимость, которая была в магазине Xbox несколько лет назад (причем после исправления появлялась еще пару раз):
- Кладёшь в корзину товар за минимальную цену в рублях.
- Ищешь в магазине дорогие игры, цены на которые указаны в долларах.
- Добавляешь их в корзину.
- Магазин считает сумму позиций, но перерасчёт от доллара к рублю происходит в соотношении один к одному.
Голоса в VKontakte генерировались с помощью SMS со счета с около-нулевым балансом. Отправляешь SMS, оператор связи не может забрать деньги (овердрафт отсутствует), а голоса пополняются.
Другой вектор через SMS — это перевод со своего счета на чужой в платежной системе QIWI. Это делалось через отправку сообщения на специальный короткий номер:
Но дело в том, что короткий номер — это алиас настоящего телефонного номера, который участвует в SMS-шлюзе для интеграции с API. Применяем немного социальной инженерии:
Дальнейшие шаги — использовать сервисы по подмене номера, чтобы отправить туда SMS от аккаунта, на котором много денег. Способ так и не проверен, хотя в теории выглядит крайне забавно. Очевидцы говорят, что подобным образом можно было привязать карту через SMS, а дальше сливать деньги с карты.
А теперь изучим операцию возврата средств (refund). Если рассмотреть каноничный процесс возврата, то станет очевидно, что на каждом этапе можно пропустить или некорректно реализовать все проверки, что приведет к финансовым потерям.
На практике встречались площадки, где возврат средств по транзакции происходил таким образом, что сумма возврата бралась от актуальной текущей стоимости товара, а не из информации о проведенной транзакции. Вместе с периодическими скидками, это приводило к понятным результатам. Ситуация редкая, но иногда встречается в той или иной форме.
Ошибки округления, переполнения и числа с отрицательным знаком
Частая категория проблем — ошибки округления чисел. Распространенные проблемы с округлением могут выглядят следующим образом:
- Пользователь переводит 0,29 RUB в доллары США.
- При стоимости одного доллара в 60 RUB, сумма в 0,29 RUB соответствует 0,00483333333333333333333333333333 USD.
- Данная сумма будет округлена до двух знаков после запятой, т. е. до 0,01 USD (один цент).
- Затем пользователь переводит 0,01 USD обратно в рубли и получает 0,60 RUB.
- Таким образом пользователь «выигрывает» 0,31 RUB.
Проблему до сих пор можно встретить в крупных финансовых организациях (различных банках и биржах).
Если присмотреться, то можно увидеть уязвимость, хоть она и не актуальна на текущий момент.
С переполнениями и операциями с числами с отрицательным знаком также периодически можно столкнуться, даже в банках из топ 100. Перевод отрицательной суммы — тривиальный пример при работе с числами со знаком, и да, такое тоже до сих пор встречается.
Менее тривиальный пример про переполнения — подсчет суммы заказа при добавлении большого числа товаров в корзину.
Еще один пример — это восприятие больших сумм при передаче между системами. В HTTP-запросе передаваемое число будет строкой, но вот обработка большого числа может отличаться, т.е. отправляется запрос на пополнение на больше чем INT_MAX+2, на локальной системе число обрабатывается корректно, а в платежной системе получаем счет на оплату размером в 1$.
Стоит учитывать, что тестируемая система может использовать не 32-разрядную переменную для хранения значения, а 64-разрядную.Чтобы лучше понять, можно потыкать циферки в вконтакте. Раньше в VKontakte все числа были 32-разрядные. Чтобы получить id1 с помощью переполнения, необходимо было посчитать 2^32+1
.
Страница Дурова открывалась под https://vk.com/id4294967297. Но сейчас уже все переведено на 64-разрядные числа, поэтому, чтобы получить единицу, необходимо посчитать 2^64+1.
Это одни и те же страницы:
https://vk.com/id1 == https://vk.com/id18446744073709551617
А теперь представь, что в веб-приложении операцию с id=100
может выполнить только администратор? А если это операция с 2^32+100
?
Кстати, иногда можно не рассчитать с циферками, и уйти глубоко в минус, так и не достигнув плюса.
Состояние гонки
Перейдем к такой проблеме, как состояние гонки (англ. race condition). Согласно Wikipedia — это ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Своё название ошибка получила от похожей ошибки проектирования электронных схем.
Условно каноничный пример:
- Выполняем операцию на перевод средств в рамках баланса.
- Совершаем ту же операцию N раз, где баланс должен закончиться при N-1 или меньше, но отправляя запросы с минимальной задержкой (тут на помощь приходит HTTP-пайплайнинг, особенности с HTTP2 (все в рамках одной TCP-сессии) и т.п.).
- Наблюдаем минус на балансе.
Небольшой пример, связанный с криптовалютной биржей.
Алгоритм эксплуатации был следующий:
- Создаем тейк-профит на 0,1 BTC, когда стоимость биткойна будет равна $100,000.
- Биржа изымает (блокирует) в аккаунте 0,1 BTC на будующий тейк-профит.
- Удаляем тейк-профит, отправляя 438695936458926734 запросов.
- Биржа «возвращает» нам 0,1 x N BTC, где N, количество одновременно выполненных операций.
Эта категория проблем не специфична для финансовых операций. Сюда же относятся проблемы типа TOCTOU, когда, например, приложение проверяет подпись на файле, далее некоторое окно, и далее работа с содержимым файла (а содержимое возможно подменить в рамках окна).
Одна из проблем присутствовала на xss.is в системе перевода BTC между учетными записями.
Для тестирования можно использовать Burp Suite с плагином Turbo Intruder. А подробнее об этой категории проблем можно почитать в статье.
Итак, кладем на депозит 0.1337 BTC, отправляем множество запросов на перевод.
Видим, что отправка перевода выполнилась больше раз, чем было денег на балансе:
Отправляем крипту обратно. Продолжаем гонять деньги туда-сюда под разными аккаунтами, генерируя деньги из воздуха:
Получаем деньги на счету. Только на самом деле такого депозита не было, поэтому выводить можно до тех пор, пока подключенный кошелек (со всеми депозитами пользователей) не опустеет.
Я думаю, существуют и другие форумы, в которых возможны депозиты и автоматический вывод без ручного подтверждения.
Резюме
Реализация безопасного приема платежей — это комплексная задача, которой должны заниматься опытные разработчики. Получившийся продукт необходимо всесторонне тестировать, иначе мы еще не один десяток лет будем наблюдать детские проблемы безопасности из начала нулевых, особенно при появлении новых классных способов платежей (привет, криптовалюты) и сопутствующих платежных систем. И это мы еще не упоминали атаки на генераторы псевдослучайных чисел, Padding Oracle, и множество других веселых штук, которые заслуживают отдельной статьи.
© Kaimi & Bo0oM