标题: WEB前端逆向拦截页面跳转 创建: 2024-12-05 10:07 更新: 2024-12-11 09:46 链接: https://scz.617.cn/web/202412051007.txt https://mp.weixin.qq.com/s/3N71L8Bqh3drc3IkF59Wgw -------------------------------------------------------------------------- 目录: ☆ 背景介绍 ☆ js跳转页面 ☆ Object.defineProperty (失败) ☆ 渣浪求助 ☆ beforeunload事件 1) 方案1 (较重) 2) 方案2 ☆ navigate事件 (推荐) ☆ AI回答 ☆ 其他「js跳转页面」方案 1) window.open 2) window.location.assign/replace 3) window.history.pushState/replaceState/go 3.1) history栈 ☆ 其他讨论 1) Tampermonkey要求Developer mode 2) debug(func) ☆ 满足原始需求 ☆ 扯淡 -------------------------------------------------------------------------- ☆ 背景介绍 云海碰上个URL,没开F12的情况下,访问中途会自动跳转到「aboub:blank」。起初 因为其他原因,他没往js反调试上想,出于对浏览器二进制漏洞的敏感,想别的方向 去了。 我看了一下目标URL,用「Script First Statement」断点一个个看过去 -------------------------------------------------------------------------- Event Listener Breakpoints Script Script First Statement -------------------------------------------------------------------------- 确认跳转发生时流程位于一个混淆过的js中,obfuscation那种风格的。应该就是反 调试代码干的,不知为何误判在F12调试,从而跳转。Babel反混淆js,再Overrides, 应该能看出误判原因。这个站有其他选择,不必面对自动跳转,我们也是正常访问, 又不是要搞它,所以不考虑反混淆js细究。 我想快速断在目标js中,找到触发跳转的js代码,确认是它导致的,仅此而已。问题 演变成一个通用技术问题,知道某页面中途发生跳转,如何快速定位跳转代码所在。 本文记录了提出问题、解决问题的学习过程。吾非WEB前端程序员,本文意义更多是 方法论层面的。 ☆ js跳转页面 向AI(选自己喜欢的吧)提问,AI回答如下 -------------------------------------------------------------------------- Q: js将当前页面跳转到「aboub:blank」,大概怎么写? A: window.location.href = "about:blank"; window.open( "about:blank", "_self" ); Q: _self怎么理解,可以是什么值? A: _blank 在新窗口或标签页中打开 _self 在当前窗口或标签页中打开 (默认行为) _parent 在当前窗口父级框架中打开,若页面没有父级,则表现为_self _top 在最顶层框架中打开,即使嵌套了多个iframe,也会在整个窗口中打开 window.open( "https://www.example.com/", "_blank" ); window.open( "https://www.example.com/", "_self" ); window.open( "https://www.example.com/", "_parent" ); window.open( "https://www.example.com/", "_top" ); -------------------------------------------------------------------------- AI回答,不背书,未查官方文档,可在F12 Console中实测。或有其他方案,但猜测 目标js大概率使用前述两种方案之一,更可能是href那种。 ☆ Object.defineProperty (失败) 先看一段油猴脚本 -------------------------------------------------------------------------- // ==UserScript== // @name Debug Helper // @namespace http://tampermonkey.net/ // @version 2024-03-28 // @description Some helper function // @author Me // @match https://*/* // @grant none // @run-at document-start // ==/UserScript== (function() { 'use strict'; function HookObjectProperty ( object, attr ) { let prop = object[attr]; let getter = function () { console.log( 'Hooking get', object, attr, prop ); debugger; return prop; }; let setter = function ( value ) { console.log( 'Hooking set', object, attr, value ); debugger; prop = value; }; Object.defineProperty( object, attr, { get: getter, set: setter, enumerable: true, configurable: true }); } HookObjectProperty( document, 'cookie' ); })(); -------------------------------------------------------------------------- 油猴脚本的好处就是尽早Hook,防止目标js先下手为强。关于先下手的事,参看: 《WEB前端逆向反反调试一例》 https://scz.617.cn/web/202406281614.txt 不用Tampermonkey,直接在F12 Console中输入HookObjectProperty()的代码也成, 只是对付不了先下手的情况。 在Console中测试 document.cookie = 'key:value'; 设置cookie时会断下来,可查看调用栈回溯。 在Console中测试 HookObjectProperty( window.document, 'location' ); HookObjectProperty( window, 'location' ); HookObjectProperty( window.location, 'href' ); 报错 Uncaught TypeError: Cannot redefine property: location Uncaught TypeError: Cannot redefine property: href AI忽悠,window.location是一个特殊对象,该对象的属性如href、protocol、host 等,由浏览器严格控制,许多属性被设计为只读或受限可写,以保证安全性。虽然可 通过直接赋值「window.location.href = ...」改变页面地址,但这实际上调用了浏 览器实现的底层逻辑,而不是直接对href属性赋值。 在浏览器中,window.location.href的descriptor(属性描述符)是不可配置的,这意 味着你无法使用Object.defineProperty来重新定义它。 在Console中执行 Object.getOwnPropertyDescriptor( window.document, 'location' ) Object.getOwnPropertyDescriptor( window, 'location' ) Object.getOwnPropertyDescriptor( window.location, 'href' ); 均返回 { configurable: false, enumerable: true, get: f, set: f } configurable为false,此时不能使用Object.defineProperty重新定义它。get和set 是由浏览器内置实现的,无法覆盖这些内置的getter和setter。 这都AI忽悠的,谨慎对之。 如下几条代码最终效果一样 window.document.location = "https://www.example.com/" window.location = "https://www.example.com/" window.location.href = "https://www.example.com/"; ☆ 渣浪求助 前一小节失败后,不想放狗,偷懒直接在渣浪求助。向人提问,向AI提问,本身就是 个技术活,我是这么问的: -------------------------------------------------------------------------- 请教个WEB前端调试的问题 想Hook如下操作 window.location.href = "something"; 已经尝试过Object.defineProperty,提示 Cannot redefine property: href 原始需求是,有js在设这个,但不知道在哪儿设的,想拦截这个操作,断下来,查看 调用栈回溯。 -------------------------------------------------------------------------- 目标js可能并非用此法,但这是个通用问题,即便解决不了目标js,有答案也是好的。 ☆ beforeunload事件 网友UID(3110320275)提供了两组解决方案,第二组是他参考另一网友解答后对第一组 的改进,从学习角度全部展示于此,因为我觉得过程与结果同样重要。需要特别感谢 的是,他提供了理论说明,同时提供了具有可操作性的测试步骤。与之对比,这么多 年在网上见识过太多「每个字都认识」系列,老虎吃天、无处下爪的那种。虽说谁都 没有义务掰碎了喂到谁嘴里,但能提供有效帮助时,我个人是不吝多写几句的。 1) 方案1 (较重) a. 写一个beforeunload事件监听方法,在Console中输入进去 b. 在F12 Sources面板启用「Event Listener Breakpoints->Load->beforeunload」 c. 触发页面跳转行为,查看调用栈回溯 步骤a代码如下 -------------------------------------------------------------------------- window.addEventListener( 'beforeunload', function ( event ) { /* * 这些console.log无所谓 */ console.log( window.location.href ); console.log( event ); /* * 若有这句,F8继续时Chrome弹框提示Leave或Cancel。若无这句,F8继续 * 时不弹框提示,直接跳转。若只为查看调用栈回溯,并不打算阻止跳转, * 不需要这句。 * * 上面是一般情况。云海那个目标URL似乎存在某种对抗措施,即使有这句, * Chrome也不弹框提示Leave或Cancel,而直接跳转,暂不清楚原因。 */ event.preventDefault(); } ); -------------------------------------------------------------------------- 步骤c可在Console中输入 window.location.href = "about:blank"; 步骤b所设断点命中,停在function(event)的入口代码处,上例就是第一条log()处。 此时调用栈的上一层就是「window.location.href = ...」。 离开function(event)后,Chrome弹框提示Leave或Cancel,前者完成跳转,后者放弃 跳转。若放弃跳转,可重复步骤c,再次测试整个流程。 2) 方案2 a. 写一个beforeunload事件监听方法,在Console中输入进去 b. 触发页面跳转行为,查看调用栈回溯 步骤a代码如下 -------------------------------------------------------------------------- window.addEventListener( 'beforeunload', function ( event ) { event.preventDefault(); debugger; } ); -------------------------------------------------------------------------- 步骤b可在Console中输入 window.location.href = "about:blank"; 相比方案1,略去原步骤b,用debugger语句断下来,操作更简洁。 ☆ navigate事件 (推荐) 网友UID(6161718960)在github提供基于navigate事件的解决方案,参看 https://github.com/LingYanSi/blog/issues/167 -------------------------------------------------------------------------- /* * 通过js触发的页面跳转 */ navigation.addEventListener( 'navigate', ( event ) => { console.log( event ); /* * 若有这句,F8继续时不跳转。若无这句,F8继续时发生跳转。若只为查 * 看调用栈回溯,并不打算阻止跳转,不需要这句。 * * 对href而言,navigate事件在beforeunload事件之前,若有这句,后续 * 触发beforeunload事件,若无这句,后续不会触发beforeunload事件。 */ event.preventDefault(); debugger; } ); -------------------------------------------------------------------------- /* * 通过a/form标签触发的页面跳转 */ window.addEventListener( 'click', ( event ) => { const findParent = ( d, check ) => { while ( d ) { if ( check( d ) ) { return d; } d = d.parentElement; } return null; } const dom = findParent( event.target, (d) => /^(a|form)$/i.test(d.tagName) ); dom && console.log( 'dom element', dom ); }, { capture: true } ) -------------------------------------------------------------------------- 在Console中测试 window.location.href = "about:blank"; 测了第一段代码,满足原始需求。第二段代码未碰上测试场景,备忘。 若不用debugger语句,可仿照beforeunload方案1 a. 写一个navigate事件监听方法,在Console中输入进去 b. 在F12 Sources面板启用「Event Listener Breakpoints->Load->navigate」 -------------------------------------------------------------------------- navigation.addEventListener( 'navigate', ( event ) => { console.log( event ); event.preventDefault(); } ); -------------------------------------------------------------------------- Event Listener Breakpoints Load navigate -------------------------------------------------------------------------- window.location.href = "about:blank"; -------------------------------------------------------------------------- 看明白了,这些「Event Listener Breakpoints」有相应事件监听方法时才会生效, 否则只在GUI中勾选这类断点,没用。 基于这种思路,对href额外测了hashchange、popstate事件,未断下来。 下面是navigate事件先于beforeunload事件的验证方案 -------------------------------------------------------------------------- navigation.addEventListener( 'navigate', ( event ) => { debugger; } ); window.addEventListener( 'beforeunload', function ( event ) { event.preventDefault(); debugger; } ); window.location.href = "about:blank"; -------------------------------------------------------------------------- ☆ AI回答 在渣浪求助前,我问AI,在F12调试中想拦截这种跳转操作,但不知具体位置,如何 做?它建议用Object.defineProperty。我说这扯呢,它转而建议用ES6 JS Proxy, 同样给了理论上就不可行的代码。 渣浪求助后,我新开会话问AI,用了Markdown的三个反引号 ``` Event Listener Breakpoints Load beforeunload ``` 这个断点什么时候命中? 这次GPT-4o的回答明显靠谱些 -------------------------------------------------------------------------- a. 什么是beforeunload事件 beforeunload是一个浏览器事件,在即将离开当前页面(例如刷新/关闭标签页、导航 到另一个页面)时触发。其主要用途是允许开发者在用户离开页面之前执行某些操作, 或者提示用户确认是否真地要离开页面。 -------------------------------------------------------------------------- b. 什么时候会触发beforeunload断点 当页面中的代码中监听了beforeunload事件时,浏览器会在以下情况下命中断点 b.0 用户操作触发页面卸载 刷新页面 关闭当前标签页或浏览器窗口 通过链接或输入新URL导航到另一个页面 b.1 代码主动触发页面卸载 在js中修改window.location.href或调用window.location.replace() 调用window.history.back()或window.history.forward() 使用window.close(),在某些浏览器中可能受限制 -------------------------------------------------------------------------- c. 如何设置beforeunload事件监听 window.addEventListener( 'beforeunload', function ( event ) { /* * 阻止页面卸载 */ event.preventDefault(); }); 或 window.onbeforeunload = function ( event ) { event.preventDefault(); /* * 显示提示,某些浏览器可能不支持,Chrome就不支持 */ return 'Are you sure you want to leave'; }; -------------------------------------------------------------------------- ☆ 其他「js跳转页面」方案 1) window.open F12 Console测试 -------------------------------------------------------------------------- function HookObjectMethod ( object, attr ) { let func = object[attr]; object[attr] = function () { console.log( 'Hooking', object, attr ); debugger; let ret = func.apply( object, arguments ); console.log( object, attr, ret ); return ret; } } HookObjectMethod( window, 'open' ); -------------------------------------------------------------------------- window.open( "about:blank", "_self" ); -------------------------------------------------------------------------- window.open比href简单,可Hook函数拦截,也可用beforeunload、navigate事件拦 截,事件拦截更通用。 2) window.location.assign/replace window.location.assign( "about:blank" ) window.location.replace( "about:blank" ) assign时,原页面进history,GUI中back跳回原页面。replace时,原页面被替换, GUI中back无法跳回原页面。 尝试Hook函数拦截,未报错,亦未生效: HookObjectMethod( window.location, 'assign' ); HookObjectMethod( window.location, 'replace' ); 但上述跳转方案均可用beforeunload、navigate事件拦截。 3) window.history.pushState/replaceState/go -------------------------------------------------------------------------- /* * 假设当前URL是"https://scz.617.cn/" * * 会修改当前地址栏中的URL,但不发生实际跳转 * * 受同源策略限制 * * pushState向history压栈,window.history.length递增 */ window.history.pushState( null, "", "https://scz.617.cn/web" ); /* * 发生实际跳转 * * 无参数时相当于go(0),等价于window.location.reload(),即刷新当前页面; * go(-1)相当于back(),go(1)相当于forward()。无论什么参数,均不影响history * 栈,window.history.length不变。 */ window.history.go() /* * 跳回原页面 */ window.history.back() window.history.forward() -------------------------------------------------------------------------- /* * 会修改当前地址栏中的URL,但不发生实际跳转 * * 受同源策略限制 * * replaceState直接修改history栈顶,window.history.length不变 */ window.history.replaceState( null, "", "https://scz.617.cn/web" ); window.history.go() /* * 无法跳回原页面 */ window.history.back() window.history.forward() -------------------------------------------------------------------------- /* * 同时监听beforeunload、navigate事件,不要preventDefault() */ window.addEventListener( 'beforeunload', function ( event ) { /* * 只有go会命中此处 */ debugger; } ); navigation.addEventListener( 'navigate', ( event ) => { /* * pushState、replaceState、go均会命中此处 * * pushState、replaceState命中此处时,后续不会触发beforeunload事件 */ debugger; } ); -------------------------------------------------------------------------- 目前测试下来,推荐navigate事件拦截,最广谱。 3.1) history栈 window.history[]相当于栈数据结构,pushState会压栈,replaceState直接修改栈 顶数据,window.history.length对应history[]的元素个数。出于安全和隐私限制, js无法访问history[]的元素。但GUI中右键点击back、forward按钮,可查看当前标 签页对应的history[],js看不到,GUI看得到。若js自己维护PrivateHistory[],与 内置window.history[]无关。 history.length为9时,有[0]至[8]共9个元素。history[]内部有绝对下标i、相对偏 移j,它们互相配合,用于索引数组元素。绝对下标i从0计,history[i]即当前元素。 j是相对于i的偏移,故j=0始终对应当前元素,向左对应j=-1,向右对应j=1,依次类 推。go()的参数是相对偏移j,go(j)实际跳至history[i+j]。go()不带参数时相当于 go(0),刷新当前页面。go(-1)等价于back(),go(1)等价于forward()。go、back、 forward会移动绝对下标i,但不增、删、改history[]元素。js看不到绝对下标i,这 是内部实现。 假设history.length为9,有[0]至[8]共9个元素,绝对下标i=8。go(-7)之后i=1,页 面跳至[1]。再在GUI地址栏输入新URL并回车,之后history[]只剩3个元素,原来的 [0]、[1]以及位于[2]的新URL,history.length变成3,绝对下标i=2,原来的[2]至 [8]被清空。这可能是对history[]弹栈清空的唯一法子,没有想像中的popState函数。 做实验: -------------------------------------------------------------------------- /* * 新建标签页,再地址栏访问"https://scz.617.cn/",F12,依次执行下列代码 */ window.history.pushState( null, "", "https://scz.617.cn/body" ); window.history.pushState( null, "", "https://scz.617.cn/misc" ); window.history.pushState( null, "", "https://scz.617.cn/unix" ); window.history.pushState( null, "", "https://scz.617.cn/windows" ); window.history.pushState( null, "", "https://scz.617.cn/network" ); window.history.pushState( null, "", "https://scz.617.cn/python" ); window.history.pushState( null, "", "https://scz.617.cn/web" ); /* * 应该为9 */ console.log( window.history.length ) /* * 跳至"https://scz.617.cn/web" */ window.history.go(7) /* * 跳至"https://scz.617.cn/" */ window.history.go(-7) /* * 地址栏访问"https://www.example.com/" * * 应该为3 */ console.log( window.history.length ) /* * 跳至"https://scz.617.cn/" */ window.history.back() /* * 跳至"https://www.example.com/" */ window.history.forward() -------------------------------------------------------------------------- ☆ 其他讨论 1) Tampermonkey要求Developer mode 参看 https://www.tampermonkey.net/faq.php#Q209 基于Chrome的浏览器,现在想用Tampermonkey,需要手动打开浏览器的「开发人员模 式」,过去没这要求。 chrome://extensions/ edge://extensions/ opera://extensions/ 2) debug(func) 网友UID(7383557079)提及debug(func),执行func()时会断下来。在Console中测试 debug( console.log ); console.log( 'scz is here' ); 无需其他动作,就会断在console.log行。这是个有用的调试手段,别处用得上。 若能取到href的setter,或可用此法拦载,但怎么取href的setter呢? ☆ 满足原始需求 云海那个目标URL,用这组测试方案 -------------------------------------------------------------------------- Event Listener Breakpoints Script Script First Statement -------------------------------------------------------------------------- navigation.addEventListener( 'navigate', ( event ) => { event.preventDefault(); debugger; } ); -------------------------------------------------------------------------- 确认在那个混淆js中执行 window.document.location = "about:blank"; 原始代码是是混淆过的,这是F12中查看变量的值再手工拼接得到的可读代码。由于 preventDefault(),不会跳转。后续会触发无限debugger,关掉F12之后,页面功能 符合预期。 本文原始需求的解决方案可能存在各种对抗措施,未碰上非解决不可的场景,打住。 ☆ 扯淡 无论在自己擅长还是不擅长的技术领域,不会就问。别给自己立个莫名其妙的人设, 觉得请教别人跌份啥的,尤其在这行干得久点的。我从不在乎自曝其短,人的精力有 限,计算机科学技术领域细分得厉害,我会的只是非常狭窄的一支,不会的是大多数 分支。某些不相识的网友不理解这么浅显直白的道理,碰上我问个啥时,说,哎哟, 你不会这个啊。心说,少见多怪,我特么还不会那个呢。 如何提问,属于老生常谈。提问前想一下,怎么不招人骂。就说这次的问题吧,好歹 说已经试过啥,不浪费双方感情。要是你的问题经常性不靠谱,浪费别人感情,次数 多了,没人搭理你。 从网上求助学来的知识,都会写清楚原作者,前文的UID是渣浪UID,可唯一定位,除 非注销。当你没有扮演全知全能的需求时,就会自然而然不贪天功为己有。 AI我用得很低级,只当成学习助手,高阶用法一概不会,这是衰老的表现。即便如此, 也受益颇多。从本例看,笼统提问时它的回答充满幻觉,越是具体的问题,回答越靠 谱。Poe的GPT-4o、4o-mini还可以,Claude、Kimi我也用,经常一问多提。但我总觉 得,这些AI靠谱与否拼运气,小心无大错。再就是,成瘾之后,美帝掐得狠点,咋办? 没有安全感。 多分享、多与人交流,至少有一个好处,遇上事儿了,会有一些愿意搭把手的。这次 提供帮助的网友,全部素不相识,再次谢过。把他们的知识整理后二次分享出来,算 是扩大助人圈,几个人知道没意思,更多人知道更有意思。对我来说,写文档本身也 是学习的一部分,写着写着,会发现需要补充、澄清若干细节,不就增长了一点么。 这些都是扯淡,当我没说。