js逆向-webpack

前言

钟表可以回到起点,但已不是昨天

⚠️声明:本文所涉及的爬虫技术及代码仅用于学习、交流与技术研究目的,禁止用于任何商业用途或违反相关法律法规的行为。若因不当使用造成法律责任,概与作者无关。请尊重目标网站的robots.txt协议及相关服务条款,共同维护良好的网络环境。

1.webpack打包

1.1概念

Webpack 是一个现代 JavaScript 应用的打包工具,它可以将模块化的代码转换成浏览器可执行的 JavaScript 文件。Webpack 主要提供以下功能:

  • 模块打包:支持 ES Modules、CommonJS、AMD 等模块格式。
  • 代码拆分(Code Splitting):按需加载代码,减少首屏加载时间。
  • 加载资源(Loaders):处理 CSS、图片、字体、TypeScript 等文件。
  • 插件扩展(Plugins):优化打包过程,如压缩代码、提取 CSS、生成 HTML 文件等。
  • 开发服务器(Webpack DevServer):提供热更新(HMR),加快开发效率。

地址:http://cls.cn/telegraph

如果一个页面大部分是script标签构成,80%以上是 webpack 打包

image-20250304212155994

1.2webpack

e其实是一个数组,包含了所有模块的定义

加载器 (n(t)):

  • n(2) 被调用,意味着执行模块 2
  • n(2) 检查 c[2] 是否已加载(最开始是 undefined)。加载直接返回return c[2].exports;
  • c[2] 还未加载,因此创建 c[2],然后执行e[2].call(a.exports, a, a.exports, n)
  • a.l = !0 表示该模块加载完成。
  • 返回 a.exports(但这里没有 exports,所以 undefined)。

总结:

  • 这个代码实现了一个简化版的 Webpack 模块加载器。
  • 其中 n(t) 负责按需加载模块,并用 c 缓存已加载模块。
  • 入口 n(2) 直接执行了 console.log('模块3')
!function (e) {
    var c = {} // 用于存储已加载的模块

    function n(t) {
        if (c[t]) // 如果模块已加载,则直接返回
            return c[t].exports;

        var a = c[t] = { // 创建新模块对象
            i: t,  // 模块 ID
            l: !1, // 是否加载完成
            exports: {} // 模块导出对象
        };

        e[t].call(a.exports, a, a.exports, n), // 执行模块函数
        a.l = !0; // 标记模块已加载
        return a.exports;
    }

    n.m = e; // 存储所有模块

    // 入口执行模块 2
    n(2);
}
([
    function () {
        console.log('模块1')
    },
    function () {
        console.log('模块2')
    },
    function () {
        console.log('模块3')
    }
]);

1.3模块加载器

Webpack 的 模块加载器 是 Webpack 运行时(Runtime)的核心部分,它的主要功能是 按需加载模块,并确保模块之间的依赖关系得到正确解析和执行

Webpack 在打包代码时,会将模块转换为 对象或数组的形式,并将其存储在 Webpack 运行时所管理的模块列表中。在运行时,模块加载器 负责:

  1. 管理已加载的模块,防止重复执行。
  2. 按需加载模块,提高执行效率。
  3. 解析依赖,确保模块之间的引用正确执行。
  4. 提供 require / import 机制,让模块可以相互调用。

总结

  • Webpack 的模块加载器 负责动态加载模块,管理缓存,并解析依赖。
  • 执行入口模块( n(2);) 是核心,它检查缓存、执行模块,并返回 exports
  • 优化点:
    • 代码分割(Code Splitting) 提高加载效率
    • 按需加载(Dynamic Import) 只加载必要模块
    • 热更新(HMR) 替换模块而不刷新页面

(function (e) { ... })([ ... ]) 存储所有模块:

  • e 是一个数组,每个元素都是一个模块函数
  • 这些模块函数是 Webpack 打包后的代码

var c = {}:模块缓存。这个对象用来存储已经加载的模块,防止重复加载。

function n(t) {}:这是 Webpack 动态加载模块的关键函数,它根据 t 来加载相应的模块。

if (c[t]) return c[t].exports:检查模块是否已经加载。如果模块已经加载,直接返回 exports,避免重复执行。

var a = c[t] = { i: t, l: !1, exports: {} };

  • 创建一个新的模块对象,并存储到 c里。
  • exports 用于存储模块的导出内容(类似于 module.exports)。

e[t].call(a.exports, a, a.exports, n)执行模块函数,传入:

  • a:当前模块对象
  • a.exports:导出对象
  • n:模块加载函数

a.l = !0;:标记模块已加载。执行完模块代码后,将 l 设置为 true,避免重复加载。

return a.exports;:返回模块的 exports,供其他模块使用。

n.m = e;:存储所有模块

n(2);:执行入口模块。这里调用 n(2),即 加载并执行 ID 为 2 的模块

!function (e) {
    var c = {} // 用于存储已加载的模块

    function n(t) {
        if (c[t]) // 如果模块已加载,则直接返回
            return c[t].exports;

        var a = c[t] = { // 创建新模块对象
            i: t,  // 模块 ID
            l: !1, // 是否加载完成
            exports: {} // 模块导出对象
        };

        e[t].call(a.exports, a, a.exports, n), // 执行模块函数
        a.l = !0; // 标记模块已加载
        return a.exports;
    }

    n.m = e; // 存储所有模块

    // 入口执行模块 2
    n(2);
}
([
    function () {
        console.log('模块1')
    },
    function () {
        console.log('模块2')
    },
    function () {
        console.log('模块3')
    }
]);

1.4webpack数组形式

在 Webpack 打包后,所有的模块会被存储在一个数组或对象中,数组索引(或对象的 key)就是模块的 ID,对应的值是模块的代码(通常是一个函数)。这种形式有助于 Webpack 高效管理模块的加载

Webpack 采用数组存储模块的原因:

  1. 索引访问更快
    • 直接通过 n(id) 即可访问 e[id],效率高。
  2. 代码结构紧凑
    • 使用数组存储函数,能减少代码体积。
  3. 避免命名冲突
    • 通过索引管理模块,避免不同模块使用相同的变量名。

模块数组 e,传入 e 是一个数组,其中:

  • e[0]function () { console.log('模块1') }
  • e[1]function () { console.log('模块2') }
  • e[2]function () { console.log('模块3') }

入口模块执行n(2);

  • n(2) 表示执行 e[2](即 console.log('模块3'),索引从0开始的)。
!function (e) {
    var c = {} // 用于存储已加载的模块

    function n(t) {
        if (c[t]) // 如果模块已加载,则直接返回
            return c[t].exports;

        var a = c[t] = { // 创建新模块对象
            i: t,  // 模块 ID
            l: !1, // 是否加载完成
            exports: {} // 模块导出对象
        };

        e[t].call(a.exports, a, a.exports, n), // 执行模块函数
        a.l = !0; // 标记模块已加载
        return a.exports;
    }

    n.m = e; // 存储所有模块

    // 入口执行模块 2
    n(2);
}
([
    function () {
        console.log('模块1')
    },
    function () {
        console.log('模块2')
    },
    function () {
        console.log('模块3')
    }
]);

image-20250304220208820

1.5webpack对象形式

Webpack 采用 键值对 存储模块,每个模块的 key 是模块 ID,value 是模块的代码(通常是一个函数)。

Webpack 将所有模块封装在一个对象 e 中:

  • 对象的 key(即 0, 1, 2)是模块 ID。
  • 对象的 value 是模块对应的函数,模块代码在其中执行。

这种方式允许 Webpack 按需加载特定模块,而不像数组形式那样需要按索引顺序组织。

!function (e) {
    var c = {} // 用于存储已加载的模块

    function n(t) {
        if (c[t]) // 如果模块已加载,则直接返回
            return c[t].exports;

        var a = c[t] = { // 创建新模块对象
            i: t,  // 模块 ID
            l: !1, // 是否加载完成
            exports: {} // 模块导出对象
        };

        e[t].call(a.exports, a, a.exports, n), // 执行模块函数
            a.l = !0; // 标记模块已加载
        return a.exports;
    }

    n.m = e; // 存储所有模块

    // 入口执行模块 2
    n(2);
}
({
    0: function () {
        console.log('模块1')
    },
    1: function () {
        console.log('模块2')
    },
    2: function () {
        console.log('模块3')
    }
});

image-20250304220238081

1.6多js文件打包

Webpack 处理多个 js 文件时,它会将模块拆分动态加载,使用全局变量 window["webpackJsonp"] 来存储和管理动态导入的模块

当 Webpack 进行代码拆分(Code Splitting),或者使用 import() 进行按需加载(Lazy Loading)时,它会:

  1. 定义全局变量 window["webpackJsonp"] = [](存储动态加载的模块)
  2. 重写 window["webpackJsonp"].push() 方法,替换为 webpackJsonpCallback() 来处理新加载的模块
  3. 动态加载的 js 文件 会调用 window["webpackJsonp"].push(),注册新的模块

Webpack 动态加载模块 时,会调用 window["webpackJsonp"].push(),通常接受 3 个参数:

  • 参数 1:模块 ID(chunkId
  • 参数 2:模块定义(modules 对象)
  • 参数 3:运行时的 webpackJsonpCallback()(可选)
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
    [1],   // 模块 ID(chunkId)
    {
        "./src/moduleA.js": function(module, exports) {
            console.log("模块 A 加载成功");
        },
        "./src/moduleB.js": function(module, exports) {
            console.log("模块 B 加载成功");
        }
    },
    [[1]]  // 可选的执行入口(runtime callback)
]);

Webpack 在 runtime 代码中会重写 window["webpackJsonp"].push(),执行 webpackJsonpCallback()

function webpackJsonpCallback(data) {
    var chunkId = data[0];    // 模块 ID
    var modules = data[1];    // 模块代码
    var runtime = data[2];    // 运行时回调(可选)

    // 将新加载的模块加入 Webpack 内部模块管理
    for (var moduleId in modules) {
        if (Object.prototype.hasOwnProperty.call(modules, moduleId)) {
            __webpack_modules__[moduleId] = modules[moduleId];
        }
    }

    // 执行回调(如果存在)
    if (runtime) runtime();
}

// 重写 `window["webpackJsonp"].push()` 为 `webpackJsonpCallback`
window["webpackJsonp"].push = webpackJsonpCallback;

运行时流程

  • 页面初始化时

    • window["webpackJsonp"] = []

    • window["webpackJsonp"].push = webpackJsonpCallback

  • 按需加载模块

    • Webpack 生成 chunk.js

    • chunk.js 执行 window["webpackJsonp"].push([...])

    • 触发 webpackJsonpCallback()

  • webpackJsonpCallback() 解析 chunk.js 并执行代码

    • 注册模块 到 Webpack 模块缓存

    • 调用回调,执行动态加载模块

Webpack 代码拆分的核心机制

  • 存储动态模块window["webpackJsonp"] = []
  • 拦截 push():Webpack 通过 webpackJsonpCallback() 处理新加载模块
  • 按需加载:动态 import() 触发 Webpack 生成 chunk.js,并执行 window["webpackJsonp"].push()
  • 模块管理:Webpack 将模块存入 __webpack_modules__ 并执行回调

2.36kr逆向

地址:https://36kr.com/

接口地址:https://gateway.36kr.com/api/mus/login/byMobilePassword

image-20250304224632824

2.1关键字搜索

关键字推断:

  • password加密,就搜索password
  • 也可以搜索encrypt

这里搜索到了很多,这个是在发送/login/byMobilePassword请求时处理了password,感觉很像就打个断点调试一下

image-20250304225429499

可以看到这里Object(i.b)就是加密的逻辑

m("/login/byMobilePassword", {
                            countryCode: o.a.get(t, "phoneItc.code"),
                            mobileNo: Object(i.b)(o.a.get(t, "mobileNo")),
                            password: Object(i.b)(o.a.get(t, "password"))
                        });

image-20250304225243897

2.2xhr断点

添加/api/mus/login/byMobilePasswordxhr断点

/api/mus/login/byMobilePassword

image-20250304225921307

在调用堆栈的异步方法之前都没有找到加密位置,所以只能在后面异步方法上打断点调试

image-20250304230201613

异步方法可能被调用多次,看参数和自己想找的不相关的话就跳过

image-20250304230548095

2.3hook拦截

因为加密肯定是在发送请求前,所以使用hook拦截了xhr

注意检查自己的地址/api/mus/login/byMobilePassword,然后ctrl + enter运行脚本

(function () {
    var open = window.XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function (method, url, async) {
        if (url.indexOf("/api/mus/login/byMobilePassword") != -1) {
            debugger;
        }
        return open.apply(this, arguments);
    };
})();

image-20250304230949350

然后就一步步调试,依然可以找到加密的地方

image-20250304231154380

2.4逆向-加载全部模块

找到加密的位置后就可以跳转到加密的实现进行具体的逆向

m("/login/byMobilePassword", {
                            countryCode: o.a.get(t, "phoneItc.code"),
                            mobileNo: Object(i.b)(o.a.get(t, "mobileNo")),
                            password: Object(i.b)(o.a.get(t, "password"))
                        });

image-20250304235236548

进入 Object(i.b)后,发现使用setPublicKey设置了一个密钥,所以这应该是非对称加密(rsa)加密,具体的实现还是r.encrypt(e),这里可以不着急跳转到r.encrypt(e),可以先查看r的实现r = n(16),所以我们跳到n的实现

var r = n(16)
      , o = n(4)
      , i = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeiLxP4ZavN8qhI+x+whAiFpGWpY9y1AHSQC86qEMBVnmqC8vdZAfxxuQWeQaeMWG07lXhXegTjZ5wn9pHnjg15wbjRGSTfwuZxSFW6sS3GYlrg40ckqAagzIjkE+5OLPsdjVYQyhLfKxj/79oOfjl/lV3rQnk/SSczHW0PEyUbQIDAQAB"
      , a = function() {
        var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "";
        if (!Object(o.isNodeEnv)()) {
            var t = n(774)
              , r = new t.JSEncrypt;
            r.setPublicKey(i);
            var a = r.encrypt(e);
            return a
        }
    }

image-20250304235459110

跳转到n的实现发现n是一个模块加载器,所以这是一个webpack打包的脚本

  • n(t):根据 t 来加载相应的模块

  • n.m = e;:存储所有模块

    function n(t) {
        if (c[t])
            return c[t].exports;
        var a = c[t] = {
            i: t,
            l: !1,
            exports: {}
        };
        return e[t].call(a.exports, a, a.exports, n),
        a.l = !0,
        a.exports
    }
    n.e = function(e) {}
    ,
    n.m = e,

image-20250305000526781

接下来我们复制整个脚本到编辑器

image-20250305000318689

声明变量moudle,在webpack内部获取到n,外部传入下标5执行一下

报错:window is not defined

var module;
!function(e) {
    ...
    module = n
}([]);
console.log(module(5))

image-20250305005508458

之前我们遇到过,window 是浏览器对象,pycharm执行没有浏览器环境,给个默认值即可

window = global;

但是报错 Cannot read properties of undefined (reading 'call'),其实就是没有找到执行的模块

image-20250305005642032

回到浏览器,使用控制台输出n(16),找到n(16)的调用

image-20250305010053521

查看n(16)的方法

image-20250305010209434

在查看整个加载代码,因为浏览器折叠不方便,我们全部复制到pycharm编辑器上

image-20250305010320140

我们将代码全部折叠,发现有985个模块,复制这个数组

image-20250305010451599

我们将985个模块全部复制到webpack中自执行的方法参数中

image-20250305010755089

声明一个数组存储执行过模块的索引,再次执行

var mouduleArr = [];


 mouduleArr.push(t)

image-20250305011448815

成功执行,解密代码去浏览器复制,我这里只写重要的代码

代码

window = global;
var module;
var moduleArr = [];
!function (e) {
       ......
       function n(t) {
        if (c[t])
            return c[t].exports;
        var a = c[t] = {
            i: t,
            l: !1,
            exports: {}
        };
        // 输出索引 方便定位缺失的模块
        // console.log(t)
        moduleArr.push(t)
        return e[t].call(a.exports, a, a.exports, n),
            a.l = !0,
            a.exports
    }
}([...放入所有模块,985个元素...]);

console.log(module(5))
console.log('已执行模块:' + moduleArr.join(','))

image-20250305011728642

2.5逆向-按需加载模块

我们把webpack所有需要的模块参数删除,然后在上面输出添加代码console.log(t)打印出执行模块的下标

image-20250305013634665

在浏览器中看到var r = n(16),说明从下标为16的模块开始执行的,所以我们测试也从console.log(module(16))开始

var r = n(16)
      , o = n(4)
      , i = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeiLxP4ZavN8qhI+x+whAiFpGWpY9y1AHSQC86qEMBVnmqC8vdZAfxxuQWeQaeMWG07lXhXegTjZ5wn9pHnjg15wbjRGSTfwuZxSFW6sS3GYlrg40ckqAagzIjkE+5OLPsdjVYQyhLfKxj/79oOfjl/lV3rQnk/SSczHW0PEyUbQIDAQAB"
      , a = function() {
        var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "";
        if (!Object(o.isNodeEnv)()) {
            var t = n(774)
              , r = new t.JSEncrypt;
            r.setPublicKey(i);
            var a = r.encrypt(e);
            return a
        }
    }

image-20250305013221539

在浏览器的控制台输出n.m[16],找到n.m[16]模块,然后复制,粘贴到webpack的参数中

n.m[16]

image-20250305013759414

浏览器是根据模块的下标的,n.m[16]代表第17个模块,但是粘贴到逆向代码中就变成0下标的模块,所以我们可以使用webpack对象形式,把这个模块设置键为16,这样就好不影响调用了

image-20250305014021658

再次执行,缺少第42个模块

image-20250305014348079

再次在浏览器的控制台输出n.m[42],找到n.m[42]模块,然后复制,粘贴到webpack的参数中

n.m[42]

image-20250305014506331

再次执行,缺少第57个模块

image-20250305014632360

再次在浏览器的控制台输出n.m[57],找到n.m[57]模块,然后复制,粘贴到webpack的参数中

n.m[57]

image-20250305014959957

再次执行总算成功了

image-20250305023010706

接下来需要重写加密方法

// 原代码
var r = n(16)
      , o = n(4)
      , i = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeiLxP4ZavN8qhI+x+whAiFpGWpY9y1AHSQC86qEMBVnmqC8vdZAfxxuQWeQaeMWG07lXhXegTjZ5wn9pHnjg15wbjRGSTfwuZxSFW6sS3GYlrg40ckqAagzIjkE+5OLPsdjVYQyhLfKxj/79oOfjl/lV3rQnk/SSczHW0PEyUbQIDAQAB"
      , a = function() {
        var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "";
        if (!Object(o.isNodeEnv)()) {
            var t = n(774)
              , r = new t.JSEncrypt;
            r.setPublicKey(i);
            var a = r.encrypt(e);
            return a
        }
    }
    
// 修改后
function get_encrypt_data(userinfo) {
    var r = module(16),
        o = module(4),
        i = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeiLxP4ZavN8qhI+x+whAiFpGWpY9y1AHSQC86qEMBVnmqC8vdZAfxxuQWeQaeMWG07lXhXegTjZ5wn9pHnjg15wbjRGSTfwuZxSFW6sS3GYlrg40ckqAagzIjkE+5OLPsdjVYQyhLfKxj/79oOfjl/lV3rQnk/SSczHW0PEyUbQIDAQAB";
    var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "";
    if (!Object(o.isNodeEnv)()) {
        var t = module(774)
            , r = new t.JSEncrypt;
        r.setPublicKey(i);
        var a = r.encrypt(e);
        return a
    }
}

image-20250305023104569

这里需要注意o = module(4),刚才我们找到了r = module(16)的所有模块,所以这里还需要找到o = module(4)需要的模块,具体使用浏览器的调试的流程我这里就不写了,跟上面一样。具体需要以下模块,找到后复制到webpack参数下面即可:

image-20250305023707847

2.6js逆向代码

测试完成后,需要修改js逆向的代码给python调用

我就写个具体修改的地方,算做个总结

window = global;
navigator = {
    appName: 'Netscape'
}
var module;
var moduleArr = [];

!function (e) {
    // 这里有代码...
    function n(t) {
        if (c[t])
            return c[t].exports;
        var a = c[t] = {
            i: t,
            l: !1,
            exports: {}
        };
        // 输出索引 方便定位缺失的模块
        console.log(t)
        moduleArr.push(t)
        return e[t].call(a.exports, a, a.exports, n),
            a.l = !0,
            a.exports
    }
    // 这里有代码...
    	// a()
    module = n
}({
    1: function (e, t, n) {},
    4: function (e, t, n) {},
    16: function (e, t, n) {},
    // 其他模块代码
    ...
})

// 测试代码
// console.log(module(16))
// console.log(module(4))
// console.log(moduleArr.join(','))
// console.log('已执行模块:' + moduleArr.join(','))
// console.log(get_encrypt_data('15566668888'))
// python调用代码
function get_encrypt_data(userinfo) {
    var r = module(16),
        o = module(4),
        i = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeiLxP4ZavN8qhI+x+whAiFpGWpY9y1AHSQC86qEMBVnmqC8vdZAfxxuQWeQaeMWG07lXhXegTjZ5wn9pHnjg15wbjRGSTfwuZxSFW6sS3GYlrg40ckqAagzIjkE+5OLPsdjVYQyhLfKxj/79oOfjl/lV3rQnk/SSczHW0PEyUbQIDAQAB";
    var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "";
    if (!Object(o.isNodeEnv)()) {
        var t = module(774)
            , r = new t.JSEncrypt;
        r.setPublicKey(i);
        var a = r.encrypt(e);
        return a
    }
}

2.7python调用

代码

大部分问题之前有遇到过,主要是确认header没问题,post请求发送的json数据

# 第一种
import subprocess
import time
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")

import execjs
import requests


headers = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Type": "application/json",
    "Origin": "https://36kr.com",
    "Pragma": "no-cache",
    "Referer": "https://36kr.com/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-site",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
    "sec-ch-ua": "Not A(Brand;v=8, Chromium;v=132, Google Chrome;v=132",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "Windows"
}
cookies = {
    "userId": "",
    "tfstk": "gNLZ_1VR8V3a7mYZn6b4Yj3Yrk_OlND5_E6fiIAc1OXGIsDVm9JACG_MuHU2wILs6ZGOWCQAoYMSPKR96ZIxXLF67J4haBf0IrmC-6iiwsDSP4OgLB0uDYT6J93NCsbcsiXGxX5PNRbcnrcFKs58mRvcoXlFi_PGssfm-WfV_ZXDnZcet9CqZWWcFy5PjjS68CXQcvIOECXUozPRGGmDtoa8yefNbEAGL11681SNEMe_RvYMdIY6WMn3YZdB4K-DEvZ1I35yLgtZUPJyBsAVidu8a9YwgeS9A7ayTiWwxFjUirABttvW0dkaXOx6LgChxSuROL6H6FxEMJAMFO7GtMgoaBjDAFIJ5v4Nr3d15nvodl6M4sjyn-C3Gr8vb-qVj6CFFXlezaGzfPk-jkrYDGKdTTGt6oEAj6CFFXlUDoIOy6WS6fC.."
}
url = "https://gateway.36kr.com/api/mus/login/byMobilePassword"
data = {
    "krtoken": "",
    "partner_id": "web",
    "timestamp": int(round(time.time() * 1000)),
    "param": {
        "countryCode": "86",
        "mobileNo": "",
        "password": ""
    }
}

js = execjs.compile(open('5.6kr按需加载模块.js',encoding='utf-8').read())
encrypt_mobileNo = js.call("get_encrypt_data",'15566669999')
encrypt_password = js.call("get_encrypt_data",'123qweasd')
# data = json.dumps(data, separators=(',', ':'))
data['param']['mobileNo']= encrypt_mobileNo
data['param']['password']= encrypt_mobileNo
print(data)
response = requests.post(url, headers=headers, cookies=cookies, json=data)

print(response.text)

image-20250305024635986

3.拼多多跨境卖家中心登录逆向

地址:https://seller.kuajingmaihuo.com/login/

接口:https://seller.kuajingmaihuo.com/bg/quiet/api/mms/login

image-20250305195516074

2.1关键字搜索定位加密

请求参数encryptPassword进行了加密,我们直接搜索

image-20250305195620958

然后打断点确认,M.password是密码,N.current是公钥,Object(P.a)是加密方法

var i = {
                            loginName: Object(W.a)({
                                phoneNo: M.mobile,
                                areaCode: M.areaCode
                            }),
                            encryptPassword: Object(P.a)(M.password, N.current),
                            keyVersion: N.current.version,
                            verifyCode: p ? M.verifyCode : void 0
                        };

image-20250305195718904

2.2逆向

跳转到Object(P.a)的实现,发现这里就是加密的实现

var g = o(1503)可以大致确定为webpack打包

var g = o(1503),
B = function(A, e) {
            try {
                var o = new g.a;
                return o.setPublicKey(e.publicKey),
                o.encrypt(A)
            } catch (t) {}

image-20250305200053846

var g = o(1503)打上断点,刷新页面,然后就可以跳转到o的实现了

image-20250305200223072

发现 c(t)是一个模块加载器,说明这是webpack打包的js脚本

function c(t) {
        if (n[t])
            return n[t].exports;
        var r = n[t] = {
            i: t,
            l: !1,
            exports: {}
        };
        return e[t].call(r.exports, r, r.exports, c),
        r.l = !0,
        r.exports
    }

image-20250305200324010

我们复制整个webpack脚本到编辑器,然后就要开始定位需要执行的模块了

image-20250305200651877

声明一个window变量用于模拟浏览器环境

声明外部变量module,在webpack内部的最后进行赋值,赋值给模块加载器

因为我们外部代码需要测试调用

image-20250305201040524

我们还需要输出模块加载器的下标或者键,方便确认代码的调用流程

image-20250305201426961

然后在浏览器调试获取公钥e.publicKey

image-20250305201237148

最后重写加密函数,执行代码,根据错误输出我们需要找到键为1503或者下标为1503的模块

console.log(get_encrypt_data('123qweasd'))
function get_encrypt_data(text) {
    var g = module(1503)
    var e = {
        publicKey: 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJpaLPONg3qWfi2kdwauqOT3L7AEK6lrMZqKYZv0ikWPZtzTqnVLMeolsE7x3Sc4UxS6ews+WS0ijwIEU8GAbhr2aJh0F+o6bpYQgWosHBts4lMW/IdtKg45DSGgBWg8gWDTK7K+T+zWfTjEQGwEgNHsZ2NbQBtTEZ35CDWUP7iQIDAQAB',
        version: '1'
    }
    var o = new g.a;
    return o.setPublicKey(e.publicKey),
        o.encrypt(text)
}

image-20250305201627349

刷新页面,再次调试定位到var g = o(1503),我们在浏览器控制台输出o.m[1503]

image-20250305201933089

跳转到o.m[1503]实现,发现这是一个对象形式的模块脚本

这里浏览器不方便折叠代码,我们把模块所有代码复制到编辑器

image-20250305202044470

发现只有3个模块,模块不多,其实可以全部复制到webpack的参数里

这里为了学习还是一个一个模块的调试,我么先复制1503模块

image-20250305202229419

以对象的形式复制到webpack参数里,然后再次执行代码,发现缺少1732

image-20250305202442706

在模块代码里找到1732模块,再次复制

image-20250305202704539

再次执行代码,发现密钥成功生成

这次模块不多,复制全部模块和复制执行的模块没多少差距,就不多做赘述了

image-20250305202802245

2.3js逆向代码

测试完成后,需要修改js逆向的代码给python调用

我就写个具体修改的地方,算做个总结

window = global
var module;
!function (e) { 
     // ...其它代码
     function c(t) {
        if (n[t])
            return n[t].exports;
        var r = n[t] = {
            i: t,
            l: !1,
            exports: {}
        };
        // 输出执行模块的下标或者键
        console.log(t)
        return e[t].call(r.exports, r, r.exports, c),
            r.l = !0,
            r.exports
    }
     ,
        // 存储了所有模块
        c.m = e,
    // ...其它代码
    // r()
    module = c
}({
   // 所有模块的代码 模块的具体实现
   1503: function (t, e, r){},
   1732: function (t, e, r){}
   // 其他模块代码
   ...
})

// // 测试
// console.log(get_encrypt_data('123qweasd'))

function get_encrypt_data(text) {
    var g = module(1503)
    var e = {
        publicKey: 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJpaLPONg3qWfi2kdwauqOT3L7AEK6lrMZqKYZv0ikWPZtzTqnVLMeolsE7x3Sc4UxS6ews+WS0ijwIEU8GAbhr2aJh0F+o6bpYQgWosHBts4lMW/IdtKg45DSGgBWg8gWDTK7K+T+zWfTjEQGwEgNHsZ2NbQBtTEZ35CDWUP7iQIDAQAB',
        version: '1'
    }
    var o = new g.a;
    return o.setPublicKey(e.publicKey),
        o.encrypt(text)
}

image-20250305203036168

2.4python调用

代码

如果调不通,排除js问题后,也记得检查自己的headerscookies、参数

# 第一种
import subprocess
import time
from functools import partial

subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")

import execjs
import requests

headers = {
    "accept": "*/*",
    "accept-language": "zh-CN,zh;q=0.9",
    "anti-content": "0aqWtqlYNcHgjgv2R4kIJVEhS6S_HM4e4KLUNxFkMP1l2eyQGez6vxyPil8n1SMYOnNDl1z29vEGKVB6xUryG42yhGHhO-BPFmpVgVE2uJQXSEEZp0IBC9TFXTb0KAYbTq8Owq0YhYhbxCKkKi-QocLR2mIcz-M0AaghfV8RVj8N2jybYJXc6gljug7oKh7azzN0rE9sEaCiu9M89umb9fanmUjV7v7aWOA--F9LR9u89PCVkf2QoVAC2UWCaxiVBHAus360E9WYLsEb2QN5lHVgMLVAC9lCKK8aEWb5mDLbAiXLQKDqLYUkzw0lf9GDK2CN6TyBoaGpCcVMTVEh0iO7Z4iNoXGnRjVH-wG5FA3oMZ_DZ7bClMuuUYmlkju8WuMCtWSohjbT6TKaXWAaizAmJuFiykszkX5OZezbIK55FAgz_ksz_nzdeoBd-W6n3ASLeP3bNxue5cRq1ijG0E3B4WwplHxtoJ1PZFj54Zaa542VpPTFKK5032ggs9HmeJHdKMTppKSGpp5tbJvIuQgQSMXSX9MNob9BuFzClC5_pSG47B_O8m7X0UfdII_dQ4WYap8Lw-hZwIBrxFEX8dOmgPoqykamcqxApyh-ybDjf3ruJrT5BcQ7R-OyZUzxbbSUIEBvV7DOPWBMccE8zK8iWCStReo4zTXStGv_6vf",
    "cache-control": "no-cache",
    "content-type": "application/json",
    "origin": "https://seller.kuajingmaihuo.com",
    "pragma": "no-cache",
    "priority": "u=1, i",
    "referer": "https://seller.kuajingmaihuo.com/login/",
    "sec-ch-ua": "\"Not A(Brand\";v=\"8\", \"Chromium\";v=\"132\", \"Google Chrome\";v=\"132\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "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/132.0.0.0 Safari/537.36"
}
cookies = {
    "api_uid": "CmaweWfHTNKOFABMOMrlAg==",
    "_nano_fp": "XpmYXpgan0mxl0TonT_ZXgfAKu4TpVbigK894tJT",
    "_bee": "yqf3uKQSTXbdy80DMDehwi20WJtkRam6",
    "_f77": "88ebfb0b-3918-44b3-841c-273300b748f3",
    "_a42": "d65c2710-c318-428e-8dfd-1e1d0763cc0d",
    "rckk": "yqf3uKQSTXbdy80DMDehwi20WJtkRam6",
    "ru1k": "88ebfb0b-3918-44b3-841c-273300b748f3",
    "ru2k": "d65c2710-c318-428e-8dfd-1e1d0763cc0d"
}
url = "https://seller.kuajingmaihuo.com/bg/quiet/api/mms/login"
data = {
    "loginName": "15588889999",
    "encryptPassword": "",
    "keyVersion": "1"
}
# data = json.dumps(data, separators=(',', ':'))
js = execjs.compile(open('7.拼多多.js', encoding='utf-8').read())
encrypt_encryptPassword = js.call("get_encrypt_data",'123qweasd')
data['encryptPassword']= encrypt_encryptPassword
print(f'参数:{data}')
response = requests.post(url, headers=headers, json=data)
print(response.text)
print(response)

image-20250305204315404

4.33搜帧逆向

地址:https://fse.agilestudio.cn/search?keyword=火车呼啸而过

image-20250305205117662

4.1关键字搜索

请求头中带有x-signature,肯定有加密或赋值的地方,所以我们直接尝试搜索x-signature

image-20250305225013071

然后打断点确认,e.params是请求参数, _是加密方法

 return e.params["_platform"] = "web",
            e.params["_versioin"] = "0.2.5",
            e.headers["X-Signature"] = _(e.params),
            e

image-20250305224954736

4.2逆向

跳转到_(e.params)的实现,l()(i)为最后加密方法

var i = n("bc3a")
          , o = n.n(i)
          , a = n("4360")
          , r = n("5f87")
          , s = n("eae4")
          , c = n("b85c")
          , u = n("6821")
          , l = n.n(u)
          , d = function(e) {
            e._ts = (new Date).getTime() - 9999;
            var t, n = Object.keys(e), i = "", o = Object(c["a"])(n.sort());
            try {
                for (o.s(); !(t = o.n()).done; ) {
                    var a = t.value;
                    void 0 !== e[a] && null !== e[a] && (i += "".concat(a, "=").concat(e[a], ","))
                }
            } catch (r) {
                o.e(r)
            } finally {
                o.f()
            }
            return console.log("signature:", i),
            l()(i)
        }

image-20250305225158920

c = n("b85c")这种写法就很像webpack,我们先跳转到n的实现,在n的实现在确定一下

image-20250305225445384

发现o(n) 就是模块加载器,确定就是webpack

function o(n) {
                    if (t[n])
                        return t[n].exports;
                    var c = t[n] = {
                        i: n,
                        l: !1,
                        exports: {}
                    };
                    return e[n].call(c.exports, c, c.exports, o),
                    c.l = !0,
                    c.exports
                }

image-20250305225631402

这个webpack在页面中,我们复制整个代码到编辑器中

image-20250305225715563

声明一个window变量用于模拟浏览器环境

声明外部变量module,在webpack内部的最后进行赋值,赋值给模块加载器

因为我们外部代码需要测试调用

console.log(n);moduleArr.push(n);是为了要执行的模块的下标或者键,方便确认代码的调用流程

console.log(module('bc3a'))是为了测试调用整个webpack代码

image-20250305230431778

执行代码如果只是下面问题的话就说明缺少执行的模块

接下来需要找到执行的模块

image-20250305230748162

在浏览器控制台输出缺少的模块值,然后通过控制台输出跳转到模块实现,根据代码可以确定这是对象形式的webpack

当然我们也可以把所有模块都复制过来

n.m['bc3a']

image-20250305230924172

我们复制整个模块到编辑器

image-20250305231231995

复制这1182个模块

image-20250305231355337

粘贴到webpack代码自执行函数的参数中(这是对象形式,使用{})

image-20250305231457259

然后执行脚本,我们发现变量c都需要84个脚本,虽然代码很多,导致编辑器卡顿,但是不建议按需加载模块,一个模块一个模块的去找,我这里就直接复制全部模块

image-20250305231730823

4.3js逆向代码

测试完成后,需要修改js逆向的代码给python调用

我就写个具体修改的地方,算做个总结

window = global
var module;
!function (e) { 
     // ...其它代码
     function o(n) {
            if (t[n])
                return t[n].exports;
            var c = t[n] = {
                i: n,
                l: !1,
                exports: {}
            };
            console.log(n)
            moduleArr.push(n);
            return e[n].call(c.exports, c, c.exports, o),
                c.l = !0,
                c.exports
     }
     ,
     // 存储了所有模块
     o.m = e,
     // ...其它代码
     // c()
     module = o;
}({
   // 所有模块的代码 模块的具体实现
   "0094": function(e, t, n){},
   "00ad": function(e, t, n),
   // 其他模块代码
   ...
})

// 测试代码
// console.log(module('b85c'))
// console.log(moduleArr.join(','))
// console.log(moduleArr.length)


var e = {
    'keyword': '火车呼啸而过',
    'page': '1',
    'limit': '12',
    '_platform': 'web',
    '_versioin': '0.2.5',
    '_ts': 1741183716088,
}

function get_encrypt_data(e) {
    var c = module("b85c"),
        u = module("6821"),
        l = module.n(u);
    var t, n = Object.keys(e), i = "", o = Object(c["a"])(n.sort());
    for (o.s(); !(t = o.n()).done;) {
        var a = t.value;
        void 0 !== e[a] && null !== e[a] && (i += "".concat(a, "=").concat(e[a], ","))
    }
    return l()(i)
}

// console.log(get_encrypt_data(e))

image-20250305232622688

4.4python调用

代码

记得检查自己的headerscookies、参数

# 第一种
import subprocess
import time
from functools import partial

subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")

import execjs
import requests

headers = {
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Origin": "https://fse.agilestudio.cn",
    "Pragma": "no-cache",
    "Referer": "https://fse.agilestudio.cn/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-site",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
    "X-Signature": "",
    "sec-ch-ua": "\"Not A(Brand\";v=\"8\", \"Chromium\";v=\"132\", \"Google Chrome\";v=\"132\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\""
}
url = "https://fse-api.agilestudio.cn/api/search"
params = {
    "keyword": "火车呼啸而过",
    "page": "1",
    "limit": "12",
    "_platform": "web",
    "_versioin": "0.2.5",
    "_ts": int(round(time.time() * 1000))
}
js = execjs.compile(open('10.33搜帧.js', encoding='utf-8').read())
sign_encrypt = js.call("get_encrypt_data", params)
headers["X-Signature"] = sign_encrypt
response = requests.get(url, headers=headers, params=params)

print(response.text)
print(response)

image-20250305232746418

5.爱奇艺登录逆向

地址:https://m.iqiyi.com/user.html#baseLogin

接口:https://passport.iqiyi.com/apis/reglogin/login.action

网页版没法测试登录

image-20250306030317114

我看了下移动版可以,反正都是逆向加密,我就看看这个移动版的

image-20250306030156846

5.1逆向webpack

查了2个多小时左右,本来以为webpack很快能搞定,但是代码嵌套太多了

一个webpack嵌套另一个webpack,只是加密的功能,所以我还是决定扣代码

image-20250306030429216

5.2逆向代码

关键字搜索passwd,基本上可以确认rsaUtil.RSAEncryption就是加密方法了

if (opts.param.passwd) {
                opts.param.passwd = rsaUtil.RSAEncryption(opts.param.passwd);
            }

image-20250306030906692

跳转到RSAEncryption实现 ,这里很简单,只需要找到RSA的生成

//RSA加密
RSAEncryption: function(proclaimedCode) {
var EXPONENT_KEY = '10001';
var MODULUS_KEY = 'ab86b6371b5318aaa1d3c9e612a9f1264f372323c8c0f19875b5fc3b3fd3afcc1e5bec527aa94bfa85bffc157e4245aebda05389a5357b75115ac94f074aefcd';

//参数1为公钥的exponent(hex),参数2请留空,参数3为公钥的modulus(hex)
var RSA_key = RSA.getKeyPair(EXPONENT_KEY, '', MODULUS_KEY);
//要加密的字符必须encode编码,replace是因为如果要加密的字符过长自动分段和后台商定用'-'分割
var result = RSA.encryptedString(RSA_key, encodeURIComponent(proclaimedCode)).replace(/\s/g, '-');
return result;
}

image-20250306031019730

在上面找到RSA

 var RSA = __webpack_require__(55);

image-20250306031142533

在这里断点,然后转到实现,看代码可以确定这是一个webpack

image-20250306031353120

我们跳转到__webpack_require__.m[55]的实现

image-20250306031518813

我们复制里面的自执行函数,里面是RSA代码的实现

image-20250306031634321

删除$,我们不是浏览器环境,也没有jquery环境

image-20250306031739349

我们这里优化一下代码,缺什么补什么

需要$w作为参数,我们就在外部声明一下全局,然后传进去

再给RSAUtils默认值

var $w = global || window; // 兼容 Node.js 和浏览器

(function ($w) {
    // 其他代码
    ...
    
    var RSAUtils = {};

    if (typeof $w.RSAUtils === 'undefined') {
        $w.RSAUtils = RSAUtils; // 将 RSAUtils 挂载到全局
    } else {
        RSAUtils = $w.RSAUtils; // 复用全局的 RSAUtils
    }

    // 其他代码
    ...
})($w);

image-20250306032125042

然后我们把页面代码修改一下,基本就没什么问题了

RSAEncryption = function (proclaimedCode) {
    var EXPONENT_KEY = '10001';
    var MODULUS_KEY = 'ab86b6371b5318aaa1d3c9e612a9f1264f372323c8c0f19875b5fc3b3fd3afcc1e5bec527aa94bfa85bffc157e4245aebda05389a5357b75115ac94f074aefcd';

    //参数1为公钥的exponent(hex),参数2请留空,参数3为公钥的modulus(hex)
    var RSA_key = RSAUtils.getKeyPair(EXPONENT_KEY, '', MODULUS_KEY);
    //要加密的字符必须encode编码,replace是因为如果要加密的字符过长自动分段和后台商定用'-'分割
    var result = RSAUtils.encryptedString(RSA_key, encodeURIComponent(proclaimedCode)).replace(/\s/g, '-');
    return result;
}
console.log(RSAEncryption('123qweasd'))

image-20250306032355391

因为登录时还需要验证码,这里暂时先完成加密

📌 创作不易,感谢支持!
每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!

wxzf
posted @ 2025-04-09 16:11  peng_boke  阅读(1478)  评论(1)    收藏  举报