在渗透测试或安全评估中,我们对前端js代码的关注度是很高的,因为JS代码中会经常包含一些接口、前端路由、敏感信息和请求参数的加解密逻辑。为防止攻击者篡改数据包,开发者常在前端对请求数据进行加密(比如登录密码、接口的参数sign、_signature、一些cookie等)或者 对返回包信息解密,这使得我们的请求或者返回包的修改和查看变得困难,JS逆向本就是一件费时间的事情,这时候小天向大家介绍一种技术:JSRPC ,它可以使得这一情况迎刃而解。
远程方法调用概述
RPC(Remote Procedure Call,远程过程调用)是一种通过网络让程序调用另一台计算机上的服务或程序的技术。它允许程序在不同的地址空间(通常是不同的计算机)之间调用函数或方法 ,而JSRPC则属于在 JavaScript 环境中的 RPC 技术的实现,rpc的技术本质很复杂我们只需要会用即可。
在前端当中经常出现JS加密的,这时候我们使用JSRPC技术则不需要关心他加密函数的具体实现方法就可以做到直接调用加密函数得到结果。
简单举个例子说明:比如请求中的加密参数sign由加密函数a
通过一系列信息(如搜索内容、时间戳等)生成。
一般情况下,我们会通过扣代码、补环境、还原加解密算法等方式解决,这样实现如果在加密逻辑很复杂同时又有很多的环境检测或者调用的情况下就比较费时间或者难以完成。这时候如果我们使用JSRPC技术可以使我们直接调用加密函数a来实现请求的加密,就像是本地函数调用一样,而a是我们写好的函数只需我们传个参数就可以有返回值。
协议介绍
因为JSRPC一般都是基于WebSocket或者WebSocket Secure协议实现的,在这里介绍一下这俩协议:
WebSocket是基于TCP的应用层协议,WSS(WebSocket Secure)是 WebSocket 的加密版本 ,WS 和 WSS的关系类似与http和https,他们采用的是双向通信模式,类似以根两端开口的管道,当客户端和服务器之间建立连接后,不论是客户端还是服务端都可以随时发送数据给对方,WebSocket协议请求url为ws://
开头,WebSocket Secure 请求协议则为wss://
开头,并且通常每隔一段时间需要发送心跳包维持长连接( 心跳包可以是一个简单的 Ping 消息 )。
一般在社交聊天室、股票实时报价、直播间信息流等场景中会存在。
大致步骤
操作方法步骤如下思路简明如下:
- 服务端:
- 搭建 WebSocket 服务端,接收需要加密的参数并返回加密结果。
- 客户端:
- 创建 WebSocket 客户端,当前作用域调用加密函数
a
,处理接收到的加密参数,并返回结果。 - 也可以将加密函数
a
绑定到window
,供 WebSocket 客户端调用。
- 创建 WebSocket 客户端,当前作用域调用加密函数
- 代码注入:
- 使用
Fiddler
、Chrome Overrides(浏览器本地替换)
、Mitmproxy
、谷歌的GRPC协议
、浏览器插件 ReRes
、浏览器控制台
等 注入 WebSocket 客户端代码到目标网页,确保代码不污染原有逻辑 (一般我们都写成自执行的形式)。
- 使用
- 数据交互:
- 服务端发送加密参数,客户端调用加密函数
a
加密后返回结果。
- 服务端发送加密参数,客户端调用加密函数
案例实战
这次我们拿某团登录的来举例,大家练练手
地址:aHR0cHM6Ly9wYXNzcG9ydC5tZWl0dWFuLmNvbS9hY2NvdW50L3VuaXRpdmVsb2dpbg==
初步分析
输入手机号,密码,抓包对比每次的请求
其中uuid、token_id、csrf在页面源码中都是存在的,直接xpath、正则、css选择器啥的获取到就OK。
多次尝试之后发现需要处理的值有两个password和h5Fingerprint,而这里的password的加密实现很简单,而h5Fingerprint这个很长很长的值就是一个h5页面的指纹,我们这个值用JSRPC实现加密,对于password这个好欺负的值我们还是老规矩,先猜一下。
首先我们计算一下他的比特位,因为password最终是base64编码形式,这里先给大家说一下base64的一些相关知识(在上一篇文章也提到了):
- base64每4个字符代表3个编码前数据的原始字节
- 如果原始数据的字节数不是3的倍数,那么最后一批数据将被填充,以确保Base64编码的输出是4字符的倍数。
- 一个等号表示原始数据最后多了1个字节(即原始数据总字节数除以3余1)。两个等号表示原始数据最后多了2个字节(即原始数据总字节数除以3余2)。
计算如下,所以我们可以得出这一串加密密码的长度是1024个比特位
结合我每次输入的密码都是不变的情况下,password的值每次都在变且都固定为1024位,说明很可能是密钥长度为1024位的RSA加密,又因为最后结果是base64编码形式所以大概率是PKCS#1的填充模式,同时单纯的RSA算法无法加密大量文本,当我们在密码框输入很多的数据抓包如下:
可以看到在输入大量的数据情况下,请求正常发送但是password直接变为了false,说明几乎确定了加密方式为RSA加密。
验证猜测
我们直接搜索setPublicKey(rsa加密的关键字),可以看到以下结果
然后我们点进去倒数第二个很容易发现h5Fingerprint和password的加密函数,如下所示。
或者我们不猜,直接搜索password =
也可以找到password和h5Fingerprint加密的位置,这样还更快(JS常见的加密关键字encrypt),如下:
直接在encrypt.setPublicKey方法运行处打个断点就能找到PublicKey,同时也看到了encrypt是JSEncrypt的对象, 而JSEncrypt 又是一个用于在 JavaScript 中实现 RSA 加密的库,所以这个rsa加密应该是未魔改的,如果魔改了大家直接使用jsrpc或者扣代码补环境即可,这个不难 ,公钥如下。
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRD8YahHualjGxPMzeIWnAqVGMI rWrrkr5L7gw+5XT55iIuYXZYLaUFMTOD9iSyfKlL9mvD3ReUX6Lieph3ajJAPPGEu SHwoj5PN1UiQXK3wzAPKcpwrrA2V4Agu1/RZsyIuzboXgcPexyUYxYUTJH48DeYBG Je2GrYtsmzuIu6QIDAQAB
简单单步跟进就能发现填充方法 PKCS#1
有了公钥和填充方法要实现一个单纯的RSA加密很简单,这里就不再演示了,无论是用python还是直接使用JS都可以,大家直接GPT即可,这里重点说一下通过JSRPC生成h5Fingerprint的结果。
手动实现
客户端
首先我们通过本地替换的方法来实现,我们在对应存在h5Fingerprint加密方法的JS代码的页面当中点击鼠标右键选择替换内容。
上方会弹出来一个这个提示,我们点击选择文件夹。
选择好之后上方会询问是否允许本地替换,我们点击允许,这样我们就可以在本地修改他的JS代码了,使得我们可以自定义他的运行逻辑 (也可以用以下工具实现Fiddler
、Mitmproxy
、谷歌的GRPC协议
、浏览器插件 ReRes
)
然后我们在h5Fingerprint的加密下方插入客户端的代码,为了防止污染环境这里改为了自执行函数
注意点:这里因为在当前作用域所以我们直接调用,否则需要将方法提升到全局例如赋给window。
简单的客户端的模板写法:
(function() {
if (window.flagSkyx) return;
try {
var ws = new WebSocket("ws://127.0.0.1:1234");
window.flagSkyx = true;
ws.onmessage = function(param) {
console.log("接受到参数: " + param.data);
if (param.data === 'exit') {
ws.close(); // 关闭 WebSocket
} else {
// 自定义代码逻辑
ws.send(aa(param.data)); // 使用 aa 函数处理参数并发送
}
};
} catch {
console.log("连接出错");
window.flagSkyx = false; // 连接错误时设置为 false
}
})();
服务端
因为加密函数的参数为window.location.origin + url
所以我们需要先构造一下这个值,这个url很简单,其实就是一个登录的url后跟了一些当前页面的固定参数,我们这里就先通过copy方法来直接复制这个值用来后续测试 (这个值在当前页面中是固定的,除非是刷新之后会导致变化):
https://passport.meituan.com/account/unitivelogin?risk_partner=-1&risk_platform=1&risk_app=-1&joinkey=&uuid=4aa6a78d699f4973ba3b.1728630140.1.0.0&token_id=DNCmLoBpSbBD6leXFdqIxA&service=www&continue=https://www.meituan.com/account/settoken?continue=https%3A%2F%2Fwww.meituan.com
我们将以下的代码粘贴到python当中运行,这里用作示范,我们实际当中可以直接使用框架、工具即可快速构造服务端和客户端,不用自己写这些代码:
import asyncio
import websockets
async def receive_message(websocket, path):
try:
while True:
send_text = input("请输入要加密的字符串: ")
if send_text.lower() == "exit":
await websocket.send(send_text)
break
await websocket.send(send_text)
response_text = await websocket.recv()
print("\n加密结果:", response_text)
except websockets.exceptions.ConnectionClosed:
print("连接已关闭。")
except Exception as e:
print(f"发生错误: {e}")
async def main():
async with websockets.serve(receive_message, '127.0.0.1', 1234):
print("WebSocket 服务器已启动,等待连接...")
await asyncio.Future() # 保持服务器运行
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n服务器已停止。")
我们先运行服务端!然后手动触发一次登录操作,触发一次登录操作!!(保证连接触发)即可连接成功,在python中运行如下,可以看到成功调用了前端加密函数并且得到了结果。
自己写太费事了,而且我们如果想将服务端变为api的形式部署到服务器的话还需自己优化接口,同时考虑很多情况:比如有的网站只支持wss怎么办?如果有CSP策略怎么办?如果有多个加密函数需要提供rpc实现怎么办?..,这时候小天向大家推荐两款来解决这些问题,如下所示。
JsRpc工具的使用
部署方法
JsRpc是一款十分优秀的go语言工具,在21年的时候发布,如今已经1.2k的star,为JS逆向和渗透测试的JSRPC调用提供了很大的便捷。
项目地址:https://github.com/jxhczhl/JsRpc
我们下载编译的版本,同时讲配置文件config.yaml放到同一目录下面
下方的目录是默认设置,如果需要https/wss服务则需要从阿里云、腾讯云等申请免费的https证书。
将客户端代码初始化代码(JsEnv_Dev.js)插入到浏览器环境,可以在页面打开可以在页面打开的时候就先注入环境(初始化代码是固定的),也可以和**接口注册(红框框起来的)**一起插入到浏览器。
接口注册代码如下,客户端初始化代码就是JsEnv_Dev.js文件里面的,是固定的
// 注入环境后连接通信
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=test");
// 可选
//var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz&clientId=hliang/"+new Date().getTime())
demo.regAction("getH5fingerprint", function (resolve,param) {
//这样添加了一个param参数,http接口带上它,这里就能获得 aa是加密参数
resolve(aa(param));
})
详细解释
- zzz是分组,注册连接的时候随便写,调用接口的时候要对应,这个就是起到区分的作用
- ws/wss代表是注册连接的接口,调用的则将其改为 go
- hello2 是调用接口时候的action参数值
- param就是调用接口所传递的参数 多个参数:param[‘aa’],param[‘bb’] 多参数的时候可以用json格式传递 ,项目文档中的示例如下:
实战演示
我们演示分开插入的方法,这两个工具的原理是类似的我们在下一个工具结合本地替换演示注册和初始化一起插入的方法。
首先运行服务端程序
然后打开登录页面,直接将客户端初始化代码输入到控制台然后回车(下面的输出不用管)
然后在加密处下个断点。
点击登录触发断点,.在控制台输入window.H5Fingerprint=utility.getH5fingerprint
将方法提升到全局
然后取消断点,继续运行,然后在浏览器控制台输入以下代码:
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=meituan");
demo.regAction("H5fingerprint", function (resolve,param) {
resolve(window.H5Fingerprint(param));
})
显示rpc连接成功
服务端也看到了连接成功的客户端id
此时访问接口,已经有了加密后的data数据
http://127.0.0.1:12080/go?group=meituan&action=H5fingerprint¶m=123
所以现在我们可以直接通过接口调用即可获得加密后的数据,我们后续可以通过python自动获取同时结合 Mitmproxy +burpsuite进行渗透测试,当然如何在burpsuite利用js逆向的结果在后续的文章中小天会为大家总结,这里就先不赘述了。
常见问题
-
websocket连接失败,内容安全策略:这个网站不让连接websocket,可以用油猴注入使用,或者更改网页响应头
-
异步操作获取值 这里可以参考:https://github.com/jxhczhl/JsRpc/issues/12
-
只允许wss连接需要下载ca证书,从腾讯云或者阿里云 申请免费的https证书,要.pem和.key文件的,然后放到目录里,再在配置文件里配置后启动即可
-
记得将方法提升到全局之后放开断点
Sekiro框架的使用
框架介绍
JsRpc是用 go 语言写的专门为 JS 逆向做的项目,而sekiro是由邓维佳(渣总)写的一个基于长链接和代码注入的 API 服务暴露框架,通用性更强,功能也更加强大,**它可以在APP逆向、JS逆向、**Android 群控、app爬虫等场景使用,同时提供了市面上主流编程语言的客户端demo以及十分完善的中文使用文档
官方文档:https://sekiro.iinti.cn/sekiro-doc/
部署方法
首先先在以下的url下载服务端程序
https://oss.iinti.cn/sekiro/sekiro-demo
需要有java环境,需要JDK版本在1.8以上。
下载好之后解压,打开bin文件夹如下所示,以下就是启动我们PRC服务端的脚本。
Linux就运行.sh,windows系统则运行.bat,本机是windows系统,我们需要先启动服务端运行效果如下:
连接端口的更改在conf文件夹下的config.properties文件当中。
需要将以下代码插入到浏览器(官网有,代码太长了这里就不粘贴了),然后触发页面的功能点即可连接,蓝框的是客户端初始化代码可以在页面打开的时候直接插入浏览器运行也可以和后面红框代码一起运行,重点是红框的事件注册和建立ws/wss连接的代码。
详细代码请查看:
https://sekiro.iinti.cn/sekiro-doc/01_manual/1.quickstart.html#浏览器js环境
function SekiroClient(e){if(this.wsURL=e,this.handlers={},this.socket={},!e)throw new Error("wsURL can not be empty!!");this.webSocketFactory=this.resolveWebSocketFactory(),this.connect()}SekiroClient.prototype.resolveWebSocketFactory=function(){if("object"==typeof window){var e=window.WebSocket?window.WebSocket:window.MozWebSocket;return function(o){function t(o){this.mSocket=new e(o)}return t.prototype.close=function(){this.mSocket.close()},t.prototype.onmessage=function(e){this.mSocket.onmessage=e},t.prototype.onopen=function(e){this.mSocket.onopen=e},t.prototype.onclose=function(e){this.mSocket.onclose=e},t.prototype.send=function(e){this.mSocket.send(e)},new t(o)}}if("object"==typeof weex)try{console.log("test webSocket for weex");var o=weex.requireModule("webSocket");return console.log("find webSocket for weex:"+o),function(e){try{o.close()}catch(e){}return o.WebSocket(e,""),o}}catch(e){console.log(e)}if("object"==typeof WebSocket)return function(o){return new e(o)};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 e=this;try{this.socket=this.webSocketFactory(this.wsURL)}catch(o){return console.log("sekiro: create connection failed,reconnect after 2s:"+o),void setTimeout(function(){e.connect()},2e3)}this.socket.onmessage(function(o){e.handleSekiroRequest(o.data)}),this.socket.onopen(function(e){console.log("sekiro: open a sekiro client connection")}),this.socket.onclose(function(o){console.log("sekiro: disconnected ,reconnection after 2s"),setTimeout(function(){e.connect()},2e3)})},SekiroClient.prototype.handleSekiroRequest=function(e){console.log("receive sekiro request: "+e);var o=JSON.parse(e),t=o.__sekiro_seq__;if(o.action){var n=o.action;if(this.handlers[n]){var s=this.handlers[n],i=this;try{s(o,function(e){try{i.sendSuccess(t,e)}catch(e){i.sendFailed(t,"e:"+e)}},function(e){i.sendFailed(t,e)})}catch(e){console.log("error: "+e),i.sendFailed(t,":"+e)}}else this.sendFailed(t,"no action handler: "+n+" defined")}else this.sendFailed(t,"need request param {action}")},SekiroClient.prototype.sendSuccess=function(e,o){var t;if("string"==typeof o)try{t=JSON.parse(o)}catch(e){(t={}).data=o}else"object"==typeof o?t=o:(t={}).data=o;(Array.isArray(t)||"string"==typeof t)&&(t={data:t,code:0}),t.code?t.code=0:(t.status,t.status=0),t.__sekiro_seq__=e;var n=JSON.stringify(t);console.log("response :"+n),this.socket.send(n)},SekiroClient.prototype.sendFailed=function(e,o){"string"!=typeof o&&(o=JSON.stringify(o));var t={};t.message=o,t.status=-1,t.__sekiro_seq__=e;var n=JSON.stringify(t);console.log("sekiro: response :"+n),this.socket.send(n)},SekiroClient.prototype.registerAction=function(e,o){if("string"!=typeof e)throw new Error("an action must be string");if("function"!=typeof o)throw new Error("a handler must be function");return console.log("sekiro: register action: "+e),this.handlers[e]=o,this};
var client = new SekiroClient("ws://127.0.0.1:5612/business-demo/register?group=test_web&clientId=" + Math.random());
client.registerAction("testAction", function (request, resolve, reject) {
resolve("ok");
});
请注意,如果目标网站是https,且demo无法正确连接,请下载证书并安装到你的系统中
我们重点关注后面这一段 (前面的代码相当于初始化可以页面打开的时候优先插入到浏览器,也可以和接口注册一起插入)
var client = new SekiroClient("ws://127.0.0.1:5612/business-demo/register?group=test_web&clientId=" + Math.random());
client.registerAction("testAction", function (request, resolve, reject) {
resolve("ok");
});
详细说明
在var client = new SekiroClient("ws://127.0.0.1:5612/business-demo/register?group=test_web&clientId=" + Math.random());
中
- new SekiroClient():就是新建一个ws或者wss连接
- ws: 是代表了WebSocket协议,wss则为WebSocket Secure协议; 127.0.0.1:5612 连接地址加端口默认端口是5612 ; business-demo是版本; register代表是连接注册 调用接口的时候要将register改为invoke ; test_web是分组 ;clientId就是随机的一个ID 访问的时候加不加都可以
在client.registerAction("testAction", function (request, resolve, reject) { resolve("ok");});
中
- testAction调用接口时候的action参数
- request是访问时候的请求,可以通过request[‘param’]来获取访问接口的param参数,参数名可自定义(上个工具JsRpc的参数名只能是param),多个参数:request[‘param1’],request[‘param1’]… 也可以使用json的方法传递 (具体大家看官网)
如下是一个访问的接口例子
http://127.0.0.1:5612/business/invoke?group=test_frida&action=testAction¶m1=testparm1¶m2=testparm2
因为我们不是商业版的,所以将business改为了business-demo
使用实战
还是那上面讲某团登录举例子,我们先启动本地的服务端
我们在上面本地替换后的JS文件当中加密函数附近输入,window.getH5fingerprint=utility.getH5fingerprint
,将加密方法赋给全局变量window,然后触发一下登录操作(也可以先个打断点,再在控制台将加密函数暴露到window全局之后放开断点,不要一直打着断点,不然是没法成功通信的)。
浏览器控制台直接输入下方代码,上面那一长串不用管,带着就行,重要的是框起来的部分,我这里因为将方法提升到全局了大家也可以在替换后的JS文件当中直接写入客户端代码从而直接调用加密函数(作用域要对)。
在浏览器控制台执行效果如下:
这样说明了连接成功了,然后我们直接访问下方url,即可得到加密的结果(123只为了演示)。
http://127.0.0.1:5612/business-demo/invoke?group=meituan&action=getH5fingerprint&input=123对于input参数的传递用get或者是post都可以,同时也支持json格式传递
常见问题
我们一般在本地回环地址127.0.0.1结合ws协议使用即可,如果https页面出现错误请到Sekiro框架官网安装证书,官网详解如下:
https://sekiro.iinti.cn/sekiro-doc/02_advance/03_sslForWebsocket.html
如果出现以下报错,说明是CSP策略禁止了连接。
refused to connect to 'wss://sekiro.iinti.cn/business/register?group=ws-group&clientId=1221' because of it violates the following Content Security Policy directive: xxxxxx
可以按照官网的解决办法:使用浏览器代理、使用子域名绕过csp等,同时需要安装Sekiro的CA证书,篇幅有限这里就不多说了,况且官网已经十分详细了。地址如下:
https://sekiro.iinti.cn/sekiro-doc/02_advance/03_sslForWebsocket.html
大家如果还是有疑问建议去仔细阅读Sekiro框架官网