Sekkiro对官网做出调整,文章中的相关链接也做了对应更新,但其余内容依旧保持原样,所以文中内容可能已经过时,仅供思路参考。
介绍
RPC(Remote Procedure Call)中文名「远程过程调用」,又是一个很蹩脚的翻译。我们拆开理解下,「过程」也叫方法或函数,「远程」就是说方法不在当前进程里,而是在其他进程或机器上面,合起来 RPC 就是调用其他进程或机器上面的函数。 ——聊聊 Node.js RPC(一)— 协议
JS RPC是指在浏览器开启一个ws和go服务连接,以调用http接口的形式来通信,浏览器端收到调用通信执行原先设置好的js代码。可以用于js逆向调用加密函数直接返回结果,也可以用来直接获取数据。 ——Python网络爬虫之js逆向之远程调用(rpc)免去抠代码补环境简介
上次实战,其实就算是实现了一次JS RPC实战, 对于JS RPC,和爬虫一样,可以手搓,也可以选择成熟的第三方框架,这里推荐使用 Sekrio -官方文档
Sekiro
SEKIRO 是一个android下的API服务暴露框架,可以用在app逆向、app数据抓取、android群控等场景。同时Sekiro也是目前公开方案唯一稳定的JSRPC框架。 ——来自官方文档
使用 参考官方文档,准备好环境:官方文档
首先需要确保已准备好 Java 环境,需要下载并配置JDK,随后运行bin/sekiro.bat
(或bin/sekiro.sh
)即可开启本地服务端
然后使用以下代码暴露RPC服务:
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-2&clientId=" + guid ()); client.registerAction ("clientTime" , function (request, resolve, reject ) { resolve ("SekiroTest:" + new Date ()); }); client.registerAction ("executeJs" , function (request, resolve, reject ) { var code = request['code' ]; if (!code) { reject ("need param:{code}" ); return ; } code = "return " + code; console .log ("executeJs: " + code); try { var result = new Function (code)(); resolve (result); } catch (e) { reject ("error: " + e); } });
但是在开始之前,我们先了解下一些概念
名词解释 group 业务类型(接口组),每个业务一个group。group下面可以注册多个终端(SekiroClient),同时Group可以挂载多个Action。 Group指代一个确定的业务,group下注册的client代码需要保持协议的完全一致。如微信机器人为一个group、xxx爬虫为另一个group。sekiro系统中Group全局唯一,同时Sekiro根据Group进行调用转发路由,并认为统一个group下面的所有client能够提供一致性的服务能力.也就是说,对sekiro来说,转发请求的时候,sekiro认为统一个group下的任意一个设备(手机、浏览器、客户端代码等)都是等价的,转发到任意一个终端都是相同的语义
SekiroClient 服务提供者客户端,主要场景为手机/浏览器等。最终的sekiro调用会转发到SekiroClient。每个client需要有一个惟一的clientId, ClientId最好用户自己生成,并且保证ClientId具备特定含义
action 接口,同一个Group下面可以有多个接口,分别做不同的功能。如视频app,区分用户搜索接口、用户视频接口、直播接口、用户详情接口等。action是为了方便用户开发的时候隔离多个业务进行的抽象,在sekiro层面可以帮你进行一次路由。 这是因为在实际开发过程中可以发现同一个设备,可能可以提供多个接口
clientId ClientId指代设备,多个设备使用多个机器提供API服务,提供群控能力和负载均衡能力。如在手机上,同一个接口组下面可以挂载多台设备, ClientId大家需要保证每个客户端的ClientId的唯一性。建议使用mac地址、IMEI、ip地址+进程号等真实的物理ID作为ClientId。
使用ClientId可以作为调度策略控制的ID,完成对特定设备的调用管理。
转自官网
同时如果你已经打开了本地服务,Sekiro提供了以下API:
现在来测试一下,以下代码是已经写好的DEMO
<!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Sekiro TEST</title > </head > <body > <h1 > Sekiro TEST</h1 > <script src ="https://sekiro.virjar.com/sekiro-doc/assets/sekiro_web_client.js" > </script > <script > 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 ("ws://127.0.0.1:5620/business-demo/register?group=rpc-test&clientId=" + guid ()); client.registerAction ("clientTime" ,function (request, resolve,reject ){ resolve ("" +new Date ()); }) client.registerAction ("getPwd" ,function (request, resolve,reject ){ const pwd = request['pwd' ] resolve (pwdEncypt (pwd)) }) </script > </body > </html >
浏览器打开此文件,确保本地服务开启,可以在浏览器开发人员工具中看到WS连接,访问http://127.0.0.1:5620/business-demo/groupList
,响应结果:
{ "data" : [ "rpc-test" ] , "ok" : true , "status" : 0 }
访问http://127.0.0.1:5620/business-demo/clientQueue?group=rpc-test
,响应结果:
{ "data" : [ "eddc8434-4c23-f7bb-31a6-21c4523b00d2" ] , "ok" : true , "status" : 0 }
现在,来调用以下刚才注册的Action。访问http://127.0.0.1:5620/business-demo/invoke?group=rpc-test&action=clientTime
,响应结果:
{ "__sekiro_seq__" : 2 , "clientId" : "eddc8434-4c23-f7bb-31a6-21c4523b00d2" , "data" : "Wed Jan 11 2023 21:27:39 GMT+0800 (香港标准时间)" , "status" : 0 }
可以看到,其将JS代码new Date()
的结果转发了出来。
传递参数 现在需要加密一个数据,明文为123456
,得到其加密后的结果,DEMO中注册了一个Action名为getPwd
,它需要一个pwd
参数,并调用返回加密结果,因此需要调用:http://127.0.0.1:5620/business-demo/invoke?group=rpc-test&action=getPwd&pwd=123456
响应结果:
{ "__sekiro_seq__" : 3 , "clientId" : "38617c08-b07d-40c0-d256-0ec1670cf20d" , "data" : "123456abc123" , "status" : 0 }
其中data
便是期待的加密值(模拟)
实战-控制台注入方式 地址:http://q.10jqka.com.cn/
目标是个股行情
,每次翻页都带有Cookie且不一致,为Cookie反爬
控制台中执行document.cookie
,可以拿到对应的Cookie值,并猜测有JS代码在生成其值,可以在控制台执行以下代码,以在每次document.cookie
值发生改变时,自动断住,方便在堆栈中找到可能的生成代码:
(function ( ) { Object .defineProperty (document , 'cookie' , { set : function (val ) { if (val.indexOf ('v' ) != -1 ) { debugger ; } console .log ('Hook捕获到cookie设置->' , val); return val; } }); })();
执行后,翻页一下,在堆栈中搜索,可以轻松找到相关代码:
按往常是可以开始扣代码了,但是现在使用RPC方式,要做的就是将对应加密函数导出到全局然后供RPC客户端调用,在控制台输入并执行:
然后选择右上角的停用断点
,取消调试状态并放行,这样就可以注入对应的RPC代码了
同时记得确保JS上下文为TOP,这样才能在控制台执行刚刚导出的encypt
,并且后面的操作都要在此种执行
在控制台,输入JsClient所有源码然后执行,以及暴露RPC服务的代码:
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 ("ws://127.0.0.1:5620/business-demo/register?group=ths&clientId=" + guid ()); client.registerAction ("getCookie" ,function (request, resolve,reject ){ resolve (encypt ()); })
此时打开http://127.0.0.1:5620/business-demo/groupList
,响应结果:
{ "data" : [ "ths" ] , "ok" : true , "status" : 0 }
表明一切正常
此时再访问http://127.0.0.1:5620/business-demo/invoke?group=ths&action=getCookie
,响应结果:
{ "__sekiro_seq__" : 2 , "clientId" : "8db06d70-f1d1-defa-e975-13d3b9a0573a" , "data" : "AzUv6rCtukrtP9729bCNgsH8RLrqsuEjs3XNPbdb8wT7mlskfwL5lEO23YdE" , "status" : 0 }
可以看到Cookie值已成功被导出
现在就可以用Python脚本愉快地爬取:
import requestsdef get_cookie (): api = 'http://127.0.0.1:5620/business-demo/invoke?group=ths&action=getCookie' res = requests.get(api) return res.json()['data' ]def get_info (page ): api = f'http://q.10jqka.com.cn/index/index/board/all/field/zdf/order/desc/page/{page} /ajax/1/' headers = { "Accept" : "application/json" , "Accept-Encoding" : "gzip, deflate, br" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Cache-Control" : "no-cache" , "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54" , "Cookie" :f"v={get_cookie()} " } res = requests.get(api,headers=headers) print (res.text)if __name__ == '__main__' : get_info(1 )
实战-油猴注入方式
Tampermonkey (油猴) 是拥有 超过 1000 万用户 的最流行的浏览器扩展之一。 它适用于 Chrome、Microsoft Edge、Safari、Opera Next 和 Firefox。
它允许用户自定义并增强您最喜爱的网页的功能。用户脚本是小型 JavaScript 程序,可用于向网页添加新功能或修改现有功能。使用 Tampermonkey,您可以轻松在任何网站上创建、管理和运行这些用户脚本。
——转自官网
油猴可以让用户自行在添加脚本,并在开启对应页面时应用,可以把它认为是一个给自己注入脚本的一个工具。
可以在拓展上点击添加脚本
来添加一个新脚本
选项
含义
@name
脚本的名称
@namespace
命名空间,用来区分相同名称的脚本,一般写作者名字或者网址就可以
@version
脚本版本,油猴脚本的更新会读取这个版本号
@description
描述这个脚本是干什么用的
@author
编写这个脚本的作者的名字
@match
从字符串的起始位置匹配正则表达式,只有匹配的网址才会执行对应的脚本,例如 *
匹配所有,https://www.baidu.com/*
匹配百度等,可以参考 Python re 模块里面的 re.match()
方法,允许多个实例
@include
和 @match 类似,只有匹配的网址才会执行对应的脚本,但是 @include 不会从字符串起始位置匹配,例如 *://*baidu.com/*
匹配百度,具体区别可以参考 TamperMonkey 官方文档
@icon
脚本的 icon 图标
@grant
指定脚本运行所需权限,如果脚本拥有相应的权限,就可以调用油猴扩展提供的 API 与浏览器进行交互。如果设置为 none 的话,则不使用沙箱环境,脚本会直接运行在网页的环境中,这时候无法使用大部分油猴扩展的 API。如果不指定的话,油猴会默认添加几个最常用的 API
@require
如果脚本依赖其他 JS 库的话,可以使用 require 指令导入,在运行脚本之前先加载其它库
@run-at
脚本注入时机,该选项是能不能 hook 到的关键,有五个值可选:document-start
:网页开始时;document-body
:body出现时;document-end
:载入时或者之后执行;document-idle
:载入完成后执行,默认选项;context-menu
:在浏览器上下文菜单中单击该脚本时,一般将其设置为 document-start
接下来将通过油猴脚本方式注入RPC来逆向一个网站
目标地址:https://www.toutiao.com/
获取推荐
栏榜单数据时,有一个关键的_signature
参数
任意方式搜寻可能的生成代码(推荐搜索_signature
),发现以下代码:
但是这并不意味着逆向结束,直接转发n
,因为n
的值是生成后的结果,要导出的目标应该是一个方法,因此进入I
方法一探究竟
注意观察最后一段代码:
var o = { url : r + e };return t.data && (o.body = t.data ), (null === (n = window .byted_acrawler ) || void 0 === n || null === (a = n.sign ) || void 0 === a ? void 0 : a.call (n, o)) || ""
重点放在后面:
(null === (n = window .byted_acrawler ) || void 0 === n || null === (a = n.sign ) || void 0 === a ? void 0 : a.call (n, o)) || ""
此处解析可参考Python 爬虫进阶必备 | 某新闻资讯站点参数 _signature 逻辑分析 (无代码)
所以这段代码执行时其实干了以下事:
n = window .byted_acrawler a = n.sign a.call (n,o)
总结一下就是window.byted_acrawler.sign(window.byted_acrawler,o)
,其中window.byted_acrawler
是可以忽略的,参考Function.prototype.call() ,也就是说可以进一步简化为window.byted_acrawler.sign(o)
可以在控制台调用下:
成功拿到结果
现在可以准备RPC代码了,同时对于o
,可以试着自行构建一个一并返回,因此油猴代码如下:
(function ( ) { 'use strict' ; 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 ("ws://127.0.0.1:5620/business-demo/register?group=jrtt&clientId=" + guid ()); client.registerAction ("getUrl" , function (request, resolve, reject ) { var channelId = request.channel ; if (!channelId){ channelId = 0 ; } var urlParams = { url :'https://www.toutiao.com/api/pc/list/feed?min_behot_time=' +Math .round (new Date ().getTime ()/1000 )+'&refresh_count=5&category=pc_profile_recommend&aid=24&app_name=toutiao_web&channel_id=' +channelId } resolve ("" +urlParams.url +"&_signature=" +window .byted_acrawler .sign (urlParams)); }); })();
并且因为window.byted_acrawler
是全局的,因此并不需要导出。保存油猴脚本并启动,回到今日头条并刷新,访问Sekiro的分组情况:
{ "data" : [ "jrtt" ] , "ok" : true , "status" : 0 }
可以看到成功连接
现在来调用一下:http://127.0.0.1:5620/business-demo/invoke?group=jrtt&action=getUrl
,响应结果:
{ "__sekiro_seq__" : 2 , "clientId" : "72c7dff4-3e96-565e-449a-b5b5d397eb1b" , "data" : "https://www.toutiao.com/api/pc/list/feed?min_behot_time=1673458709&refresh_count=5&category=pc_profile_recommend&aid=24&app_name=toutiao_web&channel_id=0&_signature=_02B4Z6wo00d01Ws5WIQAAIDAe2e9vmHbDGlrHVwAADl0A6cZQQXdRzJsXzV.yrJchiysvjx.ROE4yE2PVl450x0oeAmttZk.TZlJl6Zl1arOspUvyovWNV5x7vUHvHs3SXK2nZQubYjww-1965" , "status" : 0 }
可以额外添加一个channel
参数,用来调用不同的栏目,默认channel
为0
尝试使用Python来爬一下,发现200
却一直乱码,在源码里翻找了一番也没看到有解密的意思,直接拿生成的url试试浏览器里编辑重放却没问题,编码也设置成了utf-8
,找了半天才终于发现,requests
响应头里有个'Transfer-Encoding': 'chunked'
,我猜测问题就出在这了,requests
并没有把完整的文件下载完,然后尝试添加stream=True
参数也没有用。
后来发现,请求头只保留一个user-agent
就行,我TM@#!~&*,折腾两小时才发现这样就行了,淦 Python 代码:
import requestsdef get_list (channel=None ): def get_url (c=None ): params = { 'group' :'jrtt' , 'action' :'getUrl' , 'channel' : c if c else '' } return requests.get('http://127.0.0.1:5620/business-demo/invoke' ,params=params).json()['data' ] s = requests.session() headers = { "user-agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.76" } res = s.get(url=get_url(channel),headers=headers) res_json = res.json() return res_json['data' ]if __name__ == "__main__" : print (get_list())
实战-注入、替换方式 目标:广东省公共资源交易平台
表单以及响应结果都为明文,只有在请求头有签名,总共为3个,其中一个明显为时间戳,因此总共待破解的为2个
对于头部参数,可以采用头部HOOK方式。 在控制台输入以下代码来断住:
var code = function ( ){ var org = window .XMLHttpRequest .prototype .setRequestHeader ; window .XMLHttpRequest .prototype .setRequestHeader = function (k,v ){ if (k == 'X-Dgi-Req-Signature' || k == 'X-Dgi-Req-Nonce' ){ debugger ; } return org.apply (this ,arguments ); } }var script = document .createElement ('script' ); script.textContent = '(' +code+')()' ; (document .head ||document .documentElement ).appendChild (script); script.parentNode .removeChild (script);
在调用堆栈里一顿找,最终找到此处:
断点的位置便是两个参数的生成位置,相关代码如下:
const a = Date .now (), l = lK (16 ), c = sr ([8 , 28 , 20 , 42 , 21 , 53 , 65 , 6 ]);const d = Sg ({ p : QC .stringify (o.data , { allowDots : !0 }), t : a, n : l, k : c });
f
就是请求头的副本,因此没有给出,同时sr
方法仅仅是通过Array解析字符串的,因此c
为固定值,为'k8tUyS$m'
。按照以往可以开始扣代码了,但是现在是打算使用PRC,因此可以将lK
、Sg
以及QC
全部挂载到window
对象上,方便Sekiro客户端调用。
现在使用本地替换的方式来注入RPC,启用本地替代,然后保存源码已备用
然后在顶部注入JsClient源码,以及注册一个client,这里挂载到window
对象上,然后刷新页面,继续来到断点位置,导出需要的三个方法,同样控制台输入:
window .lK = lKwindow .Sg = Sg window .QC = QC
输入完毕后是时候放行然后注册Action了,放行代码,然后在控制台输入以下代码:
window .rpcClient .registerAction ("getPost" , function (request, resolve, reject ) { let pageNum = request.page ; if (!pageNum){ pageNum = 1 ; } const c = 'k8tUyS$m' , l = window .lK (16 ), a = Date .now (), formData = { dataType :"" , openConvert :true , pageNo :pageNum, pageSize :10 , projectType :"" , publishEndTime :"" , publishStartTime :"" , secondType :"A" , siteCode :"44" , thirdType :"" , total :0 , type :"trading-type" }, d = window .Sg ({ p : window .QC .stringify (formData, { allowDots : !0 }), t : a, n : l, k : c }); resolve (JSON .stringify ({ "headers" :{ "X-Dgi-Req-Nonce" :l, "X-Dgi-Req-Signature" :d.toString (), "X-Dgi-Req-Timestamp" :a }, "form" :formData })) });
这时候访问http://127.0.0.1:5620/business-demo/invoke?group=gdggzy&action=getPost
就能拿到需要的数据了,响应结果:
{ "__sekiro_seq__" : 2 , "clientId" : "d006a706-d960-0ff8-e7e5-3e79f761bac0" , "form" : { "dataType" : "" , "openConvert" : true , "pageNo" : 1 , "pageSize" : 10 , "projectType" : "" , "publishEndTime" : "" , "publishStartTime" : "" , "secondType" : "A" , "siteCode" : "44" , "thirdType" : "" , "total" : 0 , "type" : "trading-type" } , "headers" : { "X-Dgi-Req-Nonce" : "V6yYQSTDBQk3KWrA" , "X-Dgi-Req-Signature" : "a65c32bc126fb2c17231b94139666889bef365bcf44f17e10b6aad531b306a62" , "X-Dgi-Req-Timestamp" : 1673595897604 } , "status" : 0 }
当然,既然已经修改了代码,那干脆修改的彻底一点,直接在相关代码处进行注册然后再转发也是可以的,这里不再做演示
注意,这里只是演示通过RPC拿数据,签名是否有效不在讨论范围,想爬自己再研究把