爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)

总体概览:使用Session登录该网站,其中包括对password参数进行js逆向破解

                (涉及加密:md5加密+AES-CBC加密)

难度:两颗星

目标网址:aHR0cHM6Ly93d3cuZnhiYW9nYW8uY29tLw==

下面文章将分为四个部分:

        1、定位主体加密函数,进行断点

        2、分析整体加密流程

        3、对JS代码进行扣取、补充、重写

        4、使用Session进行登录

运行效果展示:

JS逆向运行截图:

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第1张图片

Session登陆成功截图:

正文开始: 

第一步、定位主体加密函数,进行断点

首先进入网站,点击登陆后进行抓包,很明显可以看出这里的密码(data参数)被进行了加密。

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第2张图片

        由于data参数比较常见不太好搜索,因此我们对url的接口byPhoneNumber进行搜索

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第3张图片

        搜索一共只有三个结果,比较少就都点进去看看,发现第一个很明显就是加密的主体函数并且 l 为加密的数据,直接对 l 参数进行断点后再次点击登陆,确定这个就是我们所需要的函数

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第4张图片

第二步、分析整体加密流程

var o = e.password
, s = e.mobile
, a = A.dnGetter()
, l = D().ne("".concat(a).concat(o), _()("".concat(s.slice(3)).concat(a)));

直接看代码的话初步得出以下结论:

o为密码,s为账号,a疑似一个时间戳(这个后面再去验证),l 为加密后的一串数据

其中 l 为核心,下面对 l 进行分析:

1、其中可以看出括号内前半截是a与o的一个简单的拼接

2、后半截的话是先对密码部分截取第三位数之后的数据并与a进行拼接,并通过 _() 函数进行处理,发现 _() 是对拼接后的数据进行了md5的加密

3、在括号内的数据处理完成后调用D函数下的ne方法再进行一次处理

 

  得到这些信息后开始扣取JS代码并补充缺少的函数

第三步、对JS代码进行扣取、补充、重写

步骤1:对a参数进行补充

首先将主体的加密函数复制下来写到自己定义的一个方法里,文中可以看出需要传入e参数(账号密码信息),下面是主体函数的代码:

var get_password = function (e){
        var o = e.password
      , s = e.mobile
      , a = A.dnGetter()
      , l = D().ne("".concat(a).concat(o), _()("".concat(s.slice(3)).concat(a)));
        return l
}
var e = {
    "mobile": "17688888888",
    "password": "Wu123123123"
}
console.log(get_password(e))

很明显这里缺少了 A.dnGetter() 函数,找到这个函数点击进入发现它是一个名为 ot 的函数,写入JS代码中,并将原来的 A.dnGetter() 改写为 ot() 

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第5张图片

 

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第6张图片

 点击运行,发现缺少参数c,在此设置断点,运行了几次之后发现c为固定值false,因此我们直接令他为false,同时我们发现这句话还缺少了 window.D 参数 ,运行发现 这个也是固定值,补充完后运行一下结果没毛病。

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第7张图片

 

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第8张图片爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第9张图片

window = global
window.D = 1
var ot = function() {
    return false || "number" !== typeof window.D ? +String(Date.now()).substr(0, 10) : +String(Date.now()).substr(0, 10) + window.D
}

 接下来就是我们的重头戏了!!

步骤2:对D.ne函数进行补充

首先是进入D函数下的ne方法,进入发现这里是一个很明显的AES-CBC加密,将D改为一个对象,将ne方法放入D中,并将原本的D().ne() 改写为 D.ne     (这里有两种写法,我个人更喜欢写成D.ne一点看着会简单一些,如果想要 D().ne()的写法的话也可以,都放在下面了)

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第10张图片

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第11张图片爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第12张图片

 写法1:

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第13张图片

 写法2:

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第14张图片

 再次运行发现缺少了对象,由于这里的格式与AES加密一模一样,因此在这里我将o替换成了JavaScript中的 crypto-js 库,这是一个经常用于加解密的库,调用前需要安装 jsdom

安装及使用方法可以看我上一篇文章所讲的:AES加密和解密详解_浩·的博客-CSDN博客

let CryptoJS = require('crypto-js');		// 调用crypto-js 模块
let D = {
        ne: function(e, t) {
        return CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(e), CryptoJS.enc.Utf8.parse(t.substr(0, 32)), {
            iv: CryptoJS.enc.Utf8.parse(t.substr(-16)),
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }).ciphertext.toString().toUpperCase()
    }
}

 步骤3:对 _() 函数进行补充与改写

前面分析过了在这下面的部分其实是将_()后面的数据进行了md5加密,会改写JS代码的或者使用python替代的可以直接跳过这一步不看。下面这部分主要还是练习一下对JS的逆向能力。对md5加密函数进行还原操作。

这里有一个小坑,将鼠标移到 _() 上后发现调用的是a函数,需要将a先展开再进入a中。

可以看见这里传入了我们前面所讲的后半截数据,因此可以将原本的

_()(后半截数据)  改写为 _(后半截数据) 

同时为了美观,我们将 _  函数改名为exports,具体改写后的代码如下:

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第15张图片爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第16张图片

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第17张图片

exports = function(e, n) {
    if (void 0 === e || null === e)
        throw new Error("Illegal argument " + e);
    var r = t.wordsToBytes(i(e, n));
    return n && n.asBytes ? r : n && n.asString ? a.bytesToString(r) : t.bytesToHex(r)
}
var get_password = function (e){
        var o = e.password
      , s = e.mobile
      , a = ot()
      , l = D.ne("".concat(a).concat(o), exports(("".concat(s.slice(3)).concat(a))));
        return l
}

再次运行,发现对象 t 及其附带的函数wordsToBytes、bytesToHex都未被定义,给t下断点,进入t中。刚好发现我们想要的两个函数都在里面,直接带走。新建一个对象t,并放入这两个函数。

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第18张图片爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第19张图片

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第20张图片

let t = {
    wordsToBytes: function(e) {
        for (var t = [], n = 0; n < 32 * e.length; n += 8)
            t.push(e[n >>> 5] >>> 24 - n % 32 & 255);
        return t
    },
    bytesToHex: function(e) {
        for (var t = [], n = 0; n < e.length; n++)
            t.push((e[n] >>> 4).toString(16)),
            t.push((15 & e[n]).toString(16));
        return t.join("")
    }
}

 再次运行再 t 的旁边还缺少了个 i  

下面对 i 进行补充,进入 i 并打下断点,将i先全扣下来再运行

这里报错 r 未被定义,这个比较明显,因为r就在上面,可以看见 r 为一个对象,包含了两个方法

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第21张图片爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第22张图片

 

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第23张图片

可以看见 r 调用的utf8就在这里,同时utf8的两个方法还用到了下面的bin,因此在这我们直接扣过去 并新建一个 r 对象保存。由于单个单词 t 容易造成重复,我们这里对 对象t改了一下名字,最后再 i 函数中调用

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第24张图片爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第25张图片

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第26张图片

 再次运行,缺少了t.bytesToWords,回到刚刚的地方继续重复操作,定位过去发现这里是刚刚扣 t 对象的地方,我们只需要将新的给补充进去

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第27张图片

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第28张图片

 然后重复,运行、下断点、进入函数,发现v函数是再上面被定义的,

我们进入定义它的地方 i.ff 函数中,顺路吧剩下的四个一起扣下来

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第29张图片

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第30张图片爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第31张图片

 继续重复刚刚的操作扣 t.endian([l, s, f, d]) ,发现这里还调用了上面两个函数,一起带走。

要注意一下这里名字变了,扣过去的时候要改回来,并且把下面的 n.rotl 也改成 t.rotl

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第32张图片

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第33张图片

 改完之后发现这里有个bug,如果要将n改为t的话这就与上面的for 循环中的t变量冲突了,因此我们将for循环的变量改为其他的。 于是这里改为图二。爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第34张图片

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第35张图片

 再次点击运行,发现已经大功告成啦!!!!!

 爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第36张图片

到这里逆向部分就已经完成了,下面进行登录操作

第四步、使用Session进行登录

查看登录发送的包,发现这里有一个时间戳,根据逆向时候知道的这个时间戳需要用来加密,因此我们需要将这个时间戳与JS的时间戳保持一直。所以我们需要在python上生成时间戳,然后发送给JS,对此我们还要将JS的时间戳改为一个传入的参数,即传入a(时间戳参数)

爬虫案例-使用Session登录指定网站(JS逆向AES-CBC加密+MD5加密)_第37张图片

// 主函数改写后:
var get_pas_word = function(e,a){
    var o = e.password
      , s = e.mobile
      // , a = ot()
    var l = D.ne("".concat(a).concat(o), aaa(("".concat(s.slice(3)).concat(a))));
    return l
}
var e_dic = {
    "mobile": "17836797789",
    "password": "123wu123123"
}
a = 1686207919
console.log(get_pas_word(e_dic,a))

全部代码:Session登录+JS逆向代码

JS加密代码:

window = global

var ot = function() {
    return false || "number" !== typeof window.D ? +String(Date.now()).substr(0, 10) : +String(Date.now()).substr(0, 10) + window.D
}

let CryptoJS = require('crypto-js');		// 调用crypto-js 模块
let D = {
        ne: function(e, t) {
        return CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(e), CryptoJS.enc.Utf8.parse(t.substr(0, 32)), {
            iv: CryptoJS.enc.Utf8.parse(t.substr(-16)),
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }).ciphertext.toString().toUpperCase()
    }
}

let r_obj = {
    utf8: {
        stringToBytes: function(e) {
            return r_obj.bin.stringToBytes(unescape(encodeURIComponent(e)))
        },
        bytesToString: function(e) {
            return decodeURIComponent(escape(r_obj.bin.bytesToString(e)))
        }
    },
    bin: {
        stringToBytes: function(e) {
            for (var t = [], n = 0; n < e.length; n++)
                t.push(255 & e.charCodeAt(n));
            return t
        },
        bytesToString: function(e) {
            for (var t = [], n = 0; n < e.length; n++)
                t.push(String.fromCharCode(e[n]));
            return t.join("")
        }
    }
}

i = function(e, n) {
    var r = r_obj.utf8
                e.constructor == String ? e = n && "binary" === n.encoding ? a.stringToBytes(e) : r.stringToBytes(e) : o(e) ? e = Array.prototype.slice.call(e, 0) : Array.isArray(e) || e.constructor === Uint8Array || (e = e.toString());
                for (var c = t.bytesToWords(e), u = 8 * e.length, l = 1732584193, s = -271733879, f = -1732584194, d = 271733878, p = 0; p < c.length; p++)
                    c[p] = 16711935 & (c[p] << 8 | c[p] >>> 24) | 4278255360 & (c[p] << 24 | c[p] >>> 8);
                c[u >>> 5] |= 128 << u % 32,
                c[14 + (u + 64 >>> 9 << 4)] = u;

                i._ff = function(e, t, n, r, o, a, i) {
                    var c = e + (t & n | ~t & r) + (o >>> 0) + i;
                    return (c << a | c >>> 32 - a) + t
                }
                ,
                i._gg = function(e, t, n, r, o, a, i) {
                    var c = e + (t & r | n & ~r) + (o >>> 0) + i;
                    return (c << a | c >>> 32 - a) + t
                }
                ,
                i._hh = function(e, t, n, r, o, a, i) {
                    var c = e + (t ^ n ^ r) + (o >>> 0) + i;
                    return (c << a | c >>> 32 - a) + t
                }
                ,
                i._ii = function(e, t, n, r, o, a, i) {
                    var c = e + (n ^ (t | ~r)) + (o >>> 0) + i;
                    return (c << a | c >>> 32 - a) + t
                }
                var v = i._ff
                  , m = i._gg
                  , h = i._hh
                  , y = i._ii;



                for (p = 0; p < c.length; p += 16) {
                    var g = l
                      , b = s
                      , x = f
                      , w = d;
                    l = v(l, s, f, d, c[p + 0], 7, -680876936),
                    d = v(d, l, s, f, c[p + 1], 12, -389564586),
                    f = v(f, d, l, s, c[p + 2], 17, 606105819),
                    s = v(s, f, d, l, c[p + 3], 22, -1044525330),
                    l = v(l, s, f, d, c[p + 4], 7, -176418897),
                    d = v(d, l, s, f, c[p + 5], 12, 1200080426),
                    f = v(f, d, l, s, c[p + 6], 17, -1473231341),
                    s = v(s, f, d, l, c[p + 7], 22, -45705983),
                    l = v(l, s, f, d, c[p + 8], 7, 1770035416),
                    d = v(d, l, s, f, c[p + 9], 12, -1958414417),
                    f = v(f, d, l, s, c[p + 10], 17, -42063),
                    s = v(s, f, d, l, c[p + 11], 22, -1990404162),
                    l = v(l, s, f, d, c[p + 12], 7, 1804603682),
                    d = v(d, l, s, f, c[p + 13], 12, -40341101),
                    f = v(f, d, l, s, c[p + 14], 17, -1502002290),
                    l = m(l, s = v(s, f, d, l, c[p + 15], 22, 1236535329), f, d, c[p + 1], 5, -165796510),
                    d = m(d, l, s, f, c[p + 6], 9, -1069501632),
                    f = m(f, d, l, s, c[p + 11], 14, 643717713),
                    s = m(s, f, d, l, c[p + 0], 20, -373897302),
                    l = m(l, s, f, d, c[p + 5], 5, -701558691),
                    d = m(d, l, s, f, c[p + 10], 9, 38016083),
                    f = m(f, d, l, s, c[p + 15], 14, -660478335),
                    s = m(s, f, d, l, c[p + 4], 20, -405537848),
                    l = m(l, s, f, d, c[p + 9], 5, 568446438),
                    d = m(d, l, s, f, c[p + 14], 9, -1019803690),
                    f = m(f, d, l, s, c[p + 3], 14, -187363961),
                    s = m(s, f, d, l, c[p + 8], 20, 1163531501),
                    l = m(l, s, f, d, c[p + 13], 5, -1444681467),
                    d = m(d, l, s, f, c[p + 2], 9, -51403784),
                    f = m(f, d, l, s, c[p + 7], 14, 1735328473),
                    l = h(l, s = m(s, f, d, l, c[p + 12], 20, -1926607734), f, d, c[p + 5], 4, -378558),
                    d = h(d, l, s, f, c[p + 8], 11, -2022574463),
                    f = h(f, d, l, s, c[p + 11], 16, 1839030562),
                    s = h(s, f, d, l, c[p + 14], 23, -35309556),
                    l = h(l, s, f, d, c[p + 1], 4, -1530992060),
                    d = h(d, l, s, f, c[p + 4], 11, 1272893353),
                    f = h(f, d, l, s, c[p + 7], 16, -155497632),
                    s = h(s, f, d, l, c[p + 10], 23, -1094730640),
                    l = h(l, s, f, d, c[p + 13], 4, 681279174),
                    d = h(d, l, s, f, c[p + 0], 11, -358537222),
                    f = h(f, d, l, s, c[p + 3], 16, -722521979),
                    s = h(s, f, d, l, c[p + 6], 23, 76029189),
                    l = h(l, s, f, d, c[p + 9], 4, -640364487),
                    d = h(d, l, s, f, c[p + 12], 11, -421815835),
                    f = h(f, d, l, s, c[p + 15], 16, 530742520),
                    l = y(l, s = h(s, f, d, l, c[p + 2], 23, -995338651), f, d, c[p + 0], 6, -198630844),
                    d = y(d, l, s, f, c[p + 7], 10, 1126891415),
                    f = y(f, d, l, s, c[p + 14], 15, -1416354905),
                    s = y(s, f, d, l, c[p + 5], 21, -57434055),
                    l = y(l, s, f, d, c[p + 12], 6, 1700485571),
                    d = y(d, l, s, f, c[p + 3], 10, -1894986606),
                    f = y(f, d, l, s, c[p + 10], 15, -1051523),
                    s = y(s, f, d, l, c[p + 1], 21, -2054922799),
                    l = y(l, s, f, d, c[p + 8], 6, 1873313359),
                    d = y(d, l, s, f, c[p + 15], 10, -30611744),
                    f = y(f, d, l, s, c[p + 6], 15, -1560198380),
                    s = y(s, f, d, l, c[p + 13], 21, 1309151649),
                    l = y(l, s, f, d, c[p + 4], 6, -145523070),
                    d = y(d, l, s, f, c[p + 11], 10, -1120210379),
                    f = y(f, d, l, s, c[p + 2], 15, 718787259),
                    s = y(s, f, d, l, c[p + 9], 21, -343485551),
                    l = l + g >>> 0,
                    s = s + b >>> 0,
                    f = f + x >>> 0,
                    d = d + w >>> 0
                }
                return t.endian([l, s, f, d])
            }

t = {
    wordsToBytes: function(e) {
        for (var t = [], n = 0; n < 32 * e.length; n += 8)
            t.push(e[n >>> 5] >>> 24 - n % 32 & 255);
        return t
    },
    bytesToWords: function(e) {
        for (var t = [], n = 0, r = 0; n < e.length; n++,
        r += 8)
            t[r >>> 5] |= e[n] << 24 - r % 32;
        return t
    },
    bytesToHex: function(e) {
        for (var t = [], n = 0; n < e.length; n++)
            t.push((e[n] >>> 4).toString(16)),
            t.push((15 & e[n]).toString(16));
        return t.join("")
    },
    rotl: function(e, t) {
        return e << t | e >>> 32 - t
    },
    rotr: function(e, t) {
        return e << 32 - t | e >>> t
    },

    endian: function(e) {
        if (e.constructor == Number)
            return 16711935 & t.rotl(e, 8) | 4278255360 & t.rotl(e, 24);
        for (var x = 0; x < e.length; x++)
            e[x] = t.endian(e[x]);
        return e
    }
}

aaa = function (e,n){
    // if (void 0 === e || null === e)
    //     throw new Error("Illegal argument " + e);
    var r = t.wordsToBytes(i(e, n));
    return n && n.asBytes ? r : n && n.asString ? a.bytesToString(r) : t.bytesToHex(r)
}

var get_pas_word = function(e,a){
    var o = e.password
      , s = e.mobile
      // , a = ot()
    var l = D.ne("".concat(a).concat(o), aaa(("".concat(s.slice(3)).concat(a))));
    return l
}
var e_dic = {
    "mobile": "17836797789",
    "password": "123wu123123"
}
a = 1686207919
console.log(get_pas_word(e_dic,a))

最终python登录代码:

# -*- coding: UTF-8 -*-
'''
@Project :wbh_pj 
@File :111.py
@Author :hao
@Date :2023/6/8 15:11 
'''
import time
import execjs
import requests

headers = {
    'authority': 'api.fxbaogao.com',
    'accept': '*/*',
    'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7',
    'content-type': 'application/json; charset=UTF-8',
    'origin': 'https://www.fxbaogao.com',
    'referer': 'https://www.fxbaogao.com/',
    'sec-ch-ua': '"Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    '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/113.0.0.0 Safari/537.36',
    'user-id': '0',
    'user-token': 'QQ',
}

def get_pas_word(e_dic,now_time):
    filename_js = '123.js'
    with open(filename_js, encoding='utf-8', mode='r') as f:
        pw_js = f.read()
        f.close()
    js1 = execjs.compile(pw_js)
    print('********* 正在生成 -- pas_word *********')
    pas_word = js1.call('get_pas_word', e_dic,now_time)         # 获取token
    print(pas_word)
    return pas_word

def login():
    now_time = int(time.time())      # 获取时间戳
    e_dic = {
        "mobile": "17688888888",
        "password": "123wu123123"
    }
    pas_word = get_pas_word(e_dic,now_time)

    json_data = {
        'mobile': '17688688515',
        'data': pas_word,
        'time': now_time,
    }
    session = requests.session()
    # response = requests.post('https://api.fxbaogao.com/mofoun/user/login/byPhoneNumber', headers=headers,
    #                          json=json_data)
    response1 = session.post('https://api.fxbaogao.com/mofoun/user/login/byPhoneNumber', headers=headers,json=json_data)
    print(response1.text)

if __name__ == '__main__':
    login()

JS逆向部分总结:

        1、在登录的时候对password参数与时间戳拼接;

        2、对账号参数切及时间戳的拼接;

        3、将部分关键数据转数组后先进行MD5加密处理;

        4、最终将前面数据全部拼接对数据使用AES下的CBC加密

        5、得到password的参数之后使用request模块的Session进行登录操作

AES加密解密:AES加密和解密详解_aesdecrypt_浩·的博客-CSDN博客

知名外卖平台项目:JS逆向---获取某知名外卖平台数据(_token)_浩·的博客-CSDN博客

更多爬虫项目请看:浩 的个人爬虫项目

你可能感兴趣的:(爬虫项目大全,网络爬虫,JS逆向,Session登录,AES加密,MD5加密)