一、确定爬取目标
今日受害者网址: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)
这样就顺利拿到热帖的内容了,当然如果想让格式更加美观自己可以后期再处理
今天的内容就分享到这里了,我们下期再见~