油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

admin 2025年5月26日19:53:21评论5 views字数 14960阅读49分52秒阅读模式

用JS HOOK来绕过前后端分离的前端鉴权场景

原理是从前端开发的角度去推理绕过的思路:众所周知,前端由我们浏览器展示,相当于所有资源是我们可控的,与服务器响应及配置无关,如果输入一个路由就直接跳转了,什么请求都没触发,我们岂不是不知道接口是否存在未授权的情况?So, We can do it.以VUE为例

应用场景

是否存在这样一种情况你通过VUE Route脚本获取了泄露的路由,但是你拼接路由到URL中的时候,因为没有登录,什么请求都没加载,直接跳转回了/xxx/Login?redirect=/**类似的路径,这是因为前端鉴权了。前端鉴权需要通过审计JS代码来还原鉴权判断条件。

油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

从开发的角度去思考如何鉴权

粗粗地思考

前后端分离的项目中如何去判断你是否登录:

  1. 1. 最常见的是从LocalStorage、SessionStorage之类做存储的key中去读取代表你登录的value
  2. 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控制台,狠狠地阅读是否有堆栈信息。

油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

由此可见,我们只需要为localStorage补两个参数即可:iotems_tokenUser_Info

再浅浅地猜测一下Token必然字符串,随便敲个字符串补上;Info必然是个对象,直接补个{}

油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

如果我们解决了路由守卫的话,那么我们就能抓到接口了,能够触发这样的就说明是接口鉴权。

油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

浅浅看一下流量包:

油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

第一道拦路虎已经解决---------绕过路由的拦截。

全局处理异常响应

众所周知,当接口响应不正确的时候我们可能还是不能让页面正确地加载下去,就抓不到更多的流量包,所以我们要上第二步组合拳,当然我们慢慢地改响应包也可以,或者用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. 1. 递归JSON响应结果,将我们的key/value的值直接换进去
  2. 2. 当特定场景即data: null的时候给data一个空的对象

修改这里的代码

油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

错误的话success: 0,我让他success: 1;错误的话errorCode: "10086",我让他errorCode: "",这个理解起来都不难吧,那么我们看结果。

油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

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来绕过前后端分离的前端鉴权场景(含代码)

懒得放云盘了,云盘老是崩了要重新更链接,代码类以后更这个库里。

原文始发于微信公众号(卫界安全-阿呆攻防):油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月26日19:53:21
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   油猴|用JS HOOK来绕过前后端分离的前端鉴权场景(含代码)https://cn-sec.com/archives/4004831.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息