Python3网络爬虫开发实战(11)JavaScript 逆向爬虫(上)

news2024/12/23 12:13:56

文章目录

  • 一、网站加密和混淆技术简介
    • 1. URL/API 参数加密
    • 2. JavaScript 压缩
    • 3. JavaScript 混淆
    • 4. WebAssembly
  • 二、浏览器调试常用技巧
    • 2.1 面板介绍
    • 2.2 节点事件
    • 2.3 断点调试
    • 2.4 观察调用栈
    • 2.5 Ajax 断点
    • 2.6 改写 JavaScript 文件
  • 三、JavaScript Hook 的使用
    • 3.1 Hook 操作
    • 3.2 实战分析
  • 四、无限 debugger 的原理和跳过
    • 4.1 实现原理
    • 4.2 禁用断点
  • 五、模拟执行 JavaScript
    • 5.1 使用 Python 模拟执行 JavaScript
    • 5.2 使用 Node.js 模拟执行 JavaScript
    • 5.3 浏览器环境下 JavaScript 的模拟执行

有些网站的数据接口是没有任何验证或加密参数的,我们可以轻松模拟并爬取其中的数据,但优点网站稍显复杂,网站的接口中增加了一些加密参数,同时对 JavaScript 代码采取了一些防护措施;对于这种方式,我们可以使用自动化工具去解决这一问题,这有一个缺点,那就是整页数据都需要被渲染,爬取效率很低;

我们可以通过逆向 JavaScript 代码的方式,大幅提高爬取效率;

一、网站加密和混淆技术简介

网站为了保护数据而采取的一些措施,基本可以分为两大类:

  1. URL/API 参数加密;
  2. JavaScript 压缩,混淆和加密;

1. URL/API 参数加密

网站或 APP 可以请求某个数据 API 获取到对应的数据,然后再把获取的数据展示出来。为了提升接口的安全性,客户端会和服务端约定一种接口的效验方式,一般来说会用到各种加密和编码算法,如 Base64,Hex 编码,MD5,AES,DES,RSA 等对称或非对称加密

例子:客户端和服务器双方约定一个 sign 用作接口的签名效验,其生成逻辑是客户端将 URL 路径进行 MD5 加密,然后拼接上 URL 的某个参数再进行 Base64 编码,最后得到一个字符串sign,这个 sign 会通过 Request URL 的某个参数或者 Request Headers 发送给服务器。服务器接收到请求后,对 URL 路径同样进行 MD5 加密,然后拼接上 URL 的某个参数,进行 Base64 编码,也会得到一个 sign,接着比对生成的 sign 和客户端发来的 sign 是否一致,如果一致就返回正确的结果,否者拒绝响应;

要实现接口参数加密,就需要用到一些加密算法,客户端和服务器肯定也都有对应的 SDK 实现这些加密算法,如 JavaScript 的 crypto-js,Python 的 hashlib,Crypto 等等;

2. JavaScript 压缩

JavaScript 压缩即去除 JavaScript 代码中的不必要的空格、换行等内容或者把一些可能公用的代码进行处理实现共享,最后输出的结果都压缩为几行内容,代码可读性变得很差,同时也能提高网站加载速度。

如果仅仅是去除空格换行这样的压缩方式,其实几乎是没有任何防护作用的,因为这种压缩方式仅仅是降低了代码的直接可读性。如果我们有一些格式化工具可以轻松将 JavaScript 代码变得易读,比如利用 IDE、在线工具或 Chrome 浏览器都能还原格式化的代码。

原来的 JavaScript 代码:

function echo(stringA, stringB){
	const name = "Germey";
	alert("hello " + name);
}

压缩后的 JavaScript 代码

function echo(d, c) {const e = "Germey"; alert("hello " + e);}

JavaScript 压缩技术只能在很小的程度上起到防护作用,要想真正提高防护效果还得依靠JavaScript 混淆和加密技术。

3. JavaScript 混淆

JavaScript 混淆是完全是在 JavaScript 上面进行的处理,它的目的就是使得 JavaScript 变得难以阅读和分析,大大降低代码可读性,是一种很实用的 JavaScript 保护方案。

JavaScript 混淆技术主要有以下几种:

  • 变量混淆:将带有含义的变量名、方法名、常量名随机变为无意义的类乱码字符串,降低代码可读性,如转成单个字符或十六进制字符串。
  • 字符串混淆:将字符串阵列化集中放置、并可进行 MD5 或 Base64 加密存储,使代码中不出现明文字符串,这样可以避免使用全局搜索字符串的方式定位到入口点。
  • 属性加密:针对 JavaScript 对象的属性进行加密转化,隐藏代码之间的调用关系。
  • 控制流平坦化:打乱函数原有代码执行流程及函数调用关系,使代码逻变得混乱无序。
  • 无用代码注入:随机在代码中插入不会被执行到的无用代码,进一步使代码看起来更加混乱。
  • 调试保护:基于调试器特性,对当前运行环境进行检验,加入一些强制调试 debugger 语句,使其在调试模式下难以顺利执行 JavaScript 代码。
  • 多态变异:使 JavaScript 代码每次被调用时,将代码自身即立刻自动发生变异,变化为与之前完全不同的代码,即功能完全不变,只是代码形式变异,以此杜绝代码被动态分析调试。
  • 锁定域名:使 JavaScript 代码只能在指定域名下执行。
  • 反格式化:如果对 JavaScript 代码进行格式化,则无法执行,导致浏览器假死。
  • 特殊编码:将 JavaScript 完全编码为人不可读的代码,如表情符号、特殊表示内容等等。

总之,以上方案都是 JavaScript 混淆的实现方式,可以在不同程度上保护 JavaScript 代码,在前端开发中,现在 JavaScript 混淆的主流实现是 javascript-obfuscator 和 terser 这两个库;

4. WebAssembly

WebAssembly 不同于 JavaScript 混淆技术, WebAssembly 其基本思路是将一些核心逻辑使用其他语言(如 C/C++ 语言)来编写,并编译成类似字节码的文件,并通过 JavaScript 调用执行,从而起到二进制级别的防护作用。

WebAssembly 是一种可以使用非 JavaScript 编程语言编写代码并且能在浏览器上运行的技术方案,比如借助于我们能将 C/C++ 利用 Emscripten 编译工具转成 wasm 格式的文件, JavaScript 可以直接调用该文件执行其中的方法。

WebAssembly 是经过编译器编译之后的字节码,可以从 C/C++ 编译而来,得到的字节码具有和 JavaScript 相同的功能,运行速度更快,体积更小,而且在语法上完全脱离 JavaScript,同时具有沙盒化的执行环境。

在这里利用 WebAssembly 定义 add 和 square 两个方法,代码如下:

WebAssembly.compile(
	new Uint8Array(
	` 00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01 7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61 64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02 08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c 0f 0b`
	.trim()
	.split(/[\s\r\n]+/g)
	.map((str) => parseInt(str, 16))
	))
	.then((module) => {
		const instance = new WebAssembly.Instance(module);
		const { add, square } = instance.exports;
		console.log("2 + 4 =", add(2, 4));
		console.log("3^2 =", square(3));
		console.log("(2 + 5)^2 =", square(add(2 + 5)));
});

输出结果如下

2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49

二、浏览器调试常用技巧

常用技巧即浏览器开发者模式的使用,在这里以示例网站 Scrape | Movie 演示;

2.1 面板介绍

在浏览器打开网站之后,按 F12 或者 UI 界面检查打开开发者工具,可以获得如下界面:

  • Elements:元素面板,用于查看或修改当前网页 HTML 节点的属性、CSS 属性、监听事件等等,HTML 和 CSS 都可以即时修改和即时显示。
  • Console:控制台面板,用于查看调试日志或异常信息。另外我们还可以在控制台输入 JavaScript 代码,方便调试。
  • Sources:源代码面板,用于查看页面的 HTML 文件源代码、JavaScript 源代码、CSS 源代码,还可以在此面板对 JavaScript 代码进行调试,比如添加和修改 JavaScript 断点,观察 JavaScript 变量变化等。
  • Network:网络面板,用于查看页面加载过程中的各个网络请求,包括请求、响应等各个详情。
  • Performance:性能面板,用于记录和分析页面在运行时的所有活动,比如 CPU 占用情况,呈现页面性能分析结果,
  • Memory:内存面板,用于记录和分析页面占用内存情况,如查看内存占用变化,查看 JavaScript 对象和 HTML 节点的内存分配。
  • Application:应用面板,用于记录网站加载的所有资源信息,如存储、缓存、字体、图片等,同时也可以对一些资源进行修改和删除。
  • Lighthouse:审核面板,用于分析网络应用和网页,收集现代性能指标并提供对开发人员最佳实践的意见。

2.2 节点事件

Elements 元素面板可以审查页面的节点信息,我们可以查看当前页面的 HTML 源代码及其在网页中对应的位置,这在 CSS 和 Xpath 中已经有所了解;

接下来切换到 Elements 元素面板 右侧的 Event Listeners 选项卡,这里可以显示各个节点当前已经绑定的事件,都是 JavaScript 原生支持的,下面简单列举几个事件。

  • change:HTML 元素改变时会触发的事件。
  • click:用户点击 HTML 元素时会触发的事件。
  • mouseover:用户在一个 HTML 元素上移动鼠标会触发的事件。
  • mouseout:用户从一个 HTML 元素上移开鼠标会触发的事件。
  • keydown:用户按下键盘按键会触发的事件。
  • load:浏览器完成页面加载时会触发的事件。

事件的处理逻辑一般是由 JavaScript 定义的,在我们点击页面的时候,对应的 JavaScript 代码就会执行,点击 Event Listeners 事件中对应的 javaScript 文件,就会自动跳转到 Sources 中对应的 JavaScript 代码,利用好 Event Listeners,我们可以轻松地找到各个节点绑定事件的处理方法所在的位置,帮我们在 JavaScript 逆向过程中找到一些突破口;

如果代码被压缩过,可读性很差,我们可以点击 Sources 面板中左下角的大括号,可以进行格式化;

2.3 断点调试

调试代码的时候,我们可以在需要的位置上打断点,当对应事件触发时,浏览器就会自动停在断点的位置等待调试,此时我们可以选择单步调试,在面板中观察调用栈、变量值,以更好地追踪对应位置的执行逻辑。

通过单击 Sources 中的代码行号就会出现了一个蓝色的箭头,这就证明断点已经添加好了,同时在右侧的 Breakpoints 选项卡下会出现我们添加的断点的列表。 在这里我们给对应的点击翻页事件添加断点,在翻页后,得到的结果如下:

可以发现浏览器执行到刚才设置到的断点为止出就不再继续执行了,回调参数 e 就是对应的点击事件 MouseEvent,在右侧的 Scope 面板处,可以观察到各个变量的值,比如在 Local 域下有当前方法的局部变量,我们可以在这里看到 MouseEvent 的各个属性;

切换到 Watch 面板,在这里可以自行添加想要查看的变量和方法,点击右上角的 + 按钮,我们可以任意添加想要监听的对象;

切换到 Console 面板,输入任意的 JavaScript 代码,此时便会执行输出对应的结果;

2.4 观察调用栈

通过观察右侧的调用堆栈 Call Stack 面板,我们可以得到全部的调用过程,第一行是当前调用方法,第二步是上一步方法,以此类推;

通过点击方法,可以回溯到对应逻辑执行的流程,从而快速找到突破口;

2.5 Ajax 断点

相较于在 Sources 面板中的代码行号前打断点,我们同样可以根据发送请求打断点,把之前的断点全部取消,切换到 Sources 面板下,然后展开 XHR/fetch Breakpoints,这里就可以设置 Ajax 断点;

要设置断点,首先需要观察 Ajax 请求,观察到目标 URL 中包含了 /api/movie 字段,我们可以使用该字段作为断点,得到结果如下;

格式化代码后,可以根据调用栈一层层需要构造 Ajax 的逻辑,最后找到一个 onFetchData 的方法,进入格式化后可以发现请求如下

使用了 axios 库发起了一个 Ajax 请求,还有 limit,offset 和 token 这 3 个参数;

2.6 改写 JavaScript 文件

网页里面的 JavaScript 是从对应服务器上下载下来并在浏览器执行的。有时候,我们可能想要在调试的过程中对 JavaScript 做一些更改,比如说有以下需求:

  • 发现 JavaScript 文件中包含很多阻挠调试的代码或者无效代码、干扰代码,想要将其删除。
  • 调试到某处,想要加一行 console.log 输出一些内容,以便观察某个变量或方法在页面加载过程中的调用情况。在某些情况下,这种方法比打断点调试更方便。
  • 调试过程遇到某个局部变量或方法,想要把它赋值给 window 对象以便全局可以访问或调用。
  • 在调试的时候,得到的某个变量中可能包含一些关键的结果,想要加一些逻辑将这些结果转发到对应的目标服务器。

这时候我们可以试着在 Sources 面板中对 JavaScript 进行更改,但这种更改并不能长久生效,一旦刷新页面,更改就全都没有了。比如我们在 JavaScript 文件中写入一行 JavaScript 代码,然后保存,会出现警告标志,提示更改是不会保存的;

要实现修改 JavaScript 文件,首先创建一个文件夹,然后在 Sources 面板的左上角选择 Overriders,然后选定文件夹;

选定文件夹后,进入寻找到需要修改的 js 代码,然后修改代码 ctrl + s 保存就好,下次进入依然可以保证

.then((function(a) {
	# 添加这一行代码
	console.log("response", a);
	var e = a.data
	  , s = e.results
	  , n = e.count;
	t.loading = !1,
	t.movies = s,
	t.total = n
}

刷新看效果,可以得到如下:

三、JavaScript Hook 的使用

在 JavaScript 逆向的时候,经常需要追中某些方法的堆栈调用情况,但是在很多情况下,一些 JavaScript 变量或者方法名经过混淆之后是非常难以捕捉的。如果知道事件运行的时候,会使用到其中的某个方法,我们可以使用 Hook 钩子技术来进行逆向;

Hook 是指在程序运行的过程中,对其中的某个方法进行重写,在原先的方法前后加入我们自定义的代码。相当于在系统没有调用该函数之前,钩子程序就先捕获该消息,得到控制权,这时钩子函数既可以加工处理改变该函数的执行行为,也可以强制结束消息的传递;

3.1 Hook 操作

要实现 JavaScript 代码的 Hook 操作,就需要额外在页面中执行一些有关 Hook 逻辑的自定义代码,这里可以使用一个插件名为 Tampermonkey,中文名叫 “油猴” 和 “篡改猴”,利用它几乎可以在网页中执行任何 JavaScript 代码;

其下载安装方式也很简单,直接在浏览器的管理拓展中搜索 Tampermonkey 就好,安装后在管理拓展中显示;

打开 Tampermonkey,创建一个新的脚本,如下:

其中 UserScript 部分意义如下:

  • @name:脚本的名称,就是在控制面板显示的脚本名称。
  • @namespace:脚本的命名空间。
  • @version:脚本的版本,主要是做版本更新时用。
  • @author:作者。
  • @description:脚本描述。
  • @homepage, @homepageURL, @website,@source:作者主页,用于在Tampermonkey选项页面上从脚本名称点击跳转。请注意,如果 @namespace 标记以 http://开头,此处也要一样。
  • @icon, @iconURL and @defaulticon:低分辨率图标。
  • @icon64 and @icon64URL:64x64 高分辨率图标。
  • @updateURL:检查更新的网址,需要定义 @version。
  • @downloadURL:更新下载脚本的网址,如果定义成 none 就不会检查更新。
  • @supportURL:报告问题的网址。
  • @include:生效页面,可以配置多个,但注意这里并不支持 URL Hash,配置多个时每一个都要换行以 // @include 开头;
  • @match:约等于 @include 标签,可以配置多个。
  • @exclude:不生效页面,可配置多个,优先级高于 @include 和 @match。
  • @require:附加脚本网址,相当于引入外部的脚本,这些脚本会在自定义脚本执行之前执行,比如引入一些必须的库,如 jQuery 等,这里可以支持配置多个 @require 参数。
  • @resource:预加载资源,可通过 GM_getResourceURL 和 GM_getResourceText 读取。
  • @connect:允许被 GM_xmlhttpRequest 访问的域名,每行一个。
  • @run-at:脚本注入的时刻,如页面刚加载时,某个事件发生后等等。例如:document-start:尽可能地早执行此脚本。document-body:DOM 的 body 出现时执行。document-end:DOMContentLoaded 事件发生时或发生后后执行。document-idle:DOMContentLoaded 事件发生后执行,即 DOM 加载完成之后执行,这是默认的选项。context-menu:如果在浏览器上下文菜单(仅限桌面 Chrome 浏览器)中点击该脚本,则会注入该脚本。注意:如果使用此值,则将忽略所有 @include 和 @exclude 语句。
  • @grant:用于添加 GM 函数到白名单,相当于授权某些 GM 函数的使用权限。如果没有定义过 @grant 选项,Tampermonkey 会猜测所需要的函数使用情况。
  • @noframes:此标记使脚本在主页面上运行,但不会在 iframe 上运行。
  • @nocompat:由于部分代码可能是专门为专门的浏览器所写,通过此标记,Tampermonkey 会知道脚本可以运行的浏览器。
  • GM_log:将日志输出到控制台。
  • GM_setValue:将参数内容保存到 Storage 中。
  • GM_addValueChangeListener:为某个变量添加监听,当这个变量的值改变时,就会触发回调。
  • GM_xmlhttpRequest:发起 Ajax 请求。
  • GM_download:下载某个文件到磁盘。
  • GM_setClipboard:将某个内容保存到粘贴板。

3.2 实战分析

目标网站:Scrape | Login

账号:admin
密码:admin

通过输入账号密码登录观察请求后,可以发现 post 请求中没有账号密码所包含的 admin,从这里我们可以看出来账号密码被加密了,加密变成了数据字典中的 token 字段;

通过观察 token 的内容,可以发现 token 非常像 Base64 编码,这就代表:网站很有可能将用户名和密码混为一个新的字符串,然后经过了一次 Base64 编码,最后将其赋值为 token 来提交了;

在这里,解决方法有两种,一种是 Ajax 断点,另一种就是 Hook;

首先分析一下 Ajax 断点的操作方式,由于登入的 Post 请求是一个 Ajax 请求,因此可以通过在 Sources 面板下的 XHR/fetch Breakpoints 添加 url 断点的方式截停;

或者更为简单的,由于其请求为 Ajax 请求,在 Network 面板下的 请求 的 Initiator 也可以调用 Call Stack 调用栈;

一步步找,最后可以发现入口在 onSubmit 方法那里,但实际上这里断点的栈顶是在 Promise.then 下的,因此不能直接调回去,只能取消掉 Ajax 断点,在onSubmit 那里添加断点才行;

这里通过 Json.stringify() 函数将字典转化为字符串,然后使用 encode 将字符串编码为 Base64格式,得到 token;

接下来我们分析 Hook 的操作方式,Hook 我们需要知道调用了什么函数事件,根据之前的分析,token 很明显就是使用 Base64 编码得到的,那么就很明显,只需要 Hook Base64 编码的位置就好了。JavaScript 中,使用 btoa 方法将字符串编码为 Base64 字符串,因此我们可以直接 Hook btoa 方法;

新建一个 Tampermonkey 脚本,其内容如下:

// ==UserScript==
// @name         HookBase64
// @namespace    https://login1.scrape.center/
// @version      2024-08-15
// @description  try to take over the world!
// @author       zhouyi
// @match        https://login1.scrape.center/
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Your code here...
    function hook(object, attr){
        var func = object[attr];
        object[attr] = function(){
            console.log('hooked', object, attr);
            var ret = func.apply(object, arguments);
            debugger;
            return ret;
        }
    }
    hook(window, 'btoa')
})();

首先,我们定义了一些 UserScript Header,包括 @name 和 @match 等,这里比较重要的就是 @name 表示脚本名称,另外一个就是 @match,其代表脚本生效的地址;

接着,我们定义了 hook 方法, 这里给其传入 object 和 attr 参数, 意思就是 Hook object 对象的 attr 参数。 例如, 如果我们想 hook alert 方法, 那就把 object 设置为 window , 把 attr 设置为字符串 alert。 这里我们要 Hook Base64 的编码方法, 而在 JS 中, Base64 编码用的是 btoa 方法实现的,所以这里我们只需要 Hook window 对象的 btoa 方法就好了

那么, Hook 怎么实现呢? 我们来看一下, var func = object[attr] , 相当于我们先把它赋值给一个变量, 即我们调用 func 方法就可以实现和原来相同的功能。 接着,我们直接改写这个方法的定义,将 object[attr] 改成一个新方法。 在新的方法中, 通过 func.apply 方法又重新调用了原来的方法。 这样我们就可以保证前后方法的执行效果不受影响, 之前那个方法该干啥还干啥,这里的 arguments 就是指传给 btoa 方法的参数,ret 就是 btoa 方法返回的结果;arguments 可以在 Sources 面板下的 Local 栏观察;

但是和之前不同的是, 现在我们自定义方法之后, 可以在 func 方法执行后加入自己的代码,如果通过 console.log 将信息输出到控制台, 通过 debugger 进入断点等。 在这个过程中,我们先零食保存下来 func 方法, 然后定义一个新方法, 接管程序的控制权,在其中定义我们想要的实现, 同时新方法里面重新调回 func 方法, 保证前后结果不受影响。 所以,我们达到了在不影响原方法效果的前提下, 实现方法前后自定义的功能, 这就是 Hook 的过程。

最后,我们调用 hook 方法, 传入 window 对象和 btoa 字符串, 保存

刷新页面,提交账号密码,成功进入断点模式并停下来了,代码就卡在自定义的 debugger 这行代码位置;

可以看到成功 Hook 住了,这说了 JavaScript 代码在执行的过程中调用到了 btoa 方法;验证到这里, 已经非常清晰了, 整体逻辑就是对登录表单的用户和密码进行 JSON 序列化, 然后调用 encode (也就是 btoa ) 方法, 并把 encode 方法的结果赋值为 token 发起登录的 Ajax 请求,逆向完成。

以后如果观察出一些门道,可以多使用这种方法来尝试,如 Hook encode 方法,decode 方法,stringify 方法,log 方法,alert 方法等,简单又高效;

四、无限 debugger 的原理和跳过

debugger 是 JavaScript 中定义的一个专门用于断点调试的关键字,只要遇到它,JavaScript 的执行便会在此处中断,进入调试模式;利用 debugger 这个关键字,可以非常方便地对 JavaScript 代码进行调试,比如使用 JavaScript Hook 时,我们可以加入 debugger 关键字,使其在关键的位置停下来,以便查找逆向突破口;

无限 debugger 的目标网站:Scrape | Movie

打开该网站后,一旦打开开发者工具,就会立即进入断点模式,如果尝试跳过这个断点继续执行,不管点击多少次按钮,仍然一次次地进入断点模式,无限循环下去,这种情况我们称之为 无限debugger;

4.1 实现原理

其实现原理很简单,这里使用的是利用 setInterval 循环,每一秒执行 1 次 debugger 语句;除了这样,还可以使用无限 for 循环,无限 while 循环,无限递归调用等等,原理大同小异;

4.2 禁用断点

禁用断点有如下几种方式:

全局禁用:位置处于最右边的圆圈,开启后不会有任何断点;

局部禁用:位置处于代码左侧右键,这里有添加断点,条件断点和永不断点,其中条件断点可以自己写条件,如果写 false,那么和永不断点性质一致;

添加局部禁用后,禁用的代码前面会有一个红色等于号;

替换文件:利用 ## 2.6 之前替换文件的方式,删除或注释掉 debugger 行再进行替换;

另外,我们不仅可以使用 Charles , Fiddler 等抓包工具进行替换, 也可以使用浏览器插件 ReRes 等, 还可以通过 PlayWright 等工具 使用 Request Interception 进行替换。这三种方式的原理都是将在线加载的 JavaScript 文件进行替换,最终消除无限 debugger;

五、模拟执行 JavaScript

一般来说,一些加密相关的方法通常会引用一些相关标准库,比如说 JavaScript 就有一个广泛使用的库,叫做 crypto-js,这个库实现了很多主流的加密算法,包括对称加密,非对称加密,字符编码等。比如对于 AES 加密,通常我们需要输入待加密文本和加密秘钥,实现如下:

const ciphertext = CryptoJS.AES.encrypt(messsage, key).toString();

5.1 使用 Python 模拟执行 JavaScript

对于这样的库,可以使用 Python 模拟,但是这是在对 Python 加密库非常熟悉的情况下才行;这里推荐一种其他的办法:既然 JavaScript 已经实现好了,我们可以使用 Python 直接模拟执行 JavaScript 获得结果;

目标网址:Scrape | NBA
目标字符串:如下图

分析网络链接,发现了 crypto-js.min.js 文件,则可以推断必定调用了 crypto-js,crypto-js 在调用时会用到 CryptoJS;因此我们可以在网络面板中搜索 CryptoJS 字符串,可以发现 main.js 中调用了这一字符串;

右键 main.js 请求面板,在 Sources 面板中打开;

得到加密部分的代码如下:

new Vue({
  el: '#app',
  data: function () {
    return {
      players,
      key: 'fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt'
    }
  },
  methods: {
    getToken(player) {
      let key = CryptoJS.enc.Utf8.parse(this.key)
      const {name, birthday, height, weight} = player
      let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name))
      let encrypted = CryptoJS.DES.encrypt(`${base64Name}${birthday}${height}${weight}`, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
      })
      return encrypted.toString()
    }
  }
})

getToken 方法的输入就是单个球员的信息, 就是上述列表的一个元素对象,然后 this.key 就是一个固定的字符串。整个加密逻辑就是提取球员的名字,生日,身高,体重,接着先进行 Base64 编码,然后进行 DES 加密,最后返回结果。

分析完毕后我们可以开始使用 Python 模拟执行 JavaScript 了;要使用 Python 模拟执行 JavaScript,我们需要用到一个库 pyexecjs;安装如下

pip install pyexecjs

pyexecjs 在执行 JavaScript 的时候依赖于 JavaScript 环境,因此在安装完毕这个库后,我们还需要安装 Node.js;Node.js — 在任何地方运行 JavaScript (nodejs.org)

安装完毕后测试下面这串代码

import execjs
print(execjs.get().name)

# 返回结果:Node.js (V8) 表示正常

我们需要模拟执行的内容就是以下两部分:

  1. 模拟运行 crypto-js.min.js 里面的 JavaScript,用于声明 CryptoJS 对象;
  2. 模拟运行 getToken 方法的定义,用于声明 getToken 方法;

由于浏览器环境和 Node.js 环境不一样,因此我们需要简要分析一下 crypto-js.min.js 的文件内容;

!function(t, e) {
    "object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e()
}(this, function(){/*crypto-js 函数内容*/});

首先是一个自执行方法,定义函数后紧接着调用执行:

!(function(t, e){/*函数内容*/})(this, function(){/*crypto-js 函数内容*/})

在 Node.js 上,我们有 exports ,module,module 中还有一个 exports;而没有 define;而在 浏览器环境中,exports 和 define 都没有;因此在这里,"object" == typeof exports 的结果为 true, "function" == typeof define 的结果为 false,所以就只执行了 module.exports = exports = e() 这段代码;

而在浏览器中,执行了 t.CryptoJS = e() 这行代码,其意义很明确,那就是将 function(){/*crypto-js 函数内容*/} 注册到当前环境的 CryptoJS 键中;

为了达到一样的效果,在这里我们可以直接定义一个 var 命名为 CryptoJS,然后在自执行函数中赋值就好,这样在当前环境中就会有 CryptoJS 方法;

var CryptoJS;
!function(t, e) {
    CryptoJS = e();
    "object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e()
}(this, function(){/*crypto-js 函数内容*/});

注册完毕后我们可以测试一下能否使用 getToken 方法:

var CryptoJS;
!function(t, e) {
    CryptoJS = e();
    "object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e()
}(this, function(){/*crypto-js 函数内容*/});

function getToken(player) {
      let key = CryptoJS.enc.Utf8.parse('fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt')
      const {name, birthday, height, weight} = player
      let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name))
      let encrypted = CryptoJS.DES.encrypt(`${base64Name}${birthday}${height}${weight}`, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
      })
      return encrypted.toString()
};

const player = {
    name: '凯文-杜兰特',
    image: 'durant.png',
    birthday: '1988-09-29',
    height: '208cm',
    weight: '108.9KG'
 };

console.log(getToken(player));
// 与结果一致
// DG1uMMq1M7OeHhds71HlSMHOoI2tFpWCB4ApP00cVFqptmlFKjFu9RluHo2w3mUw

说明是可以使用的,接下来我们需要在 Python 中获取这个结果:

import execjs
import json

player = {
    'name': '凯文-杜兰特',
    'image': 'durant.png',
    'birthday': '1988-09-29',
    'height': '208cm',
    'weight': '108.9KG'
}

# 利用 node 来编译 file 文件
file = 'test.js'
node = execjs.get()
ctx = node.compile(open(file, encoding='utf-8').read())

# 定义 js 语句,并使用 ctx 的 eval 函数回调 result
js = f"getToken({json.dumps(player, ensure_ascii=False)})"
result = ctx.eval(js)

print(result)
# 与结果一致
# DG1uMMq1M7OeHhds71HlSMHOoI2tFpWCB4ApP00cVFqptmlFKjFu9RluHo2w3mUw

如果不想改变原 js 文件中的代码,可以使用 require 函数接受导出的 eports 函数,具体操作如下节;

5.2 使用 Node.js 模拟执行 JavaScript

使用 Python 模拟执行中,得到了如下代码:

var CryptoJS;
!function(t, e) {
    CryptoJS = e();
    "object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e()
}(this, function(){/*crypto-js 函数内容*/});

function getToken(player) {
      let key = CryptoJS.enc.Utf8.parse('fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt')
      const {name, birthday, height, weight} = player
      let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name))
      let encrypted = CryptoJS.DES.encrypt(`${base64Name}${birthday}${height}${weight}`, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
      })
      return encrypted.toString()
};

const player = {
    name: '凯文-杜兰特',
    image: 'durant.png',
    birthday: '1988-09-29',
    height: '208cm',
    weight: '108.9KG'
 };

console.log(getToken(player));
// 与结果一致
// DG1uMMq1M7OeHhds71HlSMHOoI2tFpWCB4ApP00cVFqptmlFKjFu9RluHo2w3mUw

如果使用 Node.js 执行,仍然是需要 Node.js 环境的:Node.js — 在任何地方运行 JavaScript (nodejs.org);在这里我们使用另一种方式,不对 crypto-js.min.js 的代码有所修改,直接将文件下载下来丢到当前目录,然后重新定义一个 js 文件,代码如下:

var CryptoJS = require('./crypto-js.min.js')

function getToken(player) {
      let key = CryptoJS.enc.Utf8.parse("fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt")
      const {name, birthday, height, weight} = player
      let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name))
      let encrypted = CryptoJS.DES.encrypt(`${base64Name}${birthday}${height}${weight}`, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
      })
      return encrypted.toString()
};

const player = {
    name: '凯文-杜兰特',
    image: 'durant.png',
    birthday: '1988-09-29',
    height: '208cm',
    weight: '108.9KG'
 };

console.log(getToken(player));
// 与结果一致
// DG1uMMq1M7OeHhds71HlSMHOoI2tFpWCB4ApP00cVFqptmlFKjFu9RluHo2w3mUw

这样照样在 Python 模拟中,可以运行得到想要的结果;其中require执行的是一个导入的功能,接受的是 crypto-js.min.js 的 `module.exports = exports = e();

var CryptoJS = require('./crypto-js.min.js')

如果不想使用 pyexecjs 库来获取 node.js 中的结果,我们可以通过搭建服务的方式,通过 Python 请求来获取结果;

在这里我们可以使用 Node.js 中最流行的 HTTP 服务框架 express,首先安装 express,在 main.js 所在目录下运行如下命令:

npm i express

然后执行代码

var CryptoJS = require('./crypto-js.min.js');
const express = require("express");
const app = express();
const port = 3000
app.use(express.json())
function getToken(player) {
    let key = CryptoJS.enc.Utf8.parse("fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt");
    const {name, birthday, height, weight} = player;
    let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name));
    let encrypted = CryptoJS.DES.encrypt(
        `${base64Name}${birthday}${height}${weight}`,
        key,
        {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7,
        }
    );
    return encrypted.toString();
}

app.post("/", (req, res) => {
    const data = req.body
    res.send(getToken(data));
});

app.listen(port, () => {
    console.log(`Example app listening on port ${port}!`);
})

接着使用 node.js 执行

node test.js

编写网络请求如下:

import requests

player = {
    'name': '凯文-杜兰特',
    'image': 'durant.png',
    'birthday': '1988-09-29',
    'height': '208cm',
    'weight': '108.9KG'
}

url = "http://localhost:3000"
# 这里使用 json,不行就使用 data
resp = requests.post(url, json=player)

print(resp.text)
# DG1uMMq1M7OeHhds71HlSMHOoI2tFpWCB4ApP00cVFqptmlFKjFu9RluHo2w3mUw

5.3 浏览器环境下 JavaScript 的模拟执行

使用 pyexecjs 和 node.js 对 JavaScript 进行模拟执行的方法,在某些复杂情况下可能还是有一定的局限性;比如在浏览器中找到了一个类似的加密算法,其生成逻辑如下:

const token = encrypt(a, b)

最终目标是获取 token,token 是由加密方法 encryp 和两个参数 a,b 构成的,如果要使用 Python 或者 Node.js 来模拟整个调用过程,关键就两步:

  1. 把所有的依赖库都下载到本地;
  2. 使用 PyExecJS 或 Node.js 来加载依赖库并调用 encrypt 方法;

对于参数 a 和 b,可能比较容易找到它们是怎么生成的;但是存在一种问题就是 encrypt 方法非常复杂,其内部又关联了许多的变量和对象,甚至方法内部的逻辑也进行了混淆等操作,向内追踪非常困难;

首先便是环境差异:Node.js 中没有全局对象,取而代之的是 global 对象,如果 JavaScript 文件中有任何引用 window 对象的方法,就没法在 Node.js 环境中运行。我们需要做的就是把 window 对象改写成 global 对象,或者把一些浏览器中的对象用一其他方法代替;

其次是依赖库查找:encrypt 所依赖的全部逻辑和依赖库其实都已经加载到浏览器,如果我们要在其他环境中模拟执行,要从中完全剥离出 encrypt 所依赖的 JavaScript 库,还需要一定的功夫,一旦缺少了必要的依赖库, encrypt 方法就会无法成功运行;

在这里,我们可以尝试使用浏览器作为执行环境来辅助逆向;

首先,我们需要安装自动化测试工具,这里以 Playwright 为例子,其安装过程和使用在 Python3网络爬虫开发实战(7)JavaScript 动态渲染页面爬取_python爬取js动态网页-CSDN博客中有详细介绍;

这次我们的目标网站是 Scrape | Movie,简要分析该网站,每一次 ajax 请求都会带有一个新的 Token,首先我们可以通过 ajax 断点获取发送请求的位置;

通过在调用堆栈中查找,最终找到 onFetchData 方法中包含了请求构造;

在该方法中添加断点后,需要 Ajax 断点,然后重新运行网页;

通过逐步运行,可以发现 limit,offset 的值是非常好构建的,其中 limit 为 10, offset 表示页码;问题是 token 的值取决于方法 e = Object(i["a"])(this.$store.state.url.index, a);,这里的 this.$store.state.url 表示 /api/movie,这里的 a 和 offset 表示的东西一致;因此主要分析的部分在于 i["a"]

找到 i["a"] 的位置,得到函数如下:

从这里大致可以看到这里掺杂了时间,SHA1,Base64,列表等各种操作,要深入分析,分析得到以下参数;

  1. t 表示的是时间戳,如 1723804629,一共 10 位;
  2. r 复制了 arguments 中的内容,而 arguments 为 ["/api/movie", 0],经过 r.push(t) 传入时间戳变为 ["/api/movie", 0, "10位时间戳"]
  3. o 是 r 用 , 连接后的 SHA1 加密;
  4. c 是利用 [o, t] 进行逗号连接后的 Base64 加密;
  5. 返回的最后的值是 c,c 就是需要的 token

使用纯 Python 构建 token 如下:

import time
import hashlib
import requests
import base64


def get_token(offset):
    t = str(int(time.time()))
    r = ','.join(["/api/movie", str(offset), t])
    sha = hashlib.sha1(r.encode('utf-8'))
    o = sha.hexdigest()
    c = base64.b64encode(','.join([o, t]).encode('utf-8')).decode('utf-8')
    token = c
    return token


headers = {
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Pragma': 'no-cache',
    'Referer': 'https://spa2.scrape.center/',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-origin',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0',
    'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Microsoft Edge";v="126"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
}


params = {
    'limit': '10',
    'offset': '0',
    # 这里传入 offset
    'token': get_token('0'),
}

response = requests.get('https://spa2.scrape.center/api/movie', params=params, headers=headers)
print(response.text)

# {"count":103,"results":[{"id":1,"name":"霸王别姬",....

使用 JavaScript 似乎更加的简单,要注意在这里最好不使用补环境的做法,因为做了很多的混淆;

上述做法不是本节的重点,在这里的重点是使用浏览器环境来获取 token,从而绕过 JavaScript 的分析;在绕过逆向的时候,应该会产生这样的疑问;

  1. 怎么在不分析该方法逻辑的情况下直接拿到运行结果呢?该方法完全可以看成黑盒;
  2. 要直接拿到方法的运行结果,就需要模拟调用了,怎么模拟调用;
  3. 这个方法并不是全局方法,所以没法直接调用,该怎么办呢;

方法如下:

  1. 模拟调用是没有问题的,问题是在哪里模拟调用,根据上文的分析,既然浏览器中都已经把上下文环境和依赖库都加载成功了,可以直接使用浏览器;
  2. 如何模拟局部方法?只需要修改代码将局部方法挂载到全局 window 对象上就好;
  3. 如何修改代码,可以使用 playwright 的 request interception 机制将想要替换的任意文件进行替换就好;

在这里我们可以将 Object(i["a"]) 的全局挂载,只需要将其赋值给 window 对象的一个属性就好,修改代码保存到当前目录;

在这里我们在 e = Object(i["a"])(this.$store.state.url.index, a) 下面挂载 window.getToken

onFetchData: function() {
	var t = this;
	this.loading = !0;
	var a = (this.page - 1) * this.limit
	  , e = Object(i["a"])(this.$store.state.url.index, a);
	// 在这里添加 window.getToken 方法
	window.getToken = Object(i["a"]);
	this.$axios.get(this.$store.state.url.index, {
		params: {
			limit: this.limit,
			offset: a,
			token: e
		}
	}).then((function(a) {
		var e = a.data
		  , s = e.results
		  , n = e.count;
		t.loading = !1,
		t.movies = s,
		t.total = n
	}
	))
}

搞定后,将代码复制到一个命名为 chunk.js 的文件之中;接下来我们使用 playwright 启动一个浏览器,并使用 Request Interception 将 JavaScript 文件替换;

from playwright.sync_api import sync_playwright
import requests


playwright = sync_playwright().start()
browser = playwright.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
page.route(
	# 注意,这里需要绝对路径,即完整的URL地址
    "https://spa2.scrape.center/js/chunk-10192a00.243cb8b7.js",
    lambda route: route.fulfill(path="./chunk.js"),
)
page.goto("https://spa2.scrape.center/")


def get_token(offset):
    result = page.evaluate(
        """()=>{
        return window.encrypt("%s", "%s")    }"""        % ("/api/movie", offset)
    )
    return result


headers = {
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Pragma": "no-cache",
    "Referer": "https://spa2.scrape.center/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0",
    "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126", "Microsoft Edge";v="126"',
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": '"Windows"',
}

params = {
    "limit": "10",
    "offset": "0",
    # 这里填入 offset
    "token": get_token("0"),
}

response = requests.get(
    "https://spa2.scrape.center/api/movie", params=params, headers=headers
)
print(response.text)


context.close()
browser.close()

# {"count":103,"results":[{"id":1,"name":"霸王别姬",....

可以发现非常简单就完成了数据采取工作,这在一定程度上减轻了逆向的压力,不过这里由于需要打开浏览器渲染,速度可能会慢一些;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2047991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宝兰德持续赋能 助力金融数智化变革

金融机构作为助推数字经济发展的中坚力量,近些年在数字化转型叠加信创改造、AI大模型高速演进、监管环境变化等因素下,面临多重挑战,不得不重新审视传统IT架构,确保金融数据的安全性、可用性,从而激活自身动能&#xf…

计算机毕业设计 饮食营养管理信息系统 平衡膳食管理系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点…

【嵌入式开发之网络编程】网络分层、OSI七层模型、TCP/IP及五层体系结构

计算机网络体系的形成 两台计算机要互相传送文件需解决很多问题,比如: 必须有一条传送数据的通路。发起方必须激活通路。要告诉网络如何识别接收方。发起方要清楚对方是否已开机,且与网络连接正常。发起方要清楚对方是否准备好接收和存储文…

JS UI库DHTMLX Suite v8.4全新发布——图表、网格组件等API全面升级

DHTMLX UI 组件库允许您更快地构建跨平台、跨浏览器 Web 和移动应用程序。它包括一组丰富的即用式 HTML5 组件,这些组件可以轻松组合到单个应用程序界面中。DHTMLX JS UI 组件可用于任何服务器端技术:PHP、Java、ASP.NET、Ruby、Grails、ColdFusion、Pyt…

糟糕界面集锦-控件篇10

想要让自己的程序别具一格,正是出于这种被误导的动机。IBM 的Aptiva Communitations Center 开发者决定不使用Windows 自己的控件,用自行开发的控件取而代之。他们非常成功地做到了这一点:该程序看上去与其他Windows 环境下运行的程序完全不同…

C语言-在主函数中输入10个等长的字符串。用另一函数对他们进行排序,然后再主函数输出这10个排好序的数列(分别用①数组法和②指针法实现)

在主函数中输入10个等长的字符串。用另一函数对他们进行排序&#xff0c;然后再主函数输出这10个排好序的数列&#xff08;分别用数组法和指针法实现&#xff09; 一、数组法实现 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> void str_sort(char str[][32], …

【C语言】时间函数详解

目录 C语言时间函数详解表格汇总1. time2. localtime3. gmtime4. strftime5. clock6. difftime函数详解示例解释 7. mktime8. asctime9. ctime10. clock_gettime 和 clock_settime总结9. 结束语相关文章&#xff1a; C语言时间函数详解 在C语言中&#xff0c;时间处理功能由标…

【国奖学姐力荐】matlab智能算法的案例分析和源代码

现在各类数模比赛特别是国赛优化问题越来越多&#xff0c;而求解这些优化问题往往要用到智能启发式算法&#xff0c;今天带大家看一下matlab智能算法的案例分析和源代码&#xff0c;有两本书推荐给大家。 有需要这两本书籍电子版和单独案例的家人可以公屏留言&#xff0c;我会依…

【C# WPF WeChat UI 简单布局】

创建WPF项目 VS创建一个C#的WPF应用程序: 创建完成后项目目录下会有一个MainWindow.xaml文件以及MainWindow.cs文件,此处将MainWindow.xaml文件作为主页面的布局文件,也即为页面的主题布局都在该文件进行。 布局和数据 主体布局 Wechat的布局可暂时分为三列, 第一列为菜…

【Spring Boot】拦截器的使用

目录 前言 拦截器的使用 1.创建一个拦截器 2.注册拦截器 3.配置拦截器的匹配规则 拦截器的实际使用场景 拦截器 vs 过滤器 vs AOP 前言 在Spring Boot中&#xff0c;拦截器&#xff08;interceptor&#xff09;是一种用于拦截和处理请求的机制。通过拦截器&#xff0c;可…

聊天机器人正在膨胀技术

API 在软件中发挥的作用比任何其他东西都要大 当团队与外部 API&#xff08;包括第三方 AI&#xff09;集成时&#xff0c;他们可以将预制的外部功能引入产品中。我使用 API 让用户根据matchboxxr上的提示生成 3D 模型。 但是&#xff0c;尽管越来越多的初创公司只关注人工智能…

Java开发工具IDEA入门指南——如何从VS Code迁移到IDEA?(一)

IntelliJ IDEA是java编程语言开发的集成环境。IntelliJ在业界被公认为最好的Java开发工具&#xff0c;尤其在智能代码助手、代码自动提示、重构、JavaEE支持、各类版本工具(git、svn等)、JUnit、CVS整合、代码分析、 创新的GUI设计等方面的功能是非常强大的。 在本文中&#x…

【java基础】IDEA 的断点调试(Debug)

目录 1.为什么需要 Debug 2.Debug的步骤 2.1添加断点 2.2单步调试工具介绍 2.2.1 Step Over 2.2.2 Step Into 2.2.3 Force Step Into 2.2.4 Step Out 2.2.5 Run To Cursor 2.2.6 Show Execution Poiint 2.2.7 Resume Program 3.多种 Debug 情况介绍 3.1行断点 3.2方…

XSS GAME

源网站&#xff1a;XSS 游戏 - 学习 XSS 变得简单&#xff01; |创建者 PwnFunction 以下为解码工具&#xff1a; 在线 JSFuck 加密 - 百川在线工具箱 (chaitin.cn) CyberChef 1、Ma Spaghet! 条件 Difficulty is Easy.Pop an alert(1337) on sandbox.pwnfunction.com.No…

分析FP -Growth代码运行内存太大而无法运行的原因

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…

随记 - 2024 年 4 月 12 日

写在前面 444 字 | 生活 | 经历 | 感触 正文 或许因为压力大&#xff0c;亦或者简单的糖分不足&#xff0c;今晚好想吃面包和蛋糕。 蛋糕吃不完也买不起&#xff0c;面包还是可以。 实在饿&#xff0c;出门了。 导航两家西点店&#xff0c;关门。怏怏地找另一家。 在十点前&a…

效果炫酷的3D翻转书特效WordPress主题模板MagicBook主题v1.19

正文&#xff1a; MagicBook是一款支持3D翻书特效的书籍WordPress主题。支持可视化页面搭建&#xff0c;3D菜单&#xff0c;完全自适应设计,WPML多语言支持。 这款主题一定会让你爱不释手。虽然他是英文的&#xff0c;但不可不承认的是&#xff0c;它优雅的设计会让你愿意花时…

[Linux]将一个文件复制到多个文件夹下

一、简介 本文介绍了在linux下如何使用cp命令将一个文件复制到多个文件夹、多个文件复制到一个文件夹和多个文件复制到多个文件夹下。 二、代码 假设初始时test/文件夹的结构如下&#xff1a; 1. 将一个文件复制到多个文件夹 a.命令示例 将file1复制到目录des_dir1/&#…

【PGCCC】pg_bestmatch.rs:使用 BM25 提升您的 PostgreSQL 文本查询#PCA

这是一个 PostgreSQL 扩展&#xff0c;它将最佳匹配 25 分数 (BM25) 文本查询的强大功能引入您的数据库&#xff0c;从而增强您执行高效和准确的文本检索的能力。此扩展允许用户从文本生成 BM25 统计稀疏向量&#xff0c;利用 BM25 在各种基准测试任务中经过验证的性能。 为什…

8.16 QT

1.思维导图 2 将day1做的登录界面升级优化【资源文件的添加】 2> 在登录界面的登录取消按钮进行一下设置&#xff1a; 使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5…