abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · lago-stripe-invoice-idor
Lago v1.53.0 - GetLago
POST /webhooks/stripe/:organization_id 使用该 URL 所属组织的 webhook secret 验证 Stripe-Signature。随后 StripeService#update_payment_status 中的一次性分支使用 Invoice.find_by(id:) 加载发票,且没有 organization_id。handle_missing_payment 是有作用域限制的。Adyen 发票已经传入了 organization_id:。Cashfree 和 Flutterwave 复制了未限定作用域的 Stripe find_by(id:)。本实验针对 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 的 Stripe 一次性 webhook;共享数据库 |
| 认证 | 已连接 Stripe 的攻击者组织;受害者发票 UUID |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅限 127.0.0.1 |
以他们的组织身份签名一个 payment_intent.succeeded,填入 payment_type=one-time 和你的发票的 lago_invoice_id。有作用域限制的路径永远不会执行。受害者的记录变为 payment_status=succeeded。钱包充值 / 支付门控订阅解锁。
这不是未认证攻击。不是 shell。单组织自托管不存在此漏洞:那个签名密钥本来就掌管该组织的应付款项。你需要一个受害者发票 UUID(门户 URL、邮件、PDF、泄露的 API)以及一条受害者的 stripe_customer 记录。伪造事件。无需真实的 Stripe。
与 acceptInvite ATO 属于同一代码树。不同的漏洞。
我阅读了 update_payment_status,然后是一次性的 create_payment 中的 find_by(id:),接着是 handle_missing_payment 的 organization_id:。有作用域限制的路径上的注释担心的是同一个 Stripe secret 被跨组织使用。而一次性分支忘了担心这一点。
第一个看到这段代码的客户端会 POST 一个不带 payment_type 的 payment_intent.succeeded。handle_missing_payment 会同时查找发票和 organization_id。攻击者组织,受害者发票。nil。Pending。这就是对照组。实验环境对此进行了断言。
已记录的弯路:使用受害者的 webhook secret 签名(那样你本来就掌管了该商户的应付款项);单组织自托管;来自攻击者组织的发票 UUID(组织内支付);受害者上没有 stripe_customer(异常,而非 succeeded);将 webhook HTTP 200 当作判定依据(任务是异步的——轮询 GET /api/v1/invoices/:id);反向 shell。都是花架子。
然后:两次 registerUser 调用植入 Victim Corp 和 Attacker Corp。受害者 REST key 创建一个附加项、一个客户,以及一张使用 skip_psp 的未支付发票,这样我就不需要真实的 PSP 来生成该记录。seed_stripe.rb 在受害者上植入 Stripe provider 和一条 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:),就像 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。仅限回环实验环境。无任何担保。