返回 AI 资讯
安全研究言零的博客

0 PHP 白嫖 ChatGPT Plus — 跨区定价混淆攻击的完整技术拆解

联系方式 & 交流群 QQ : 46333839 微信 : GOV-HACK 进微信群请联系博主,各位觉得文章对你有帮助的话可否打赏一些呀~ 免责声明 :本文仅供安全研究与技术讨论。文中不提供可直接复用的攻击工具或完整利用脚本。利用类似手段对正式服务进行未授权操作属违法行为,后果自负。本文目的是帮助支付系统开发者理解跨区定价攻击面并加固防御。 前言 这两天群里炸了。 有人甩出一张截图:Stripe 结账页面,产品写着 “ChatGPT Plus”,金额一栏赫然显示 ₱0.00 PHP。菲律宾比索,零元,提交即开通…

内容摘要

联系方式 & 交流群 QQ : 46333839 微信 : GOV-HACK 进微信群请联系博主,各位觉得文章对你有帮助的话可否打赏一些呀~ ** 免责声明**:本文仅供安全研究与技术讨论。文中不提供可直接复用的攻击工具或完整利用脚本。利用类似手段对正式服务进行未授权操作属违法行为,后果自负。本文目的是帮助支付系统开发者理解跨区定价攻击面并加固防御。 前言 这两天群里炸了。 有人甩出一张截图:Stripe 结账页面,产品写着 “ChatGPT Plus”,金额一栏赫然显示 ₱0.00 PHP。菲律宾比索,零元,提交即开通。 评论区画风可想而知——“假的吧"“PS 的吧"“你怎么不说 Plus 倒找你钱”。 然后陆陆续续有人贴出了完整流程,甚至还配了视频录屏。操作步骤不复杂,总共四步,不需要 Root,不需要 Frida,不需要改一行代码。唯一的技术含量在于——你得知道用什么顺序把哪些区域串起来。 这就有意思了。之前我们拆过 prorationMode hook (改一个 putInt 白嫖 Claude Max)、拆过 焚决脚本 (劫持 checkout_capabilities 走 SEPA 假账号)、拆过 RevenueCat 凭证转移 (匿名购买 + restore 归属偷渡)。那些多少都是"改代码"的路子——Hook 内存、注入响应、篡改参数。 但这次不一样。这次 什么都没改 。没有脚本,没有 Hook,没有中间人。攻击者只是以一种特定的顺序访问了几个网页,然后 Stripe 自己就吐出了 0 元账单。 今天把它完整拆开。 一、攻击全景:四步,从 JP 到 PH,从 $20 到 ₱0 先把流传的步骤还原一下: Step 1:日本节点注册 OpenAI 账号 挂 JP 代理,开一个干净的浏览器 Profile(指纹浏览器或者 Chrome 新 Profile),去 OpenAI 官网正常注册,邮箱验证,登录确认。 此时你的账号状态: account.locale = "ja-JP" account.billing_country = "JP" account.currency = "JPY" OpenAI 在注册时根据你的 IP 地理位置初始化账号的计费区域。这一步的目的不是"要日本"本身,而是要一个非美区的初始定价锚点。 Step 2:切美国节点,提取 Access Token 保持同一个浏览器 Profile,VPN 切到 US 节点,重新登录刚才注册的账号。然后新标签页打开: https://chatgpt.com/api/auth/session 浏览器返回一个 JSON,里面有 accessToken 字段——一个标准 JWT。复制保存。 此时你的 session 状态出现了第一层矛盾: account.billing_country = "JP" ← 注册时写入,持久化在 OpenAI 数据库 session.ip_country = "US" ← 当前 IP 推断,存在 session/edge 层 Step 3:第三方工具生成 Checkout 链接 打开某个第三方"CDK 提炼"工具站点(这类工具本质上就是一个 AT → Stripe Checkout Session 的代理),把 Access Token 粘进去,点生成。 工具返回一个链接,格式: https://chatgpt.com/checkout/openai_llc/oaics_xxxxxxxxxxxxxxxxxxxxx oaics_ 是 OpenAI 的 Stripe Checkout Session ID 前缀。这个 session 是 OpenAI 后端基于你的 AT 创建的,包含了产品、价格、货币等信息。 关键问题来了:这个 Checkout Session 的 currency 和 amount 是怎么决定的? Step 4:打开链接,惊喜时刻 在美国节点下打开这个 oaics 链接。Stripe 结账页面加载出来,你看到: ChatGPT Plus ₱0.00 PHP / month 菲律宾比索。零元。 填入指定 BIN 的卡片信息(523686 或 4513),美国账单地址,提交。 Stripe 收了一笔 0 元交易,OpenAI 后端确认 checkout session 成功,权益生效。回到 ChatGPT 首页,左上角已经是 Plus 了。 二、为什么是 0 PHP?——三种假说 先澄清一个重要事实:OpenAI 在菲律宾是有明确定价的,ChatGPT Plus 的 PH 区定价大约是 ₱990 PHP/月。所以这不是简单的"价格表空洞导致 fallback 到 0”——如果定价系统正常查 PH 价格表,应该返回 ₱990,不是 ₱0。 那 0 是怎么来的?没有对 Checkout Session 创建过程的完整抓包,我们只能做推断。以下三种假说按可能性排序。 假说一:第三方工具在创建 Checkout Session 时注入了零元参数(最可能) OpenAI 的 /backend-api/payments/checkout 接口接受多个参数来创建 Stripe Checkout Session。如果这个接口接受客户端传入的 discount 、 coupon 、 promotion_code 或类似参数,且后端没做资格校验——工具直接传了一个 100% 折扣的 coupon code 或者 amount_override: 0 。 这和之前 RevenueCat 文章 里分析过的 eligible_promo_campaigns 注入如出一辙:客户端声称"我有优惠资格”,后端不校验就信了。 // 工具的真实请求可能长这样 body : JSON . stringify ({ plan : "plus" , promotion_code : "某个内部免费试用码" , // 或者 billing_country : "PH" , currency : "php" , // 或者更直接的 price_override : 0 }) JP 注册 + US 登录制造的区域歧义,可能不是直接触发 0 元的原因,而是绕过风控检查的前置条件——当账号区域和 IP 区域不一致时,后端对 checkout 参数的校验逻辑可能走入了一个宽松的分支。 假说二:跨区信号冲突导致定价路由进入异常分支 当 OpenAI 创建 Stripe Checkout Session 时,面前有至少四个区域信号: 信号来源 值 决定时机 账号注册地 JP 注册时固化 当前 session IP US 每次请求实时推断 Stripe 卡 BIN 发行国 PH 填卡时由 Stripe 推断 浏览器 Accept-Language 取决于浏览器设置 每次请求携带 正常场景下这四个信号一致——美国人在美国用美国卡买东西,直接查 US 价格表。 但当 JP ≠ US ≠ PH 三个信号同时存在时,定价系统需要做一个选择。如果选择逻辑是 if (account.country != ip.country) { use card.country } 这种优先级策略,而 PH 价格表虽然存在但在某个 子路径 (比如"非本地注册用户+跨区IP"的特殊定价分支)中没有配置——就可能在这个特殊分支里 fallback 到 0。 换句话说:PH 的主价格表有值(₱990),但某个异常处理路径的价格映射没覆盖到 PH,攻击者通过区域信号碎片化精确命中了这条路径。 假说三:Stripe Checkout Session 的 line_items 被工具篡改 Stripe checkout.sessions.create 的 line_items 参数包含 price 或 price_data 。如果 OpenAI 的接口允许客户端指定 price_data.unit_amount (哪怕是间接的),工具就能在创建 session 时直接把金额写成 0。 line_items: [{ price_data: { currency: "php", unit_amount: 0, // 直接指定 0 product: "prod_xxx", recurring: { interval: "month" } }, quantity: 1 }] 这种情况下 PH BIN 的作用不是"触发 PH 价格表",而是让 currency 字段说得通——如果你传 currency: "php" 但卡是 US 卡,Stripe 可能会有额外的 currency mismatch 警告。PH 卡 + PHP 货币是自洽的。 为什么是 PH BIN? 无论哪种假说,流传的两个 BIN 都有其作用: BIN 网络 发行国 523686 Mastercard 菲律宾 4513xx Visa 取决于具体发行行 523686 开头的卡填入 Stripe 表单时,Stripe 内部标记 payment_method.card.country = "PH" 。这个信号在整个支付流中传播。PH BIN 的作用至少有两个: 让 PHP 货币的 checkout session 能正常完成——卡的发行国和结账货币匹配 可能绕过 OpenAI 的 card-country vs account-country 风控规则——PH 卡付 PHP 金额在 Stripe 层面是"正常交易" 三、JP 注册的作用——制造"区域身份分裂" 你可能会问:为什么不直接在美国注册、美国提 Token、用 PH 卡付? 答案是:直接美区注册的账号,定价系统的行为是确定性的——走 US 价格表,$20 USD,没有歧义可利用。 JP 注册的作用是制造一个区域身份不确定的账号。当账号的 billing_country 是 JP,session IP 是 US,这两个信号已经矛盾了。定价系统在处理这种矛盾时,可能会: 走入一个 异常处理分支 ,该分支的参数校验比正常路径更宽松 允许第三方工具注入的额外参数(如 currency、discount)覆盖默认行为 使用一个与主价格表不同的 备用定价逻辑 ,而这个逻辑有漏洞 用状态机的视角看: ┌──────────────┐ │ 注册时: JP │ │ locale=ja-JP │ └──────┬───────┘ │ ┌──────▼───────┐ │ 登录时: US │ │ ip_country=US│ │ ≠ locale │ └──────┬───────┘ │ ┌──────▼───────────┐ │ 创建 Checkout │ │ JP ≠ US → 歧义 │ │ → 进入异常分支 │ │ → 参数校验宽松 │ └──────┬───────────┘ │ ┌──────▼───────────┐ │ 工具注入参数 │ │ + Card BIN: PH │ │ → amount = 0 PHP │ └──────────────────┘ 三个不同的区域信号(JP / US / PH)制造了歧义,歧义打开了异常路径,异常路径上的校验缺失被利用。 这是一个经典的安全反模式:当多个权威源对同一个问题给出不同答案时,系统在歧义中放松了校验——不是直接返回 0,而是给了攻击者操纵结果的空间。 四、第三方"提炼工具"在干什么? 流程里的那个第三方网站( sms.linlinflow.ccwu.cc ),名字叫"CDK 提炼",听着玄乎,其实它干的事非常直白: 接收你的 Access Token 用这个 AT 调用 OpenAI 后端 API,创建一个 Stripe Checkout Session 把 Checkout Session 的 URL 返回给你 本质就是一个 AT → oaics_ 链接 的代理服务。 它的技术实现大概率是这样的: // 伪代码,非真实接口 const response = await fetch ( "https://chatgpt.com/backend-api/payment/checkout" , { method : "POST" , headers : { "Authorization" : `Bearer ${ accessToken } ` , "Content-Type" : "application/json" }, body : JSON . stringify ({ plan : "plus" , // 关键:这里可能注入了额外参数 // billing_country? currency? locale? }) }); const { checkout_url } = await response . json (); // checkout_url = "https://chatgpt.com/checkout/openai_llc/oaics_..." 关键问题:工具只是"转发"了 AT,还是"动了手脚"? 考虑到 PH 区有明确的 Plus 定价(约 ₱990/月),而结果是 ₱0——工具大概率不只是简单转发。它在创建 Checkout Session 时注入了额外参数,利用跨区账号的异常处理路径绕过了服务端校验。 具体注入了什么参数?没有抓包数据我们无法确定。可能是 promotion_code 、 discount 、 currency + amount 覆盖、甚至是一个 OpenAI 内部的免费试用 API 路径。但有一点可以确定:如果工具只是忠实转发 AT,以 PH 区 ₱990 的定价,结果不可能是 ₱0。 工具是这个攻击链中最不透明的环节。你把 Access Token——等同于你的登录态——交给了一个第三方服务。它用你的身份做了什么请求、传了什么参数,你完全不知道。这也是为什么整个流程的"技术含量"看似很低——核心逻辑都藏在工具服务端里,用户只是按步骤操作。 五、BIN 选择——不是随便什么卡都行 流传的步骤里特别强调了两个 BIN: 523686 (Mastercard)和 4513 (Visa)。这不是随便挑的。 BIN 在支付流中的角色 BIN(Bank Identification Number)是卡号的前 6-8 位,编码了发卡行、卡网络、卡类型和发行国等信息。Stripe 在收到卡号的前 6 位时就能确定: 523686 → Mastercard / Philippines / Credit / 某发卡行 这个信息会被写入 payment_method.card.country ,并在整个支付流中传播。 为什么必须是 PH BIN? PH BIN 的作用不是"触发一个空的价格表"——PH 价格表是存在的(约 ₱990/月)。 更可能的解释是: 货币匹配 :如果工具创建的 Checkout Session 指定了 currency: "php" ,那么 PH 发行的卡才能让 Stripe 的 currency-card 一致性检查通过。US 卡 + PHP 货币会触发额外的风控审查。 风控绕过 :OpenAI/Stripe 的风控系统可能对"card country = PH + checkout currency = PHP"这个组合判定为"正常的本地交易",不会触发跨境支付的额外校验。而如果用 JP 卡或 US 卡来付一笔 0 PHP 的订单,风控更容易拦截。 AVS 绕过 :PH 卡不走 Stripe 的 AVS 地址校验,所以填什么地址都行——这降低了被拒的概率。 523686 和 4513 是经过实测确认能被 Stripe 正确识别为 card.country = "PH" 的 BIN 段。不是所有 PH BIN 都行,因为某些 BIN 可能已被 OpenAI/Stripe 的风控规则标记或黑名单。 为什么要填美国账单地址? 这步看起来矛盾——卡是菲律宾的,地址填美国? 原因是 Stripe 的 AVS(Address Verification Service)只对美国和英国的卡做地址校验。PH 卡不走 AVS,所以你填什么地址都无所谓——但如果你填 PH 地址,可能触发 OpenAI 的额外风控逻辑。填 US 地址是为了"看起来正常",降低被风控拦截的概率。 六、从防御视角看——OpenAI 需要修什么 不管具体是哪种假说成立,防御原则是通用的。 修复方案(从简单到全面) Level 1:Checkout Session 金额校验 def create_checkout_session ( user , plan ): price = get_price ( plan , resolve_country ( user )) if price [ "amount" ] == 0 and plan != "free" : raise ValueError ( "Zero-amount checkout for paid plan" ) # ... 付费产品金额为 0 时直接拒绝创建 session。这应该是支付系统的基本不变量。 Level 3:区域一致性校验 def resolve_country ( user ): signals = { "registration" : user . billing_country , # JP "session_ip" : geoip ( user . current_ip ), # US "card_bin" : None , # 创建 session 时还不知道 } # 如果注册地和 session IP 不一致,以注册地为准 # 不让 card BIN 覆盖定价区域 return signals [ "registration" ] 定价的 country 由一个确定性函数决定,不受 card BIN 影响。Card BIN 的 country 只用于风控信号,不参与定价。 Level 4:Webhook 后置校验 def on_checkout_completed ( event ): session = event . data . object if session . amount_total == 0 and session . mode == "subscription" : # 0 元订阅?不对劲 stripe . Subscription . delete ( session . subscription ) alert ( "Zero-amount subscription detected" , session ) 即使 Checkout Session 创建时金额为 0,在 checkout.session.completed webhook 里也要做最终校验。这是最后一道防线。 七、这个漏洞和之前的几个有什么关系? 放在一起看,这几个月被扒出来的 ChatGPT/Claude 支付漏洞其实形成了一个谱系: 漏洞 攻击层 改了什么 核心缺陷 prorationMode Hook 客户端内存 putInt 的一个参数 Google Play 信任客户端传入的升级策略 焚决 (cassia) HTTP 响应 checkout_flow 字段 前端根据 API 响应选择支付通道,后端不校验 RevenueCat 转移 聚合器 API is_restore + app_user_id 匿名购买 + restore 归属无限转移 API 响应注入 HTTP 响应 eligible_promo_campaigns 优惠码资格校验仅在前端 0 PHP 跨区 (本文) 定价系统 区域信号 + 工具注入 跨区歧义打开异常路径 + checkout 参数校验缺失 注意到了吗?攻击层在不断"上移"。 早期的漏洞需要 Root、需要 Frida、需要改代码——门槛不低。焚决降到了一个油猴脚本的水平。而 0 PHP 攻击连脚本都不需要——纯操作流程,任何人都能复现。 从攻击者的角度看,这是一个自然演进:当客户端层的防御越来越强(代码混淆、完整性校验、证书绑定),攻击者就会转向更高层——业务逻辑层、定价系统层、区域路由层。 这些层的防御往往更薄弱,因为它们不像"加密"“签名"那样有明确的安全框架,它们是业务系统的一部分,安全评审时容易被忽略。 八、更通用的攻击模式——“区域信号碎片化” 0 PHP 攻击其实揭示了一个通用的攻击模式,我把它叫做"区域信号碎片化攻击”(Region Signal Fragmentation)。 攻击模型 victim_system.pricing = f(country) country = resolve( signal_1: registration_country, ← 攻击者控制(注册时选择 VPN 节点) signal_2: session_ip_country, ← 攻击者控制(登录时选择 VPN 节点) signal_3: card_bin_country, ← 攻击者控制(选择特定 BIN 的卡) signal_4: browser_locale, ← 攻击者控制(浏览器设置) signal_5: billing_address, ← 攻击者控制(表单填写) ) 所有五个信号都由攻击者完全控制。如果 resolve() 函数对歧义输入的处理不当——比如进入异常分支后放松了参数校验,或者 fallback 逻辑允许客户端覆盖定价——攻击者就能把定价推入非预期状态。 防御原则 原则一:定价权归一 pricing_country = user.billing_country // 只有一个来源,注册时确定 // card BIN、IP、locale 都不参与定价决策 // 只作为风控辅助信号 原则二:Checkout 参数不可客户端覆盖 # 服务端决定 price、currency、amount # 客户端/第三方传入的 discount、coupon、price_override 一律忽略 # 只接受服务端白名单内的 promotion_code,且校验账号资格 checkout_session = stripe.checkout.Session.create( line_items=[server_determined_price], # 不接受客户端 price_data discounts=[], # 不接受客户端 coupon ) 客户端能影响定价 = 攻击面。 原则三:零元红线 if checkout.amount == 0 and plan.type == "paid": REJECT() // 无条件拒绝 付费产品不可能合法地出现 0 元。这应该是一个硬编码的不变量,不依赖任何业务逻辑。 九、写在最后 从 prorationMode 到焚决到 RevenueCat 到 0 PHP,每一个漏洞都指向同一个结构性问题: 当支付系统由多个独立组件(商店、聚合器、定价引擎、checkout 前端、webhook 后端)拼装而成时,组件之间的信任传递就是攻击面。 prorationMode 的信任传递是"Google Play 信任客户端传入的升级策略"。焚决的信任传递是"前端信任 API 返回的支付通道"。RevenueCat 的信任传递是"聚合器信任 SDK 传入的 app_user_id"。0 PHP 的信任传递是"定价系统信任 card BIN 推断的国家"。 没有一个单点漏洞,都是链路上的信任滑坡。 而防御的核心思路也始终一样:在链路的最末端(服务端 webhook、权益发放点)做最终校验,不信任链路上游传递的任何"结论",只信任可验证的"事实"。 具体到 0 PHP 这个 case:Stripe checkout.session.completed 事件里的 amount_total 是事实, currency 是事实。如果一个 ChatGPT Plus 订阅的 amount_total 是 0——不管前面经历了多少层区域路由和定价查询—— 这就不对 。拦住它。 这比修价格表更重要。因为价格表总有遗漏,但"付费产品不能 0 元"这条规则没有例外。 往期相关 : Google Play 内购 CC Max 漏洞深度拆解 焚决 Claude — 油猴脚本撬开支付大门 GPT Plus 订阅漏洞 — RevenueCat 凭证转移 GPT Plus 订阅漏洞 — Google Play Billing 鉴权缺失

资讯来源

言零的博客

原文链接

打开原文
0 PHP 白嫖 ChatGPT Plus — 跨区定价混淆攻击的完整技术拆解 · AI 资讯 · AIGOOD - AIGOOD