[{"data":1,"prerenderedAt":571},["ShallowReactive",2],{"/articles/gpt-teachers-workspace-exploit":3},{"id":4,"title":5,"body":6,"date":557,"description":558,"extension":559,"featured":560,"meta":561,"navigation":562,"path":563,"seo":564,"stem":565,"tags":566,"__hash__":570},"articles/articles/gpt-teachers-workspace-exploit.md","无限爽毛K12?GPT For Teachers工作区漏洞的小记",{"type":7,"value":8,"toc":544},"minimark",[9,17,21,24,27,39,52,55,58,63,70,122,141,152,156,159,228,231,257,261,264,343,357,362,365,380,385,396,400,403,412,415,439,442,445,453,456,487,490,493,504,507,526,529,540],[10,11,12,16],"p",{},[13,14,15],"span",{},"!TIP","\n文章由 LongCat-2.0 生成。",[18,19,20],"h2",{"id":20},"背景",[10,22,23],{},"最近网上冲浪的时候发现了一个强行加入 K12 空间的脚本。因好奇其实现原理，遂进行分析，发现它利用了 ChatGPT 平台的一系列安全缺陷来实现「无限K12」的效果。",[18,25,26],{"id":26},"脚本概述",[10,28,29,30,34,35,38],{},"该脚本是一个浏览器 Userscript（通过 Tampermonkey 等扩展注入），运行在 ",[31,32,33],"code",{},"https://chatgpt.com/*"," 页面上（",[31,36,37],{},"@run-at document-idle","）。安装后会自动在页面右上角注入一个浮层面板，提供以下功能：",[40,41,42,46,49],"ul",{},[43,44,45],"li",{},"自动获取当前登录账号的 Session Token（AT）",[43,47,48],{},"向指定的多个 Workspace 发送「加入申请」（request）",[43,50,51],{},"自动接受已发出的邀请（accept）",[10,53,54],{},"默认配置中硬编码了十几个 Workspace ID，批量操作为母号（管理员）和子号（被加入的账号）之间建立关联。",[18,56,57],{"id":57},"核心漏洞分析",[59,60,62],"h3",{"id":61},"_1-ataccess-token窃取","1. AT（Access Token）窃取",[10,64,65,66,69],{},"脚本的核心入口是 ",[31,67,68],{},"fetchSession()"," 函数：",[71,72,77],"pre",{"className":73,"code":74,"language":75,"meta":76,"style":76},"language-javascript shiki shiki-themes github-light github-dark","async function fetchSession() {\n  const res = await fetch(\"/api/auth/session\", {\n    headers: { accept: \"*/*\" },\n    credentials: \"include\",\n  });\n  return await res.json();\n}\n","javascript","",[31,78,79,86,92,98,104,110,116],{"__ignoreMap":76},[13,80,83],{"class":81,"line":82},"line",1,[13,84,85],{},"async function fetchSession() {\n",[13,87,89],{"class":81,"line":88},2,[13,90,91],{},"  const res = await fetch(\"/api/auth/session\", {\n",[13,93,95],{"class":81,"line":94},3,[13,96,97],{},"    headers: { accept: \"*/*\" },\n",[13,99,101],{"class":81,"line":100},4,[13,102,103],{},"    credentials: \"include\",\n",[13,105,107],{"class":81,"line":106},5,[13,108,109],{},"  });\n",[13,111,113],{"class":81,"line":112},6,[13,114,115],{},"  return await res.json();\n",[13,117,119],{"class":81,"line":118},7,[13,120,121],{},"}\n",[10,123,124,125,128,129,132,133,136,137,140],{},"这里利用了 ChatGPT 的 ",[31,126,127],{},"/api/auth/session"," 端点——该接口返回当前用户的完整会话信息，包括 ",[31,130,131],{},"accessToken","（AT）。由于浏览器 Same-Origin Policy 允许同源页面发起带 ",[31,134,135],{},"credentials: \"include\""," 的请求，任何注入到 ",[31,138,139],{},"chatgpt.com"," 的 JavaScript 代码都可以无感知地获取到当前登录用户的 Token。",[10,142,143,147,148,151],{},[144,145,146],"strong",{},"漏洞本质："," 服务端没有在 Token 传输层做足够的保护（如 ",[31,149,150],{},"httpOnly"," Cookie 绑定、Token 指纹绑定等），使得前端 JS 可以直接读取到高权限的 Access Token。",[59,153,155],{"id":154},"_2-jwt-结构解析","2. JWT 结构解析",[10,157,158],{},"获取到 AT 后，脚本进一步解析 JWT 载荷：",[71,160,162],{"className":73,"code":161,"language":75,"meta":76,"style":76},"function decodeJwt(at) {\n  const payload = at.split(\".\")[1];\n  const json = JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n  const auth = json[\"https://api.openai.com/auth\"] || {};\n  const prof = json[\"https://api.openai.com/profile\"] || {};\n  return {\n    account_id: auth.chatgpt_account_id || \"\",\n    email: prof.email || \"\",\n    plan_type: auth.chatgpt_plan_type || \"\",\n    exp: json.exp || 0,\n  };\n}\n",[31,163,164,169,174,179,184,189,194,199,205,211,217,223],{"__ignoreMap":76},[13,165,166],{"class":81,"line":82},[13,167,168],{},"function decodeJwt(at) {\n",[13,170,171],{"class":81,"line":88},[13,172,173],{},"  const payload = at.split(\".\")[1];\n",[13,175,176],{"class":81,"line":94},[13,177,178],{},"  const json = JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n",[13,180,181],{"class":81,"line":100},[13,182,183],{},"  const auth = json[\"https://api.openai.com/auth\"] || {};\n",[13,185,186],{"class":81,"line":106},[13,187,188],{},"  const prof = json[\"https://api.openai.com/profile\"] || {};\n",[13,190,191],{"class":81,"line":112},[13,192,193],{},"  return {\n",[13,195,196],{"class":81,"line":118},[13,197,198],{},"    account_id: auth.chatgpt_account_id || \"\",\n",[13,200,202],{"class":81,"line":201},8,[13,203,204],{},"    email: prof.email || \"\",\n",[13,206,208],{"class":81,"line":207},9,[13,209,210],{},"    plan_type: auth.chatgpt_plan_type || \"\",\n",[13,212,214],{"class":81,"line":213},10,[13,215,216],{},"    exp: json.exp || 0,\n",[13,218,220],{"class":81,"line":219},11,[13,221,222],{},"  };\n",[13,224,226],{"class":81,"line":225},12,[13,227,121],{},[10,229,230],{},"这说明 AT 是一个标准的 JWT，载荷中包含：",[40,232,233,239,245,251],{},[43,234,235,238],{},[31,236,237],{},"https://api.openai.com/auth.chatgpt_account_id","：账号唯一标识",[43,240,241,244],{},[31,242,243],{},"https://api.openai.com/profile.email","：用户邮箱",[43,246,247,250],{},[31,248,249],{},"chatgpt_plan_type","：订阅类型",[43,252,253,256],{},[31,254,255],{},"exp","：过期时间",[59,258,260],{"id":259},"_3-越权加入工作区","3. 越权加入工作区",[10,262,263],{},"有了 AT 后，脚本直接调用 Workspace 管理 API：",[71,265,267],{"className":73,"code":266,"language":75,"meta":76,"style":76},"async function sendOne(wsId, route, attempt = 0) {\n  const url = `/backend-api/accounts/${wsId}/invites/${route}`;\n  const headers = {\n    authorization: `Bearer ${STATE.at}`,\n    \"oai-device-id\": STATE.deviceId,\n    \"oai-language\": navigator.language || \"en-US\",\n  };\n  const res = await fetch(url, {\n    method: \"POST\",\n    headers,\n    body: null,\n    credentials: \"include\",\n  });\n  // ...\n}\n",[31,268,269,274,279,284,289,294,299,303,308,313,318,323,327,332,338],{"__ignoreMap":76},[13,270,271],{"class":81,"line":82},[13,272,273],{},"async function sendOne(wsId, route, attempt = 0) {\n",[13,275,276],{"class":81,"line":88},[13,277,278],{},"  const url = `/backend-api/accounts/${wsId}/invites/${route}`;\n",[13,280,281],{"class":81,"line":94},[13,282,283],{},"  const headers = {\n",[13,285,286],{"class":81,"line":100},[13,287,288],{},"    authorization: `Bearer ${STATE.at}`,\n",[13,290,291],{"class":81,"line":106},[13,292,293],{},"    \"oai-device-id\": STATE.deviceId,\n",[13,295,296],{"class":81,"line":112},[13,297,298],{},"    \"oai-language\": navigator.language || \"en-US\",\n",[13,300,301],{"class":81,"line":118},[13,302,222],{},[13,304,305],{"class":81,"line":201},[13,306,307],{},"  const res = await fetch(url, {\n",[13,309,310],{"class":81,"line":207},[13,311,312],{},"    method: \"POST\",\n",[13,314,315],{"class":81,"line":213},[13,316,317],{},"    headers,\n",[13,319,320],{"class":81,"line":219},[13,321,322],{},"    body: null,\n",[13,324,325],{"class":81,"line":225},[13,326,103],{},[13,328,330],{"class":81,"line":329},13,[13,331,109],{},[13,333,335],{"class":81,"line":334},14,[13,336,337],{},"  // ...\n",[13,339,341],{"class":81,"line":340},15,[13,342,121],{},[10,344,345,348,349,352,353,356],{},[31,346,347],{},"route"," 参数可以是 ",[31,350,351],{},"\"request\"","（申请加入）或 ",[31,354,355],{},"\"accept\"","（接受邀请）。这里的关键问题在于：",[10,358,359],{},[144,360,361],{},"漏洞一：AT 直接用于跨账号操作",[10,363,364],{},"Bearer Token 直接作为身份凭证，服务端只验证 Token 本身的有效性（是否过期、签名是否正确），而没有校验操作意图的合法性。这意味着：",[40,366,367,370,377],{},[43,368,369],{},"子号的 AT 可以用于向任意 Workspace 发起加入请求",[43,371,372,373,376],{},"母号的 AT 可以通过 ",[31,374,375],{},"accept"," 接口直接接受邀请，无需二次确认",[43,378,379],{},"整个流程没有任何 CSRF Token、确认步骤或人机验证",[10,381,382],{},[144,383,384],{},"漏洞二：无速率限制与设备指纹绕过",[10,386,387,388,391,392,395],{},"脚本使用了 ",[31,389,390],{},"crypto.randomUUID()"," 生成固定的 ",[31,393,394],{},"oai-device-id","，并在请求头中发送。这种客户端生成的设备标识符显然无法作为有效的风控依据，攻击者可以随意伪造。",[59,397,399],{"id":398},"_4-token-自动刷新机制","4. Token 自动刷新机制",[10,401,402],{},"脚本还实现了自动刷新机制，保证 AT 持续有效：",[71,404,406],{"className":73,"code":405,"language":75,"meta":76,"style":76},"setInterval(refreshSession, CONFIG.sessionPollMs); // 每20秒刷新一次\n",[31,407,408],{"__ignoreMap":76},[13,409,410],{"class":81,"line":82},[13,411,405],{},[10,413,414],{},"当检测到 401/403 响应时，脚本会主动清空 AT 并重新获取：",[71,416,418],{"className":73,"code":417,"language":75,"meta":76,"style":76},"if (res.status === 401 || res.status === 403) {\n  STATE.at = \"\";\n  await refreshSession();\n}\n",[31,419,420,425,430,435],{"__ignoreMap":76},[13,421,422],{"class":81,"line":82},[13,423,424],{},"if (res.status === 401 || res.status === 403) {\n",[13,426,427],{"class":81,"line":88},[13,428,429],{},"  STATE.at = \"\";\n",[13,431,432],{"class":81,"line":94},[13,433,434],{},"  await refreshSession();\n",[13,436,437],{"class":81,"line":100},[13,438,121],{},[10,440,441],{},"这意味着即使原始 Token 过期或被吊销，只要用户保持登录状态，脚本就能持续获取新的有效 Token 来维持攻击。",[18,443,444],{"id":444},"攻击链总结",[71,446,451],{"className":447,"code":449,"language":450},[448],"language-text","用户在 chatgpt.com 登录\n       ↓\nTampermonkey 注入 Userscript\n       ↓\nfetch(\"/api/auth/session\") → 窃取 AT\n       ↓\nJWT 解析 → 提取 account_id、email、plan_type\n       ↓\nPOST /backend-api/accounts/{workspaceId}/invites/request\n       ↓\n向多个目标 Workspace 批量发送加入申请\n       ↓\nPOST /backend-api/accounts/{workspaceId}/invites/accept\n       ↓\n自动接受邀请，子号进入母号的工作区\n       ↓\n每 20 秒刷新 Session，维持持久访问\n","text",[31,452,449],{"__ignoreMap":76},[18,454,455],{"id":455},"影响与危害",[457,458,459,465,471,477],"ol",{},[43,460,461,464],{},[144,462,463],{},"账号劫持风险","：任何运行该脚本的页面都可以窃取用户的 AT，用于冒充用户身份执行任意 API 操作",[43,466,467,470],{},[144,468,469],{},"工作区权限滥用","：子号可以在不知情的情况下被批量加入工作区，母号可以无审核地扩张团队规模",[43,472,473,476],{},[144,474,475],{},"批量注册/滥用","：结合自动化注册工具，可以低成本地批量创建账号并加入工作区，绕过付费限制",[43,478,479,482,483,486],{},[144,480,481],{},"供应链攻击面","：Tampermonkey 脚本的 ",[31,484,485],{},"@grant none"," 模式意味着脚本运行在页面主上下文中，拥有与页面 JS 同等的权限",[18,488,489],{"id":489},"修复建议",[10,491,492],{},"从空间管理者角度：",[40,494,495,498,501],{},[43,496,497],{},"定期审查空间成员列表，移除不认识或不活跃的账号",[43,499,500],{},"废弃被泄漏的空间",[43,502,503],{},"关闭不需要的 Codex 权限",[10,505,506],{},"从平台角度：",[40,508,509,517,520,523],{},[43,510,511,513,514,516],{},[31,512,127],{}," 返回的 AT 应限制为短期有效，或改用 ",[31,515,150],{}," Cookie 方案",[43,518,519],{},"Workspace 操作 API 应增加人机验证和意图确认",[43,521,522],{},"引入设备指纹绑定，使窃取的 Token 在其他设备上无法使用",[43,524,525],{},"增加 API 速率限制和异常行为检测",[10,527,528],{},"从用户角度：",[40,530,531,534,537],{},[43,532,533],{},"不要安装来源不明的 Tampermonkey 脚本",[43,535,536],{},"定期检查 ChatGPT 账号的登录活动和工作区成员列表",[43,538,539],{},"使用浏览器容器隔离不同站点的登录状态",[541,542,543],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":76,"searchDepth":88,"depth":88,"links":545},[546,547,548,554,555,556],{"id":20,"depth":88,"text":20},{"id":26,"depth":88,"text":26},{"id":57,"depth":88,"text":57,"children":549},[550,551,552,553],{"id":61,"depth":94,"text":62},{"id":154,"depth":94,"text":155},{"id":259,"depth":94,"text":260},{"id":398,"depth":94,"text":399},{"id":444,"depth":88,"text":444},{"id":455,"depth":88,"text":455},{"id":489,"depth":88,"text":489},"2026-07-05","对某 ChatGPT Workspace 自动化加入脚本的核心漏洞利用逻辑进行技术分析——AT 窃取、越权加入工作区","md",false,{},true,"/articles/gpt-teachers-workspace-exploit",{"title":5,"description":558},"articles/gpt-teachers-workspace-exploit",[567,568,569],"安全","ChatGPT","漏洞分析","1VI5HwznKs1nNHM5YpPzq9JZhNqXM-omeCk5YMkeYkI",1783865040173]