Skip to content

jsRPC技术

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

RPC 是指远程过程调用,jsRPC 就是借助 websocket 利用 js 代码实现远程控制浏览器控制台的一种手段,提供了一种浏览器与代码以及接口交互的一种可行性。

jsRPC特点

jsRPC优势:

  1. 理论上只要可以突破所有网站在js层面上的反爬限制;
  2. jsRPC与传统自动化框架相比,稳定性更高且不需要考虑浏览器指纹。操作得当的话,性能不在一个数量级。
  3. 如果搭建在服务器公网IP上,建立多个group,那么可以在任何地点、任何时间实现群控抓取。
  4. 可以通过js直接获取加密参数,对于全局变量的参数暴露具有极佳的体验。【一剑破光阴】
  5. 可以直接获取接口数据,对于获取数据接口暴露明显的网站具有极佳的体验。
  6. 不考虑风控的情况下,能达到高并发。

jsRPC缺陷:

  1. 内置不可更改的浏览器环境不可更改,对于风控突破而言是个极大的隐患【浏览器沙箱可能会解决这个问题】

  2. 需要对js有较深理解,需要寻找合适的注入时机和注入位置【这个算缺陷么?菜是原罪】。

  3. 需要注入脚本,所以要依托于 油猴脚本/autoResponse/手动注入【如果使用抓包工具需要注意端口问题】。

  4. 对于全局js混淆、风控严格、js参数分布散乱、鼠标轨迹指纹、变量作用域复杂的网址的支持较差。

  5. 需要保持浏览器开启,所以多少会占用些资源。

  6. 如果不取参数,而是只取接口内容或者参数与IP绑定的加密,对代理池接入的支持不佳。

?> 提示:详细文档参看https://sekiro.virjar.com/sekiro-doc/

jsRPC使用

安装java环境

首先我们需要安装java1.8版本的环境,访问官网地址:https://www.oracle.com/java/technologies/downloads/#java8-windows ,下载和电脑对应的JDK安装包进行安装:

20220807194456

详细的安装教程以及环境配置参看:https://blog.csdn.net/DH626942210/article/details/123283008

安装好以后,在命令行执行 java -version 命令出现如下信息,就说明环境配置好了:

20220807195329

安装sekiro工具

网盘地址:https://oss.iinti.cn/ 文件路径:sekiro/sekiro-demo

访问地址:https://oss.iinti.cn/sekiro/sekiro-demo

下载最新版的sekiro工具:

20220807165914

下载好以后,解压会得到三个文件夹:

20220807193128

进入bin文件夹,Windows系统运行 sekiro.bat 文件,Linux系统运行 sekiro.sh 文件,出现如下界面就说明服务启动,准备工作完成了:

20220807201756

注入代码

首先注入代码前,保证前面启动的sekiro工具服务窗口不能关闭,接下里将如下两个地址里面的代码内容合并到一起:

https://sekiro.virjar.com/sekiro-doc/assets/sekiro_web_client.js

https://sekiro.virjar.com/sekiro-doc/assets/sekkiro_js_demo.html

javascript
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链接。

20220807210636

我们再打开一个选项卡,访问如下地址: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 函数的执行结果的返回值:

20220807211647

返回所注入的页面,也打印了响应的内容:

20220807211944

?> 提示:如果注入 wss 开头的不能成功,就尝试注入 ws 开头的。

接口调用

既然访问上面的地址我们就可以直接获取到对应的action行为函数响应,那么通过Python代码调用该接口也可以获取响应:

20220807214553

jsRPC实战

难度:困难

这道题是内部题目,就不提供网址了,只分享解题流程。

这里首先还是要保证sekiro工具已经正常运行,且服务窗口未关闭。我们先将需要注入的代码中最后一部分需要修改的拿出来,修改后内容如下:

javascript
// 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 干扰,我们放开断点继续加载就能出现题目页面:

20220910030720

在XHR选项中我们找到了获取数据的请求,通过Initiator进行逆向分析:

20220910031145

找到了发送XHR请求的js代码,可以看到 call() 就是发送获取数据请求的函数,其最后一行是 call(1) 代表着发送获取第1页的数据请求:

20220910031428

我们将这段代码拷贝下来,进行修改,内容如下:

javascript
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选项卡注入到浏览器中执行,结果如下:

20220910035417

现在我们再打开一个窗口,访问如下地址: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页返回的数据:

20220910040111

这里我们将URL中的page改为5,同样可以返回第5页的数据:

20220910040334

这样我们就完成了对该道题目的解析,是不是很简单,不用经过复杂的js逆向分析,利用jsRPC实现了一剑破光阴的效果。

爬虫代码

通过上面的分析爬虫代码也很简单了,就是改变page进行翻页100次,就能拿到全部的数据了:

python
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代码逻辑,破解速度慢。

对于想进一步提高爬虫技术的小伙伴必读《逆向》的章节。