[{"data":1,"prerenderedAt":947},["ShallowReactive",2],{"/articles/gpt-teachers-workspace-exploit":3},{"id":4,"title":5,"body":6,"date":932,"description":933,"extension":934,"featured":935,"meta":936,"navigation":937,"path":938,"seo":939,"stem":940,"tags":941,"tip":945,"__hash__":946},"articles/articles/gpt-teachers-workspace-exploit.md","无限爽毛K12?GPT For Teachers工作区漏洞的小记",{"type":7,"value":8,"toc":919},"minimark",[9,13,17,20,32,45,48,51,56,63,174,193,204,208,211,446,449,475,479,482,669,682,687,690,705,710,721,725,728,750,753,814,817,820,828,831,862,865,868,879,882,901,904,915],[10,11,12],"h2",{"id":12},"背景",[14,15,16],"p",{},"最近网上冲浪的时候发现了一个强行加入 K12 空间的脚本。因好奇其实现原理，遂进行分析，发现它利用了 ChatGPT 平台的一系列安全缺陷来实现「无限K12」的效果。",[10,18,19],{"id":19},"脚本概述",[14,21,22,23,27,28,31],{},"该脚本是一个浏览器 Userscript（通过 Tampermonkey 等扩展注入），运行在 ",[24,25,26],"code",{},"https://chatgpt.com/*"," 页面上（",[24,29,30],{},"@run-at document-idle","）。安装后会自动在页面右上角注入一个浮层面板，提供以下功能：",[33,34,35,39,42],"ul",{},[36,37,38],"li",{},"自动获取当前登录账号的 Session Token（AT）",[36,40,41],{},"向指定的多个 Workspace 发送「加入申请」（request）",[36,43,44],{},"自动接受已发出的邀请（accept）",[14,46,47],{},"默认配置中硬编码了十几个 Workspace ID，批量操作为母号（管理员）和子号（被加入的账号）之间建立关联。",[10,49,50],{"id":50},"核心漏洞分析",[52,53,55],"h3",{"id":54},"_1-ataccess-token窃取","1. AT（Access Token）窃取",[14,57,58,59,62],{},"脚本的核心入口是 ",[24,60,61],{},"fetchSession()"," 函数：",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"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","",[24,71,72,92,121,133,145,151,168],{"__ignoreMap":69},[73,74,77,81,84,88],"span",{"class":75,"line":76},"line",1,[73,78,80],{"class":79},"szBVR","async",[73,82,83],{"class":79}," function",[73,85,87],{"class":86},"sScJk"," fetchSession",[73,89,91],{"class":90},"sVt8B","() {\n",[73,93,95,98,102,105,108,111,114,118],{"class":75,"line":94},2,[73,96,97],{"class":79},"  const",[73,99,101],{"class":100},"sj4cs"," res",[73,103,104],{"class":79}," =",[73,106,107],{"class":79}," await",[73,109,110],{"class":86}," fetch",[73,112,113],{"class":90},"(",[73,115,117],{"class":116},"sZZnC","\"/api/auth/session\"",[73,119,120],{"class":90},", {\n",[73,122,124,127,130],{"class":75,"line":123},3,[73,125,126],{"class":90},"    headers: { accept: ",[73,128,129],{"class":116},"\"*/*\"",[73,131,132],{"class":90}," },\n",[73,134,136,139,142],{"class":75,"line":135},4,[73,137,138],{"class":90},"    credentials: ",[73,140,141],{"class":116},"\"include\"",[73,143,144],{"class":90},",\n",[73,146,148],{"class":75,"line":147},5,[73,149,150],{"class":90},"  });\n",[73,152,154,157,159,162,165],{"class":75,"line":153},6,[73,155,156],{"class":79},"  return",[73,158,107],{"class":79},[73,160,161],{"class":90}," res.",[73,163,164],{"class":86},"json",[73,166,167],{"class":90},"();\n",[73,169,171],{"class":75,"line":170},7,[73,172,173],{"class":90},"}\n",[14,175,176,177,180,181,184,185,188,189,192],{},"这里利用了 ChatGPT 的 ",[24,178,179],{},"/api/auth/session"," 端点——该接口返回当前用户的完整会话信息，包括 ",[24,182,183],{},"accessToken","（AT）。由于浏览器 Same-Origin Policy 允许同源页面发起带 ",[24,186,187],{},"credentials: \"include\""," 的请求，任何注入到 ",[24,190,191],{},"chatgpt.com"," 的 JavaScript 代码都可以无感知地获取到当前登录用户的 Token。",[14,194,195,199,200,203],{},[196,197,198],"strong",{},"漏洞本质："," 服务端没有在 Token 传输层做足够的保护（如 ",[24,201,202],{},"httpOnly"," Cookie 绑定、Token 指纹绑定等），使得前端 JS 可以直接读取到高权限的 Access Token。",[52,205,207],{"id":206},"_2-jwt-结构解析","2. JWT 结构解析",[14,209,210],{},"获取到 AT 后，脚本进一步解析 JWT 载荷：",[64,212,214],{"className":66,"code":213,"language":68,"meta":69,"style":69},"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",[24,215,216,233,262,335,359,379,386,398,410,422,435,441],{"__ignoreMap":69},[73,217,218,221,224,226,230],{"class":75,"line":76},[73,219,220],{"class":79},"function",[73,222,223],{"class":86}," decodeJwt",[73,225,113],{"class":90},[73,227,229],{"class":228},"s4XuR","at",[73,231,232],{"class":90},") {\n",[73,234,235,237,240,242,245,248,250,253,256,259],{"class":75,"line":94},[73,236,97],{"class":79},[73,238,239],{"class":100}," payload",[73,241,104],{"class":79},[73,243,244],{"class":90}," at.",[73,246,247],{"class":86},"split",[73,249,113],{"class":90},[73,251,252],{"class":116},"\".\"",[73,254,255],{"class":90},")[",[73,257,258],{"class":100},"1",[73,260,261],{"class":90},"];\n",[73,263,264,266,269,271,274,277,280,282,285,288,291,293,296,300,302,305,308,311,314,316,318,320,323,325,327,329,332],{"class":75,"line":123},[73,265,97],{"class":79},[73,267,268],{"class":100}," json",[73,270,104],{"class":79},[73,272,273],{"class":100}," JSON",[73,275,276],{"class":90},".",[73,278,279],{"class":86},"parse",[73,281,113],{"class":90},[73,283,284],{"class":86},"atob",[73,286,287],{"class":90},"(payload.",[73,289,290],{"class":86},"replace",[73,292,113],{"class":90},[73,294,295],{"class":116},"/",[73,297,299],{"class":298},"sA_wV","-",[73,301,295],{"class":116},[73,303,304],{"class":79},"g",[73,306,307],{"class":90},", ",[73,309,310],{"class":116},"\"+\"",[73,312,313],{"class":90},").",[73,315,290],{"class":86},[73,317,113],{"class":90},[73,319,295],{"class":116},[73,321,322],{"class":298},"_",[73,324,295],{"class":116},[73,326,304],{"class":79},[73,328,307],{"class":90},[73,330,331],{"class":116},"\"/\"",[73,333,334],{"class":90},")));\n",[73,336,337,339,342,344,347,350,353,356],{"class":75,"line":135},[73,338,97],{"class":79},[73,340,341],{"class":100}," auth",[73,343,104],{"class":79},[73,345,346],{"class":90}," json[",[73,348,349],{"class":116},"\"https://api.openai.com/auth\"",[73,351,352],{"class":90},"] ",[73,354,355],{"class":79},"||",[73,357,358],{"class":90}," {};\n",[73,360,361,363,366,368,370,373,375,377],{"class":75,"line":147},[73,362,97],{"class":79},[73,364,365],{"class":100}," prof",[73,367,104],{"class":79},[73,369,346],{"class":90},[73,371,372],{"class":116},"\"https://api.openai.com/profile\"",[73,374,352],{"class":90},[73,376,355],{"class":79},[73,378,358],{"class":90},[73,380,381,383],{"class":75,"line":153},[73,382,156],{"class":79},[73,384,385],{"class":90}," {\n",[73,387,388,391,393,396],{"class":75,"line":170},[73,389,390],{"class":90},"    account_id: auth.chatgpt_account_id ",[73,392,355],{"class":79},[73,394,395],{"class":116}," \"\"",[73,397,144],{"class":90},[73,399,401,404,406,408],{"class":75,"line":400},8,[73,402,403],{"class":90},"    email: prof.email ",[73,405,355],{"class":79},[73,407,395],{"class":116},[73,409,144],{"class":90},[73,411,413,416,418,420],{"class":75,"line":412},9,[73,414,415],{"class":90},"    plan_type: auth.chatgpt_plan_type ",[73,417,355],{"class":79},[73,419,395],{"class":116},[73,421,144],{"class":90},[73,423,425,428,430,433],{"class":75,"line":424},10,[73,426,427],{"class":90},"    exp: json.exp ",[73,429,355],{"class":79},[73,431,432],{"class":100}," 0",[73,434,144],{"class":90},[73,436,438],{"class":75,"line":437},11,[73,439,440],{"class":90},"  };\n",[73,442,444],{"class":75,"line":443},12,[73,445,173],{"class":90},[14,447,448],{},"这说明 AT 是一个标准的 JWT，载荷中包含：",[33,450,451,457,463,469],{},[36,452,453,456],{},[24,454,455],{},"https://api.openai.com/auth.chatgpt_account_id","：账号唯一标识",[36,458,459,462],{},[24,460,461],{},"https://api.openai.com/profile.email","：用户邮箱",[36,464,465,468],{},[24,466,467],{},"chatgpt_plan_type","：订阅类型",[36,470,471,474],{},[24,472,473],{},"exp","：过期时间",[52,476,478],{"id":477},"_3-越权加入工作区","3. 越权加入工作区",[14,480,481],{},"有了 AT 后，脚本直接调用 Workspace 管理 API：",[64,483,485],{"className":66,"code":484,"language":68,"meta":69,"style":69},"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",[24,486,487,517,542,553,572,585,600,604,619,629,634,644,652,657,664],{"__ignoreMap":69},[73,488,489,491,493,496,498,501,503,506,508,511,513,515],{"class":75,"line":76},[73,490,80],{"class":79},[73,492,83],{"class":79},[73,494,495],{"class":86}," sendOne",[73,497,113],{"class":90},[73,499,500],{"class":228},"wsId",[73,502,307],{"class":90},[73,504,505],{"class":228},"route",[73,507,307],{"class":90},[73,509,510],{"class":228},"attempt",[73,512,104],{"class":79},[73,514,432],{"class":100},[73,516,232],{"class":90},[73,518,519,521,524,526,529,531,534,536,539],{"class":75,"line":94},[73,520,97],{"class":79},[73,522,523],{"class":100}," url",[73,525,104],{"class":79},[73,527,528],{"class":116}," `/backend-api/accounts/${",[73,530,500],{"class":90},[73,532,533],{"class":116},"}/invites/${",[73,535,505],{"class":90},[73,537,538],{"class":116},"}`",[73,540,541],{"class":90},";\n",[73,543,544,546,549,551],{"class":75,"line":123},[73,545,97],{"class":79},[73,547,548],{"class":100}," headers",[73,550,104],{"class":79},[73,552,385],{"class":90},[73,554,555,558,561,564,566,568,570],{"class":75,"line":135},[73,556,557],{"class":90},"    authorization: ",[73,559,560],{"class":116},"`Bearer ${",[73,562,563],{"class":100},"STATE",[73,565,276],{"class":116},[73,567,229],{"class":90},[73,569,538],{"class":116},[73,571,144],{"class":90},[73,573,574,577,580,582],{"class":75,"line":147},[73,575,576],{"class":116},"    \"oai-device-id\"",[73,578,579],{"class":90},": ",[73,581,563],{"class":100},[73,583,584],{"class":90},".deviceId,\n",[73,586,587,590,593,595,598],{"class":75,"line":153},[73,588,589],{"class":116},"    \"oai-language\"",[73,591,592],{"class":90},": navigator.language ",[73,594,355],{"class":79},[73,596,597],{"class":116}," \"en-US\"",[73,599,144],{"class":90},[73,601,602],{"class":75,"line":170},[73,603,440],{"class":90},[73,605,606,608,610,612,614,616],{"class":75,"line":400},[73,607,97],{"class":79},[73,609,101],{"class":100},[73,611,104],{"class":79},[73,613,107],{"class":79},[73,615,110],{"class":86},[73,617,618],{"class":90},"(url, {\n",[73,620,621,624,627],{"class":75,"line":412},[73,622,623],{"class":90},"    method: ",[73,625,626],{"class":116},"\"POST\"",[73,628,144],{"class":90},[73,630,631],{"class":75,"line":424},[73,632,633],{"class":90},"    headers,\n",[73,635,636,639,642],{"class":75,"line":437},[73,637,638],{"class":90},"    body: ",[73,640,641],{"class":100},"null",[73,643,144],{"class":90},[73,645,646,648,650],{"class":75,"line":443},[73,647,138],{"class":90},[73,649,141],{"class":116},[73,651,144],{"class":90},[73,653,655],{"class":75,"line":654},13,[73,656,150],{"class":90},[73,658,660],{"class":75,"line":659},14,[73,661,663],{"class":662},"sJ8bj","  // ...\n",[73,665,667],{"class":75,"line":666},15,[73,668,173],{"class":90},[14,670,671,673,674,677,678,681],{},[24,672,505],{}," 参数可以是 ",[24,675,676],{},"\"request\"","（申请加入）或 ",[24,679,680],{},"\"accept\"","（接受邀请）。这里的关键问题在于：",[14,683,684],{},[196,685,686],{},"漏洞一：AT 直接用于跨账号操作",[14,688,689],{},"Bearer Token 直接作为身份凭证，服务端只验证 Token 本身的有效性（是否过期、签名是否正确），而没有校验操作意图的合法性。这意味着：",[33,691,692,695,702],{},[36,693,694],{},"子号的 AT 可以用于向任意 Workspace 发起加入请求",[36,696,697,698,701],{},"母号的 AT 可以通过 ",[24,699,700],{},"accept"," 接口直接接受邀请，无需二次确认",[36,703,704],{},"整个流程没有任何 CSRF Token、确认步骤或人机验证",[14,706,707],{},[196,708,709],{},"漏洞二：无速率限制与设备指纹绕过",[14,711,712,713,716,717,720],{},"脚本使用了 ",[24,714,715],{},"crypto.randomUUID()"," 生成固定的 ",[24,718,719],{},"oai-device-id","，并在请求头中发送。这种客户端生成的设备标识符显然无法作为有效的风控依据，攻击者可以随意伪造。",[52,722,724],{"id":723},"_4-token-自动刷新机制","4. Token 自动刷新机制",[14,726,727],{},"脚本还实现了自动刷新机制，保证 AT 持续有效：",[64,729,731],{"className":66,"code":730,"language":68,"meta":69,"style":69},"setInterval(refreshSession, CONFIG.sessionPollMs); // 每20秒刷新一次\n",[24,732,733],{"__ignoreMap":69},[73,734,735,738,741,744,747],{"class":75,"line":76},[73,736,737],{"class":86},"setInterval",[73,739,740],{"class":90},"(refreshSession, ",[73,742,743],{"class":100},"CONFIG",[73,745,746],{"class":90},".sessionPollMs); ",[73,748,749],{"class":662},"// 每20秒刷新一次\n",[14,751,752],{},"当检测到 401/403 响应时，脚本会主动清空 AT 并重新获取：",[64,754,756],{"className":66,"code":755,"language":68,"meta":69,"style":69},"if (res.status === 401 || res.status === 403) {\n  STATE.at = \"\";\n  await refreshSession();\n}\n",[24,757,758,785,800,810],{"__ignoreMap":69},[73,759,760,763,766,769,772,775,778,780,783],{"class":75,"line":76},[73,761,762],{"class":79},"if",[73,764,765],{"class":90}," (res.status ",[73,767,768],{"class":79},"===",[73,770,771],{"class":100}," 401",[73,773,774],{"class":79}," ||",[73,776,777],{"class":90}," res.status ",[73,779,768],{"class":79},[73,781,782],{"class":100}," 403",[73,784,232],{"class":90},[73,786,787,790,793,796,798],{"class":75,"line":94},[73,788,789],{"class":100},"  STATE",[73,791,792],{"class":90},".at ",[73,794,795],{"class":79},"=",[73,797,395],{"class":116},[73,799,541],{"class":90},[73,801,802,805,808],{"class":75,"line":123},[73,803,804],{"class":79},"  await",[73,806,807],{"class":86}," refreshSession",[73,809,167],{"class":90},[73,811,812],{"class":75,"line":135},[73,813,173],{"class":90},[14,815,816],{},"这意味着即使原始 Token 过期或被吊销，只要用户保持登录状态，脚本就能持续获取新的有效 Token 来维持攻击。",[10,818,819],{"id":819},"攻击链总结",[64,821,826],{"className":822,"code":824,"language":825},[823],"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",[24,827,824],{"__ignoreMap":69},[10,829,830],{"id":830},"影响与危害",[832,833,834,840,846,852],"ol",{},[36,835,836,839],{},[196,837,838],{},"账号劫持风险","：任何运行该脚本的页面都可以窃取用户的 AT，用于冒充用户身份执行任意 API 操作",[36,841,842,845],{},[196,843,844],{},"工作区权限滥用","：子号可以在不知情的情况下被批量加入工作区，母号可以无审核地扩张团队规模",[36,847,848,851],{},[196,849,850],{},"批量注册/滥用","：结合自动化注册工具，可以低成本地批量创建账号并加入工作区，绕过付费限制",[36,853,854,857,858,861],{},[196,855,856],{},"供应链攻击面","：Tampermonkey 脚本的 ",[24,859,860],{},"@grant none"," 模式意味着脚本运行在页面主上下文中，拥有与页面 JS 同等的权限",[10,863,864],{"id":864},"修复建议",[14,866,867],{},"从空间管理者角度：",[33,869,870,873,876],{},[36,871,872],{},"定期审查空间成员列表，移除不认识或不活跃的账号",[36,874,875],{},"废弃被泄漏的空间",[36,877,878],{},"关闭不需要的 Codex 权限",[14,880,881],{},"从平台角度：",[33,883,884,892,895,898],{},[36,885,886,888,889,891],{},[24,887,179],{}," 返回的 AT 应限制为短期有效，或改用 ",[24,890,202],{}," Cookie 方案",[36,893,894],{},"Workspace 操作 API 应增加人机验证和意图确认",[36,896,897],{},"引入设备指纹绑定，使窃取的 Token 在其他设备上无法使用",[36,899,900],{},"增加 API 速率限制和异常行为检测",[14,902,903],{},"从用户角度：",[33,905,906,909,912],{},[36,907,908],{},"不要安装来源不明的 Tampermonkey 脚本",[36,910,911],{},"定期检查 ChatGPT 账号的登录活动和工作区成员列表",[36,913,914],{},"使用浏览器容器隔离不同站点的登录状态",[916,917,918],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":69,"searchDepth":94,"depth":94,"links":920},[921,922,923,929,930,931],{"id":12,"depth":94,"text":12},{"id":19,"depth":94,"text":19},{"id":50,"depth":94,"text":50,"children":924},[925,926,927,928],{"id":54,"depth":123,"text":55},{"id":206,"depth":123,"text":207},{"id":477,"depth":123,"text":478},{"id":723,"depth":123,"text":724},{"id":819,"depth":94,"text":819},{"id":830,"depth":94,"text":830},{"id":864,"depth":94,"text":864},"2026-07-05","对某 ChatGPT Workspace 自动化加入脚本的核心漏洞利用逻辑进行技术分析——AT 窃取、越权加入工作区","md",false,{},true,"/articles/gpt-teachers-workspace-exploit",{"title":5,"description":933},"articles/gpt-teachers-workspace-exploit",[942,943,944],"安全","ChatGPT","漏洞分析","本文由 LongCat-2.0 生成。","rQx5gfR1-2MMc_e3wikxJjUWMH2EwuSVDmrZZZLv_-E",1787216606324]