
Воспроизводит CWE-639 IDOR в Lago v1.53.0, где одноразовый вебхук Stripe загружает счета без привязки к организации, что позволяет обойти оплату между организациями. Включает лабораторию с loopback.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · lago-stripe-invoice-idor
Lago v1.53.0 - GetLago
POST /webhooks/stripe/:organization_id проверяет Stripe-Signature с помощью webhook-секрета той организации из URL. Затем ветка one-time в StripeService#update_payment_status загружает счёт через Invoice.find_by(id:) и без organization_id. handle_missing_payment ограничен областью. Счета Adyen уже передают organization_id:. Cashfree и Flutterwave копируют неограниченный find_by(id:) из Stripe. В этой лаборатории — Stripe.
Другой арендатор в той же базе данных, с подключённым Stripe, может пометить ваш счёт как оплаченный. В его панели Stripe нет соответствующего платежа. В вашей — возможно, есть.
| ID | CVE пока нет |
| CWE | CWE-639, CWE-345 |
| CVSS | High: 7.7 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N |
| Продукт | Lago / lago-api |
| Затронуто | вплоть до v1.53.0 one-time webhook Stripe; общая база данных |
| Аутентификация | организация атакующего с подключённым Stripe; UUID счёта жертвы |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Подписать payment_intent.succeeded от имени своей организации, указав payment_type=one-time и lago_invoice_id вашего счёта. Ограниченный путь никогда не выполняется. Строка жертвы становится payment_status=succeeded. Пополнение кошелька / подписка с оплатой разблокируется.
Это не неаутентифицированный доступ. Это не шелл. Самостоятельный хостинг с одной организацией — это не та ошибка: этот секрет подписи уже владеет обязательствами к оплате этой организации. Вам нужен UUID счёта жертвы (URL портала, письмо, PDF, утёкший API) и строка stripe_customer жертвы. Фиктивное событие. Без живого Stripe.
То же дерево, что и acceptInvite ATO. Другая ошибка.
Я прочитал update_payment_status, затем find_by(id:) в one-time create_payment, затем organization_id: в handle_missing_payment. Комментарий на ограниченном пути опасается одного и того же секрета Stripe, используемого в разных организациях. Ветка one-time забыла опасаться.
Первый клиент, который посмотрит на это, отправит POST с payment_intent.succeeded без payment_type. handle_missing_payment ищет счёт и organization_id. Организация атакующего, счёт жертвы. nil. Pending. Это контроль. Лаборатория это утверждает.
Уже зафиксированные неверные повороты: подпись с помощью webhook-секрета жертвы (тогда вы уже владеете обязательствами к оплате этого мерчанта); самостоятельный хостинг с одной организацией; UUID счёта из организации атакующего (платёж внутри организации); отсутствие stripe_customer у жертвы (исключение, а не succeeded); трактовка HTTP 200 от webhook как оракула (задача асинхронная — опрашивайте GET /api/v1/invoices/:id); обратный шелл. Театр.
Затем: два вызова registerUser создают Victim Corp и Attacker Corp. REST-ключ жертвы создаёт add-on, клиента и неоплаченный счёт с skip_psp, чтобы мне не нужен был живой PSP для создания строки. seed_stripe.rb создаёт провайдеров Stripe и stripe_customer у жертвы. Фиктивный webhook_secret. Фиктивный sk_test_lab_*. Подписанное контрольное событие без payment_type: всё ещё pending. Подписанное событие с payment_type=one-time: succeeded, paid=1000.
cd lab
./run.sh
Цель только http://127.0.0.1:13001. Invite ATO использовал 13000. Этот не разделяет том с той лабораторией.
IOC payment_status_before=pending
IOC control webhook (no payment_type) payment_status_after_control=pending
IOC attack webhook payment_type=one-time
IOC poll payment_status=succeeded paid=1000
SUCCESS Lago Stripe one-time webhook unscoped invoice
Invoice.find_by(id:, organization_id:) на пути one-time, как уже делают handle_missing_payment и счета Adyen. То же для Cashfree/Flutterwave.
webhooks_controller.rb · stripe_service.rb · adyen_service.rb · cashfree_service.rb · flutterwave_service.rbGNU Affero GPL v3.0. См. LICENSE. Только лаборатория на loopback. Без гарантий.