
jsRPC技术
更新: 2025/2/24 字数: 0 字 时长: 0 分钟
RPC 是指远程过程调用,jsRPC 就是借助 websocket 利用 js 代码实现远程控制浏览器控制台的一种手段,提供了一种浏览器与代码以及接口交互的一种可行性。
jsRPC特点
jsRPC优势:
- 理论上只要可以突破所有网站在js层面上的反爬限制;
- jsRPC与传统自动化框架相比,稳定性更高且不需要考虑浏览器指纹。操作得当的话,性能不在一个数量级。
- 如果搭建在服务器公网IP上,建立多个group,那么可以在任何地点、任何时间实现群控抓取。
- 可以通过js直接获取加密参数,对于全局变量的参数暴露具有极佳的体验。【一剑破光阴】
- 可以直接获取接口数据,对于获取数据接口暴露明显的网站具有极佳的体验。
- 不考虑风控的情况下,能达到高并发。
jsRPC缺陷:
内置不可更改的浏览器环境不可更改,对于风控突破而言是个极大的隐患【浏览器沙箱可能会解决这个问题】
需要对js有较深理解,需要寻找合适的注入时机和注入位置【这个算缺陷么?菜是原罪】。
需要注入脚本,所以要依托于 油猴脚本/autoResponse/手动注入【如果使用抓包工具需要注意端口问题】。
对于全局js混淆、风控严格、js参数分布散乱、鼠标轨迹指纹、变量作用域复杂的网址的支持较差。
需要保持浏览器开启,所以多少会占用些资源。
如果不取参数,而是只取接口内容或者参数与IP绑定的加密,对代理池接入的支持不佳。
?> 提示:详细文档参看https://sekiro.virjar.com/sekiro-doc/
jsRPC使用
安装java环境
首先我们需要安装java1.8版本的环境,访问官网地址:https://www.oracle.com/java/technologies/downloads/#java8-windows ,下载和电脑对应的JDK安装包进行安装:
详细的安装教程以及环境配置参看:https://blog.csdn.net/DH626942210/article/details/123283008
安装好以后,在命令行执行 java -version
命令出现如下信息,就说明环境配置好了:
安装sekiro工具
网盘地址:https://oss.iinti.cn/ 文件路径:sekiro/sekiro-demo
访问地址:https://oss.iinti.cn/sekiro/sekiro-demo
下载最新版的sekiro工具:
下载好以后,解压会得到三个文件夹:
进入bin文件夹,Windows系统运行 sekiro.bat
文件,Linux系统运行 sekiro.sh
文件,出现如下界面就说明服务启动,准备工作完成了:
注入代码
首先注入代码前,保证前面启动的sekiro工具服务窗口不能关闭,接下里将如下两个地址里面的代码内容合并到一起:
https://sekiro.virjar.com/sekiro-doc/assets/sekiro_web_client.js
https://sekiro.virjar.com/sekiro-doc/assets/sekkiro_js_demo.html
function SekiroClient(wsURL) {
this.wsURL = wsURL;
this.handlers = {};
this.socket = {};
// check
if (!wsURL) {
throw new Error('wsURL can not be empty!!')
}
this.webSocketFactory = this.resolveWebSocketFactory();
this.connect()
}
SekiroClient.prototype.resolveWebSocketFactory = function () {
if (typeof window === 'object') {
var theWebSocket = window.WebSocket ? window.WebSocket : window.MozWebSocket;
return function (wsURL) {
function WindowWebSocketWrapper(wsURL) {
this.mSocket = new theWebSocket(wsURL);
}
WindowWebSocketWrapper.prototype.close = function () {
this.mSocket.close();
};
WindowWebSocketWrapper.prototype.onmessage = function (onMessageFunction) {
this.mSocket.onmessage = onMessageFunction;
};
WindowWebSocketWrapper.prototype.onopen = function (onOpenFunction) {
this.mSocket.onopen = onOpenFunction;
};
WindowWebSocketWrapper.prototype.onclose = function (onCloseFunction) {
this.mSocket.onclose = onCloseFunction;
};
WindowWebSocketWrapper.prototype.send = function (message) {
this.mSocket.send(message);
};
return new WindowWebSocketWrapper(wsURL);
}
}
if (typeof weex === 'object') {
// this is weex env : https://weex.apache.org/zh/docs/modules/websockets.html
try {
console.log("test webSocket for weex");
var ws = weex.requireModule('webSocket');
console.log("find webSocket for weex:" + ws);
return function (wsURL) {
try {
ws.close();
} catch (e) {
}
ws.WebSocket(wsURL, '');
return ws;
}
} catch (e) {
console.log(e);
//ignore
}
}
//TODO support ReactNative
if (typeof WebSocket === 'object') {
return function (wsURL) {
return new theWebSocket(wsURL);
}
}
throw new Error("the js environment do not support websocket");
};
SekiroClient.prototype.connect = function () {
console.log('sekiro: begin of connect to wsURL: ' + this.wsURL);
var _this = this;
// 涓峜heck close锛岃
// if (this.socket && this.socket.readyState === 1) {
// this.socket.close();
// }
try {
this.socket = this.webSocketFactory(this.wsURL);
} catch (e) {
console.log("sekiro: create connection failed,reconnect after 2s");
setTimeout(function () {
_this.connect()
}, 2000)
}
this.socket.onmessage(function (event) {
_this.handleSekiroRequest(event.data)
});
this.socket.onopen(function (event) {
console.log('sekiro: open a sekiro client connection')
});
this.socket.onclose(function (event) {
console.log('sekiro: disconnected ,reconnection after 2s');
setTimeout(function () {
_this.connect()
}, 2000)
});
};
SekiroClient.prototype.handleSekiroRequest = function (requestJson) {
console.log("receive sekiro request: " + requestJson);
var request = JSON.parse(requestJson);
var seq = request['__sekiro_seq__'];
if (!request['action']) {
this.sendFailed(seq, 'need request param {action}');
return
}
var action = request['action'];
if (!this.handlers[action]) {
this.sendFailed(seq, 'no action handler: ' + action + ' defined');
return
}
var theHandler = this.handlers[action];
var _this = this;
try {
theHandler(request, function (response) {
try {
_this.sendSuccess(seq, response)
} catch (e) {
_this.sendFailed(seq, "e:" + e);
}
}, function (errorMessage) {
_this.sendFailed(seq, errorMessage)
})
} catch (e) {
console.log("error: " + e);
_this.sendFailed(seq, ":" + e);
}
};
SekiroClient.prototype.sendSuccess = function (seq, response) {
var responseJson;
if (typeof response == 'string') {
try {
responseJson = JSON.parse(response);
} catch (e) {
responseJson = {};
responseJson['data'] = response;
}
} else if (typeof response == 'object') {
responseJson = response;
} else {
responseJson = {};
responseJson['data'] = response;
}
if (Array.isArray(responseJson)) {
responseJson = {
data: responseJson,
code: 0
}
}
if (responseJson['code']) {
responseJson['code'] = 0;
} else if (responseJson['status']) {
responseJson['status'] = 0;
} else {
responseJson['status'] = 0;
}
responseJson['__sekiro_seq__'] = seq;
var responseText = JSON.stringify(responseJson);
console.log("response :" + responseText);
this.socket.send(responseText);
};
SekiroClient.prototype.sendFailed = function (seq, errorMessage) {
if (typeof errorMessage != 'string') {
errorMessage = JSON.stringify(errorMessage);
}
var responseJson = {};
responseJson['message'] = errorMessage;
responseJson['status'] = -1;
responseJson['__sekiro_seq__'] = seq;
var responseText = JSON.stringify(responseJson);
console.log("sekiro: response :" + responseText);
this.socket.send(responseText)
};
SekiroClient.prototype.registerAction = function (action, handler) {
if (typeof action !== 'string') {
throw new Error("an action must be string");
}
if (typeof handler !== 'function') {
throw new Error("a handler must be function");
}
console.log("sekiro: register action: " + action);
this.handlers[action] = handler;
return this;
};
function guid() {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
}
var client = new SekiroClient("wss://sekiro.virjar.com/business/register?group=ws-group&clientId=" + guid());
client.registerAction("clientTime", function (request, resolve, reject) {
resolve("" + new Date());
})
接下来还需要配置一下上面 SekiroClient
的URL:wss://sekiro.virjar.com/business/register?group=ws-group&clientId=
ws:代表http开头的地址
wss:代表https开头的地址
sekiro.virjar.com:这是其他人的地址(不使用)
127.0.0.1:5620:使用本地的地址以及默认端口5620(使用)
business:代表sekiro工具是商业版
business-demo:代表sekiro工具是测试版(根据上面工具版本)
ws-group:代表group组群(可自定义)
?> 提示:需要自定义端口可以修改sekiro工具里的conf文件夹里的 config.properties
文件内容。
最终的URL内容如下:
http开头:
ws://127.0.0.1:5620/business-demo/register?group=ws-group&clientId=
https开头:
wss://127.0.0.1:5620/business-demo/register?group=ws-group&clientId=
现在我们访问 https://match.yuanrenxue.com/list 地址,将代码注入到浏览器中:从最下面提示可以看到已经开启了websocket链接。
我们再打开一个选项卡,访问如下地址:http://127.0.0.1:5620/business-demo/invoke?group=ws-group&action=clientTime
127.0.0.1:5620:代表本机的端口号
business-demo:代表sekiro工具版本
clientTime:代表注入代码中的clientTime函数的行为
可以看到地址返回的内容,就是注入代码中 clientTime
函数的执行结果的返回值:
返回所注入的页面,也打印了响应的内容:
?> 提示:如果注入 wss
开头的不能成功,就尝试注入 ws
开头的。
接口调用
既然访问上面的地址我们就可以直接获取到对应的action行为函数响应,那么通过Python代码调用该接口也可以获取响应:
jsRPC实战
难度:困难
这道题是内部题目,就不提供网址了,只分享解题流程。
这里首先还是要保证sekiro工具已经正常运行,且服务窗口未关闭。我们先将需要注入的代码中最后一部分需要修改的拿出来,修改后内容如下:
// ws:代表http开头的地址
// 127.0.0.1:5620:使用本地的地址以及默认端口5620(使用)
// business-demo:代表sekiro工具是测试版
// match5:代表group组群名称
var client = new SekiroClient("ws://127.0.0.1:5620/business-demo/register?group=match5&clientId=" + guid());
// 注册行为名称GetData
client.registerAction("GetData", function (request, resolve, reject) {
// 发送Ajax请求的js代码
})
题目解析
打开开发者工具,访问题目地址,会出现无限 debugger
干扰,我们放开断点继续加载就能出现题目页面:
在XHR选项中我们找到了获取数据的请求,通过Initiator进行逆向分析:
找到了发送XHR请求的js代码,可以看到 call()
就是发送获取数据请求的函数,其最后一行是 call(1)
代表着发送获取第1页的数据请求:
我们将这段代码拷贝下来,进行修改,内容如下:
var url = "/api/challenge5";
// 新增,上面注册行为GetData中request请求,而page就代表着请求URL中page对应的页码
num = request['page']
call = function (num) {
var list = {
"page": String(num),
"token": window.token,
};
$.ajax({
url: url,
dataType: "json",
async: true,
data: list,
type: "POST",
beforeSend: function (request) {
(function () {
var httpRequest = new XMLHttpRequest();
var url = '/cityjson';
httpRequest.open('POST', url, false);
httpRequest.send()
})()
},
success: function (data) {
window[data.k['k'].split('|')[0]] = parseInt(data.k['k'].split('|')[1]);
var s = '<tr class="odd">';
datas = data.data;
// 新增,上面注册行为GetData中resolve,表示返回括号中的内容
resolve(data);
$.each(datas, function (index, val) {
var html = '<td class="info">' + val.value + '</td>';
s += html
});
$('.data').text('').append(s + '</tr>')
},
complete: function () {
$("#page").paging({
nowPage: num,
pageNum: 100,
buttonNum: 7,
canJump: 1,
showOne: 1,
callback: function (num) {
call(num)
},
})
},
error: function () {
alert('加载失败败')
location.reload()
}
})
};
// 修改,将call(1)改为call(num),其num来源上面page对应的页码
call(num)
**将修改好的代码粘贴到我们开始修改的注入代码中的注册行为 GetData
中发送Ajax请求的js代码位置,再结合未修改的注入代码部分,代码就算弄好了。**通过Console选项卡注入到浏览器中执行,结果如下:
现在我们再打开一个窗口,访问如下地址:http://127.0.0.1:5620/business-demo/invoke?group=match5&action=GetData&page=1
127.0.0.1:5620:代表本机的端口号
business-demo:代表sekiro工具版本
group=match5:代表组群名称
action=GetData:代表注册行为名称
page=1:代表获取第1页的数据
可以看到,我们通过该接口成功的拿到了第1页返回的数据:
这里我们将URL中的page改为5,同样可以返回第5页的数据:
这样我们就完成了对该道题目的解析,是不是很简单,不用经过复杂的js逆向分析,利用jsRPC实现了一剑破光阴的效果。
爬虫代码
通过上面的分析爬虫代码也很简单了,就是改变page进行翻页100次,就能拿到全部的数据了:
import json
import requests
sum_value = 0
session = requests.session()
for i in range(1, 101):
response = session.get(f'http://127.0.0.1:5620/business-demo/invoke?group=match5&action=GetData&page={i}')
for item in json.loads(response.text).get('data'):
sum_value += int(item.get('value'))
print(sum_value)
归纳总结
前面破解JS的方法都是依托于Python三方库和其他的框架,这种方法优势和劣势都很明显:
- 优势:调用简单;不用太过关注JS代码逻辑,破解速度快;
- 劣势:第三方库只能应对现有常规加密,无法破解自定义加密;Selenium框架在大规模爬取上太重,效率低,易出错。
代码最终都是要追求效率和稳定。因此,JS逆向就是精进爬虫的必经之路,学会了JS逆向,我们有相当于又多了一条解决JS加密的道路,而且优势巨大,找爬虫类的工作也是轻而易举。
- 优势:大幅提高爬虫效率和稳定性;提升自身爬虫实力;
- 劣势:深度理解JS代码逻辑,破解速度慢。
对于想进一步提高爬虫技术的小伙伴必读《逆向》的章节。