视频加载失败

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

4701 字
24 分钟
0 PHP 白嫖 ChatGPT Plus — 跨区定价混淆攻击的完整技术拆解
微信二维码

联系方式 & 交流群

  • 微信: gzs-47

进微信群请联系博主,各位觉得文章对你有帮助的话可否打赏一些呀~


⚠️ 免责声明:本文仅供安全研究与技术讨论。文中不提供可直接复用的攻击工具或完整利用脚本。利用类似手段对正式服务进行未授权操作属违法行为,后果自负。本文目的是帮助支付系统开发者理解跨区定价攻击面并加固防御。


前言#

这两天群里炸了。

有人甩出一张截图: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。如果这个接口接受客户端传入的 discountcouponpromotion_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 IPUS每次请求实时推断
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.createline_items 参数包含 priceprice_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网络发行国
523686Mastercard菲律宾
4513xxVisa取决于具体发行行

523686 开头的卡填入 Stripe 表单时,Stripe 内部标记 payment_method.card.country = "PH"。这个信号在整个支付流中传播。PH BIN 的作用至少有两个:

  1. 让 PHP 货币的 checkout session 能正常完成——卡的发行国和结账货币匹配
  2. 可能绕过 OpenAI 的 card-country vs account-country 风控规则——PH 卡付 PHP 金额在 Stripe 层面是”正常交易”

三、JP 注册的作用——制造”区域身份分裂”#

你可能会问:为什么不直接在美国注册、美国提 Token、用 PH 卡付?

答案是:直接美区注册的账号,定价系统的行为是确定性的——走 US 价格表,$20 USD,没有歧义可利用。

JP 注册的作用是制造一个区域身份不确定的账号。当账号的 billing_country 是 JP,session IP 是 US,这两个信号已经矛盾了。定价系统在处理这种矛盾时,可能会:

  1. 走入一个异常处理分支,该分支的参数校验比正常路径更宽松
  2. 允许第三方工具注入的额外参数(如 currency、discount)覆盖默认行为
  3. 使用一个与主价格表不同的备用定价逻辑,而这个逻辑有漏洞

用状态机的视角看:

┌──────────────┐
│ 注册时: 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 提炼”,听着玄乎,其实它干的事非常直白:

  1. 接收你的 Access Token
  2. 用这个 AT 调用 OpenAI 后端 API,创建一个 Stripe Checkout Session
  3. 把 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_codediscountcurrency + 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/月)。

更可能的解释是:

  1. 货币匹配:如果工具创建的 Checkout Session 指定了 currency: "php",那么 PH 发行的卡才能让 Stripe 的 currency-card 一致性检查通过。US 卡 + PHP 货币会触发额外的风控审查。

  2. 风控绕过:OpenAI/Stripe 的风控系统可能对”card country = PH + checkout currency = PHP”这个组合判定为”正常的本地交易”,不会触发跨境支付的额外校验。而如果用 JP 卡或 US 卡来付一笔 0 PHP 的订单,风控更容易拦截。

  3. 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 转移聚合器 APIis_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 元”这条规则没有例外。


往期相关

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

相关文章智能推荐
1
提链 — ChatGPT 跨区支付链接提取的完整技术拆解
漏洞分析完整拆解 ChatGPT 提链技术:从 Session Token 出发,通过 12 步协议流程构造 Stripe Checkout Session,最终提取 iDEAL/UPI/PIX/Kakao Pay 等本地支付链接。附带八种支付方式的技术对比和完整代码分析。
2
GPT Plus 订阅漏洞深度分析 - Google Play Billing 鉴权缺失导致 0 元订阅
漏洞分析技术拆解:通过 API 响应注入和 offerToken 内存替换两种方式,利用 Google Play Billing 与 RevenueCat 回调鉴权缺失实现 GPT Plus/Pro 零元订阅。
3
虚拟发卡系统回调伪造漏洞全链路拆解 — 从备份文件泄露到批量盗取卡密
漏洞分析深度分析一起针对 ACG-Faka + BEpusdt 的真实攻击事件:攻击者通过两条路径窃取支付密钥——下载 Web 根目录残留的备份文件,或利用 Nginx 配置错误直读 Config.php 源码——进而伪造合法回调通知批量盗取卡密。附官方安全公告、五站实测审计结果与完整防御方案。
4
焚决 Claude — 一个油猴脚本如何撬开 Anthropic 的支付大门
漏洞分析深入拆解传说中的「焚决」——通过 Tampermonkey 油猴脚本劫持 checkout_capabilities API 响应,前端注入 cassia 支付流,配合随机德国 IBAN 实现 Claude Max/Pro 零元订阅。完整技术分析:Fetch/XHR 双通道拦截、SEPA CORE 直接借记清算机制、MOD 97-10 校验算法局限性、客户端信任边界漏洞的通用攻击框架与防御方案。
5
GPT Plus 订阅转移全解析 - 从 Google Play Token 到一键开通的完整技术链路
安全研究深度剖析 GPT Plus Android 订阅架构:Google Play purchase token 如何通过 RevenueCat 中间层实现跨账号转移,从原理到 ADB 提取、MITM 拦截、API 调用的完整实战指南。
随机文章随机推荐
Profile Image of the Author
47工作室
47工作室:记录技术实践与安全研究。
公告
欢迎来到 47工作室!微信:gzs-47
分类
标签
最新动态
站点统计
文章
39
分类
16
标签
165
总字数
121,285
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
站点 vunknown
文章许可
CC BY-NC-SA 4.0