Skip to content

JavaScript拦截器

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

拦截器简介

JavaScript 拦截器是在 request 请求、response 响应或异步操作中插入自定义逻辑的一种模式。

定位流程

有的反爬网站在请求的时候,会返回加密的数据:

image-20231224135610965

再看 Initiator 启动器,会发现是异步的加载,那么数据解密操作就很有可能是在拦截器里面进行的:

image-20231224140059485

这里我们就可以全局搜索一下 interceptors 拦截器关键字,选择第一处进入,可以看到 a.interceptors.request.usea.interceptors.response.use 就是请求和响应拦截器,它不会直接拦截异步加载的过程,但可以在发送请求之前或在接收到响应之后执行自定义逻辑。

image-20231224140652391

网站实践

湘易办

难度:简单

地址:https://mobile.zwfw.hunan.gov.cn:8088/h5webs/#/tgm?cityCode=431322000000

“湘易办”是湖南全力打造的全省统一的企业、群众、公务人员“掌上办事”的总入口、优化营商环境的总平台、建设数字政府的总引擎,由中国建设银行、中国电子集团共同建设。首先我们打开开发者工具,点击左上角手机按钮,让网页以移动端的形式展示,然后乱填一些信息,登录进行抓包:

image-20240130232700763

请求头中没有加密字段,查看表单是一串密文,查看相应内容发现也是一串密文:

image-20240130232934842

面对这种情况,我们就可以全局搜索一下拦截器 interceptor 关键词,总共有两处文件中存在该关键词,我们点击第一个文件,里面搜索 interceptor 关键词,可以看到 u.interceptors.request.useu.interceptors.response.use 关于请求和响应拦截器,我们在响应处打上断点:

image-20240130233735583

我们再次点击登录,断点断在第 1209 行,打印 e 变量,里面就是响应的密文,再打印一下 s["a"](e) 输出的就是明文,这就说明解密的逻辑一定是在 s["a"]() 函数里面:

image-20240130234213513

我们进入 s["a"]() 函数里面一步步运行调试,发现解密的部分是在第 1512 至 1515 行,最后输出了 a 赋值给了 e.data 属性,我们输出一下 a 确认就是我们要的明文信息:

image-20240130235228691

现在我们来解读这几行代码,具体分析如下:

  • 第一行 window.atob(e.data) 获取 e.data 属性,即响应的密文进行 Base64 解码;
  • 第二行、第三行都使用到了 window 中的一个 DCSAPPClientAPI 属性中的一些方法对变量值进行了一些处理,最后返回了变量 a
  • 第四行使用 JSON.parse(a) 对变量a进行JSON序列化;

产业政策平台

和这个站有点像 http://49.77.204.6:17001/Website/#/compQuery/enterInfoQuery

难度:中等

地址:http://www.spolicy.com/

产业政策平台是一个提供各行各业公共产业政策的大数据平台。首先要注意一点,不能先启动开发者工具再进入该网站,必须是先进入该网站再启动开发者工具,然后发现该网站禁用了按 F12 启动Chrome开发者工具,那我们就通过Chrome菜单中的选项来启动开发者:

20230724173810

开发者工具一启动就进到了 VM 虚拟环境的“无限debugger”当中,我们就用前面学习的无限 debugger 绕过方式来绕过,右键点 debugger 所在行的左侧,选择 Never pause here 从不在此停留选项:

20230719173350

断点打上以后,我们点击按钮继续运行:

20230719173718

本以为能顺利的过掉无限debugger,结果页面直接卡死,重试几次都是这样:原因就在于,前面的这种绕过方式只针对小部分网站,而绝大部分网站在 JS 代码中存留了一个写入的功能,它会有一个无限循环的变量来写入到我们的内存当中,从而导致整个页面处于一个卡死状态,如果运行时间过长,甚至会导致我们的电脑处于一个卡死状态,这就是”内存爆破“。

20230719174320

其实绕过的方式也非常简单,通过注入Hook代码,修改debugger条件,就能绕过去了。我们在 Sources 选项卡中选择 Snippet 片段,选择 New Snippet 新建片段,命名为 hook_debugger,写入下面Hook代码,写入后可以看到左侧的文件名中带有 * 号,这是没有保存修改的状态,我们 Ctrl+S 保存后,点击下面的运行来注入Hook代码:

javascript
AAA = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
    if(a === 'debugger') {
        return function () {};
    }
    return AAA(a);
};

20230724171658

注入成功后,在控制台会有输出,然后我们点击按钮继续运行,浏览器就不会卡死了:

20230724172651

在网站的”政策类型“中选择”政策文件“一项,在右侧就出现了数据请求,里面的响应正是页面加载的数据:

20230724174152

观察请求的数据接口和请求头,都没有特殊的加密字段,然而POST发送的数据是一串加密数据:

20230724174652

现在我们拷贝请求的路径,打上 XHR 断点,再次点击网站中的”政策文件“选项,断点卡住:

20230802163145

接着我们点击按钮向上执行,会来到下图这里,可以看到一个完整的请求对象:

20230802163652

继续点击按钮向上执行,会来到下图这里,可以看到上面执行一个函数,我们将其中的关键字进行输出,会发现其中一个关键词 interceptors 就是拦截器的意思:

axiosInstance['interceptors']['response']['use']

20230802164504

现在我们只在控制台输出 axiosInstance['interceptors'] 拦截器的这一部分,可以看到只有“request请求”和“response响应”两部分,展开“request请求”部分,点击进入函数地址:

20230802170359

跳转之后,我们来到了如下图这里,可以看到这也是一个函数:

20230802170653

来到函数末尾处打上断点,再次点击网站中的”政策文件“选项,函数末尾断点卡住,我们在右侧边栏观察断点处 o 变量中的 data 属性,展开后是一个数组,点击右侧的小方块,下方就出现了我们前面请求的密文参数,可以看到这种形式其实就是Octet-stream进制流,它是一种通用的二进制数据表示方法,可以表示任何类型的数据,包括图像、声音、视频、文本等等:

20230802172610

那接着我们就要去逆向 o['data'] 中的函数逻辑,刚好在函数中就有其生成逻辑,我们在此处打上断点,再次点击网站中的”政策文件“选项,断点卡住,可以看到,在执行逻辑之前 o['data'] 是明文的请求参数,在执行逻辑之后 o['data'] 就变为了密文的请求参数::

20230802174119

20230802174500

我们将 o['data'] 中的关键参数进行还原,整体如下:

javascript
data = {
    "policyType": "4",
    "province": "",
    "city": "",
    "downtown": "",
    "garden": "",
    "centralId": "",
    "sort": 0,
    "homePageFlag": 1,
    "pageNum": 1,
    "pageSize": 7
}
// o['data']变量
o_data = f['encode'](data)['finish']()['slice']()

20230802175653

我们选中 f['encode'] 方法,进入到该函数的地址中,发现整个函数只有29行,我们将其中的函数拷贝到js文件当中去,注意将函数名中的 $ 替换为 _ 以避免歧义产生:

20230802180107

20230802180352

当我们运行代码后,会提示 Writer is not defined 未定义:

20230804111718

如果我们去浏览器中定位这行代码函数位置,就会发现跳转到了另一个虚拟机文件中,而且定位不了具体的函数位置,跟不了栈,因为这个是框架代码:

20230804112209

20230804112324

现在有一点可以确定的是,Writer 一定是在 VM5 这个文件当中的,我们可以在文件中搜索一下 Writer.create() 发现有一处,可以看到这段字符串其实就是上面的那段代码,观察函数还发现前面有一个 13 的编号,这个就有点像Webpack打包的形式:

20230804113141

我们沿着函数编号往上寻找,发现这就是一个将内部函数模块化的一个大函数,我们将其全部扣下来,拷贝到我们的文件当中,进行格式化,发现就是一个Webpack打包的形式:

20230804113812

20230804114738

由于该函数是一个自执行函数,运行一下,发现 commonjsGlobal 未定义,我们去到浏览器中进行断点,发现断不住,就直接在 o['data'] 断点处进行控制台输出,发现是一个 Window 对象,由于平常我们补环境是 Window = global; 写法,那我们现在就可以直接在上面添加一个 commonjsGlobal = global; 即可:

20230804155740

20230804161005

现在我们还无法直接调用这个函数,因为这个函数是一个整体,在外部是是调用不了内部的模块的,参考Webpack的处理方式,在外部定义一个 cz 全局变量,来接收函数内部的加载器 i 对象,再通过 cz 加载器来调用 Writer.create() 方法,再一运行我们想要的二进制结果就出来了:

20230804162238

接下来,我们就定义一个函数将 o['data'] 的字符形式设为的返回值,在Python进行调用:

20230804164407

20230804164503

最后,我们将其添加到爬虫当中测试,效果如下:

20230804164654