js逆向——客户端js生成cookie反爬

一、确定爬取目标

今日受害者网址:https://xueqiu.com/today

我们的目标是爬取雪球的热帖内容

二、逆向分析

直接全局搜索字符串,找到响应数据所在的接口(查看标头)

然后我们检索对应接口

ok,找到接口文件之后可以重放测试反爬参数了

把你认为可疑的参数抹掉,观察能否正常拿到数据

如果问题不大,下一步测试cookie存不存在反爬

上来直接人狼🐺话不多,把所有cookie都删了,发现拿不到数据包了,说明至少有一个cookie是必要的,或者说是被设置了反爬

下面就是一个个的删除测试是哪个cookie了呗。至于怎么删除,我的评价是二分法删除,直接二话不说就把客户端生成的cookie统统删掉

然后重放,发现不影响,说明客户端的那堆cookie并不影响

ok,现在我们继续来删除服务端的cookie

继续二分,可以先删除不带_的,或者什么其他的逻辑,别搞混就彳亍

剩下几个差不多的,可以直接记录下来比较,反正最后我们发现xq_a_token就是那个罪魁祸首

然后我们首先需要对xq_a_token进行定性,发现是服务端生成的(easy,内心窃喜😃)

ok,我们直接搜索xq_a_token,很快找到是在xueqiu.com/today这个接口里面进行的Set-Cookie

自然地,下一步,就是要过滤today接口了

结果发现today生成的位置一共有2处

我们先看瀑布流的第一个接口

再来看第二个接口,发现xq_a_token正是由这个接口生成

接下来我们愉快地编码实现,发现如果只是单纯地请求2次接口,得到的是相同的结果,第二次请求并不能拿到a_token

然后我们去观察浏览器上的第二次请求,发现是携带了2个cookie进行请求,才得到的xq_a_token

其中acw_tc比较容易搞定,通过会话session维持即可,重点就是由js生成的acw_sc_v2参数,这个是我们需要重点js逆向分析的对象

首先清除缓存和cookie,由于我们不清楚cookie生成的时机,为了确保正确注入,我们需要给网页下script断点,然后重新加载网页

结果脚本运行到后面遇到了无限debugger,而 无限debugger是在setcookie之前执行的

三、过掉无限debugger

下一步就是过掉无限debugger

然后我们来简单分析一下这个无限debugger

function _0x355d23(_0x450614) {
                if (('' + _0x450614 / _0x450614)[_0x55f3('0x1c', '\x56\x32\x4b\x45')] !== 0x1 || _0x450614 % 0x14 === 0x0) {
                    (function() {}
                    [_0x55f3('0x1d', '\x43\x4e\x55\x59')]((undefined + '')[0x2] + (!![] + '')[0x3] + ([][_0x55f3('0x1e', '\x77\x38\x50\x52')]() + '')[0x2] + (undefined + '')[0x0] + (![] + [0x0] + String)[0x14] + (![] + [0x0] + String)[0x14] + (!![] + '')[0x3] + (!![] + '')[0x1])());
                } else {
                    (function() {}
                    ['\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72']((undefined + '')[0x2] + (!![] + '')[0x3] + ([][_0x55f3('0x1f', '\x4c\x24\x28\x44')]() + '')[0x2] + (undefined + '')[0x0] + (![] + [0x0] + String)[0x14] + (![] + [0x0] + String)[0x14] + (!![] + '')[0x3] + (!![] + '')[0x1])());
                }
                _0x355d23(++_0x450614);
            }

由于混淆并不困难,可以尝试手动解开混淆,发现其实if..else两个流程都是一个效果,而且最前面的if条件显然也是恒成立的,所以这一大段function实现的功能其实就是一句话:(function(){}['constructor']('debugger')())

那么这条语句是什么意思呢?问一下chatgpt:

其实就等效于(Function(){debugger;}()),'debugger'其实就是执行的语句

也就是经典的:

(function anonymous(
) {
debugger;
})

下面再补充一个直观的展示:

(function(){}['constructor']('var x = 10; console.log(x);')())

ok,现在我们已经知道无限debugger的生成位置了,也就是下面这个函数:

function _0x355d23(_0x450614) {
                if (('' + _0x450614 / _0x450614)[_0x55f3('0x1c', '\x56\x32\x4b\x45')] !== 0x1 || _0x450614 % 0x14 === 0x0) {
                    (function() {}
                    [_0x55f3('0x1d', '\x43\x4e\x55\x59')]((undefined + '')[0x2] + (!![] + '')[0x3] + ([][_0x55f3('0x1e', '\x77\x38\x50\x52')]() + '')[0x2] + (undefined + '')[0x0] + (![] + [0x0] + String)[0x14] + (![] + [0x0] + String)[0x14] + (!![] + '')[0x3] + (!![] + '')[0x1])());
                } else {
                    (function() {}
                    ['\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72']((undefined + '')[0x2] + (!![] + '')[0x3] + ([][_0x55f3('0x1f', '\x4c\x24\x28\x44')]() + '')[0x2] + (undefined + '')[0x0] + (![] + [0x0] + String)[0x14] + (![] + [0x0] + String)[0x14] + (!![] + '')[0x3] + (!![] + '')[0x1])());
                }
                _0x355d23(++_0x450614);
            }

向上回溯找到调用位置

过掉无限debugger: 

不能never pause here的原因:会出现无限递归调用,而且代码生成的位置一直在变化,使得栈溢出(内存炸弹)

不能使用fiddler的原因:1、加载的太早了,有可能后面刷新控制台/再次重写了2、服务器发包后接收客户端响应超时,触发reload函数,reload之后,之前运行的hook脚本会被重置3、时机必须是设置定时器之前,如果太早重写的代码可能会被再次覆盖 

而一旦setcookie没有超时,就会加载真正的网页了,前面的流程也就彻底结束了

先hook无限debugger再hook cookie能成功的原因:执行完setcookie之后会reload(同一个文件(today),成功之后可能代码会发生变化(没超时)),由于setcookie之前没有reload所以能顺利过掉无限debugger
有没有方法能跳过/不让一个函数执行》js本地保存调试:将网页js保存到本地,把debugger函数进行修改然后使用浏览器开发者工具替换修改js,或者通过FD工具替换。

无限debbugger不会真正得死循环,而是有规律得执行逻辑,一般用定时器

四、acw_sc_v2参数的获取

由于先生成无限debugger,再setcookie,所以先执行hook debugger的脚本,再执行hook cookie的脚本

然后我们就顺利跟到了acw_sc_v2生成的位置

然后跟栈,找到参数值生成的位置

五、扣js代码

下面就进入愉快的扣代码环节了

直接放到vscode里面去跑

补充:arg1='FA6AEB89B2318F527AD3AE807660BD7BCE67DDFA'

补充后结果报错: 

我们将arg1.unsbox函数扣去出来,并解开混淆

String['prototype']['unsbox'] = function() {
    var _0x4b082b = [0xf, 0x23, 0x1d, 0x18, 0x21, 0x10, 0x1, 0x26, 0xa, 0x9, 0x13, 0x1f, 0x28, 0x1b, 0x16, 0x17, 0x19, 0xd, 0x6, 0xb, 0x27, 0x12, 0x14, 0x8, 0xe, 0x15, 0x20, 0x1a, 0x2, 0x1e, 0x7, 0x4, 0x11, 0x5, 0x3, 0x1c, 0x22, 0x25, 0xc, 0x24];
    var _0x4da0dc = [];
    var _0x12605e = '';
    for (var _0x20a7bf = 0x0; _0x20a7bf < this['length']; _0x20a7bf++) {
        var _0x385ee3 = this[_0x20a7bf];
        for (var _0x217721 = 0x0; _0x217721 < _0x4b082b['length']; _0x217721++) {
            if (_0x4b082b[_0x217721] == _0x20a7bf + 0x1) {
                _0x4da0dc[_0x217721] = _0x385ee3;
            }
        }
    }
    _0x12605e = _0x4da0dc['join']('');
    return _0x12605e;
}

结果报错:

这个我们直接静态补充完整即可:var _0x5e8b26 = '3000176000856006061501533003690027800375' 

结果又报错:

 

我们将对应函数从浏览器当中扣去出来,并解开混淆

String['prototype']['hexXor'] = function(_0x4e08d8) {
    var _0x5a5d3b = '';
    for (var _0xe89588 = 0x0; _0xe89588 < this['length'] && _0xe89588 < _0x4e08d8['length']; _0xe89588 += 0x2) {
        var _0x401af1 = parseInt(this['slice'](_0xe89588, _0xe89588 + 0x2), 0x10);
        var _0x105f59 = parseInt(_0x4e08d8['slice'](_0xe89588, _0xe89588 + 0x2), 0x10);
        var _0x189e2c = (_0x401af1 ^ _0x105f59)['toString'](0x10);
        if (_0x189e2c['length'] == 0x1) {
            _0x189e2c = '\x30' + _0x189e2c;
        }
        _0x5a5d3b += _0x189e2c;
    }
    return _0x5a5d3b;
}

再次运行得到了正确的结果:

 

我们重新加载网页,输出arg1和0x5e8b26的数值,观察他们是不是静态参数,结果发现arg1是动态生成的,0x5e8b26是静态的可以写死

 

我们直接搜索arg1,结果发现就是每次请求返回的today接口里面从script里面提取出来的 

 

ok,现在get_acw_sc_v2功能就彻底实现了,下面就是愉快的编码环节了

首先将get_acw_sc_v2函数进行封装,便于在python当中进行调用

var _0x5e8b26 = '3000176000856006061501533003690027800375'
String['prototype']['unsbox'] = function() {
    var _0x4b082b = [0xf, 0x23, 0x1d, 0x18, 0x21, 0x10, 0x1, 0x26, 0xa, 0x9, 0x13, 0x1f, 0x28, 0x1b, 0x16, 0x17, 0x19, 0xd, 0x6, 0xb, 0x27, 0x12, 0x14, 0x8, 0xe, 0x15, 0x20, 0x1a, 0x2, 0x1e, 0x7, 0x4, 0x11, 0x5, 0x3, 0x1c, 0x22, 0x25, 0xc, 0x24];
    var _0x4da0dc = [];
    var _0x12605e = '';
    for (var _0x20a7bf = 0x0; _0x20a7bf < this['length']; _0x20a7bf++) {
        var _0x385ee3 = this[_0x20a7bf];
        for (var _0x217721 = 0x0; _0x217721 < _0x4b082b['length']; _0x217721++) {
            if (_0x4b082b[_0x217721] == _0x20a7bf + 0x1) {
                _0x4da0dc[_0x217721] = _0x385ee3;
            }
        }
    }
    _0x12605e = _0x4da0dc['join']('');
    return _0x12605e;
}

String['prototype']['hexXor'] = function(_0x4e08d8) {
    var _0x5a5d3b = '';
    for (var _0xe89588 = 0x0; _0xe89588 < this['length'] && _0xe89588 < _0x4e08d8['length']; _0xe89588 += 0x2) {
        var _0x401af1 = parseInt(this['slice'](_0xe89588, _0xe89588 + 0x2), 0x10);
        var _0x105f59 = parseInt(_0x4e08d8['slice'](_0xe89588, _0xe89588 + 0x2), 0x10);
        var _0x189e2c = (_0x401af1 ^ _0x105f59)['toString'](0x10);
        if (_0x189e2c['length'] == 0x1) {
            _0x189e2c = '\x30' + _0x189e2c;
        }
        _0x5a5d3b += _0x189e2c;
    }
    return _0x5a5d3b;
}

function get_acw_sc_v2(arg1){
    var _0x23a392 = arg1['unsbox']();
    arg2 = _0x23a392['hexXor'](_0x5e8b26);
    return arg2;
}

var acw_sc_v2 = get_acw_sc_v2('FA6AEB89B2318F527AD3AE807660BD7BCE67DDFA')
console.log(acw_sc_v2);
console.log('over')

六、最终Python代码实现

然后就是具体的python代码实现爬取热帖内容了

大体上就是首先请求today接口获取acw_tc cookie,然后再次请求today接口获取关键参数acw_sc_v2,返回a_token之后就可以顺利请求热帖接口获取响应文本了

具体的代码实现如下:

import requests
import re
import execjs

headers = {
    "Connection": "keep-alive",
    "Pragma": "no-cache",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Core/1.94.253.400 QQBrowser/12.6.5678.400",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Dest": "document",
    "Referer": "https://xueqiu.com/today",
}
url_today = "https://xueqiu.com/today"
ses = requests.session()
# 第一次请求
response = ses.get(url_today,headers=headers,timeout=10)
# print(response.text)
arg1 = re.search("arg1='(.*?)'", response.text).group(1)
print(dict(ses.cookies))

# 通过js生成acw_sc_v2
with open('acw_sc_v2.js','r',encoding='utf-8') as f:
    ctx = execjs.compile(f.read())
acw_sc_v2 = ctx.call('get_acw_sc_v2', arg1)
ses.cookies.update({'acw_sc__v2': acw_sc_v2})
# 第二次请求获取xq_a_token
ses.get(url_today,headers=headers,timeout=10)
print(dict(ses.cookies))
# 第三次请求拿到热帖的具体文本
url_hot = 'https://xueqiu.com/statuses/hot/listV2.json?since_id=-1&max_id=-1&size=15'
response = ses.get(url_hot,headers=headers,timeout=10)
print(response.json())
for item in response.json()['items']:
    title = item['original_status']['title']
    description = item['original_status']['description']
    # print('标题:', title)
    print('描述:', description)

这样就顺利拿到热帖的内容了,当然如果想让格式更加美观自己可以后期再处理

 

今天的内容就分享到这里了,我们下期再见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值