免责声明
文章中涉及的内容可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
前言
最近在做一个项目的渗透,发现是存在签名的,由于存在这个签名,我们无法篡改数据包的内容,也不能重放数据包进行请求,难以进行渗透测试。于是乎花了一个小时把签名函数找到之后使用JsRpc进行远程调用,最后通过yakit的热加载即可进行动态生成签名,就可以随意篡改数据包内容和重放数据包了。
什么是JSRPC?
JSRPC(JavaScript Remote Procedure Call)是一种基于 WebSocket/WSS 协议的远程过程调用技术,允许外部程序直接调用网页中的JavaScript函数,而无需逆向其具体实现逻辑。
在渗透测试和安全评估中,前端JavaScript代码通常包含 敏感接口、加密参数(如sign、**_signature****)、数据加解密逻辑** 等。传统方法需要逆向分析加密算法,而JSRPC可以直接调用目标加密函数,极大提升测试效率。
为什么需要JSRPC?
现代Web应用普遍采用:
-
参数签名(如 sign
/token
生成) -
反爬虫混淆(WebAssembly/代码混淆) -
动态加密(AES密钥随时间变化)
传统逆向需要:
-
反混淆数万行JS代码 -
定位关键函数 -
重写加密逻辑(Python/Node.js复现)
传统逆向分析的痛点
-
复杂加密逻辑:如AES、RSA、自定义混淆算法,逆向耗时。 -
环境依赖:部分JS加密依赖浏览器环境(如 window
、document
),补环境困难。 -
频繁更新:前端加密逻辑可能动态变化,逆向方法可能失效。
JSRPC的优势
✅ 直接调用加密函数:无需关心内部实现,传入参数即可获取加密结果。
✅ 适用于动态加密:即使加密逻辑变化,只要函数名不变,仍可调用。
✅ 减少逆向工作量:特别适合sign
、密码加密、数据解密等场景。
JSRPC核心原理
JSRPC基于 WebSocket(ws://
)或 WebSocket Secure(wss://
)协议,采用 客户端-服务端 架构:
|
|
---|---|
服务端 |
|
客户端 |
|
通信协议 |
|
工作流程:
-
注入JS客户端:将WebSocket客户端代码注入目标网页。 -
暴露加密函数:将目标加密函数绑定到 window
对象,供远程调用。 -
服务端调用:外部程序通过WebSocket发送参数,客户端执行加密并返回结果。
flowchart TB A[客户端] -->|1. 建立CDP连接| B(Chrome DevTools Protocol) B -->|2. 注入调试器| C[目标网页JS上下文] A -->|3. 请求函数调用| C C -->|4. 执行目标函数| D[签名/加密逻辑] D -->|5. 返回结果| C C -->|6. 回传数据| A
实战破解签名
当我在前端输入了手机号和验证码后进行登录请求,抓包后发现数据包中有一个si字段。当我进行第三次重放数据包请求的时候发现报错了。
喵一眼就知道大概和header的si字段有关,于是我把headeer的si删除之后,再次发起请求后发现还是一样的报错,说明存在签名校验。存在签名校验的话,我就无法进行重放攻击去进行一些漏洞测试,比如越权漏洞、爆破漏洞等。
于是开始从前端中寻找签名的方法,首先是进行一次请求,然后查看网络请求,查看我们的启动器。
注意⚠️:启动器的内容是从下往上调用的,也就是说最下面的代码是最先执行的。我首先分析e.doctorLoginByCode启动器的代码进行调试。观察作用域的内容发现,此时签名还没生成,于是进行往上看
经过一条条debug分析后发现,在介于下面的这两个启动器之间生成的签名。
于是分别对这两个启动器下断点进行每一步调试
直到调试定位到了一个bs的函数调用,因为经过执行这个函数之后在作用域就能看到了签名。这段代码的大致意思也是处理一些数据然后设置给请求头(也就是si)
我们F111跟进去这个函数看看具体的函数内容,确认这段混淆后的代码正是进行加密的函数,具体的函数内容我们可以不用深入研究,只需要看我们需要传入什么数据即可。我们看到bsh函数中需要传入的内容
分析发现我们需要传入的内容正是我们进行请求时的body的内容,如果没有内容则传入一个空对象,避免函数报错。
既然签名函数被找到了,那么我们就进行简单的测试调用。我们需要修改一下原本的代码,插入一些代码方便我们调试。
右键选择“替换内容”,然后在顶部“选择文件夹”处选择存放我们修改后的代码的位置。
插入一段代码尝试进行调用这个加密函数进行加密内容,然后打印到控制台。
console.log("加密开始!");console.log(r.default.bs("{'account':'15555555555','code':'123123'}"));console.log("加密结束!");
工具下载地址:
https://github.com/jxhczhl/JsRpc/releases
工具的具体使用就不多说了,github官方有使用的示例。
https://github.com/jxhczhl/JsRpc
api 简介
-
/list
:查看当前连接的ws服务 (get) -
/ws
:浏览器注入ws连接的接口 (ws | wss) -
/wst
:ws测试使用-发啥回啥 (ws | wss) -
/go
:获取数据的接口 (get | post) -
/execjs
:传递jscode给浏览器执行 (get | post) -
/page/cookie
:直接获取当前页面的cookie (get) -
/page/html
:获取当前页面的html (get)
首先我们先执行工具,直接在终端执行即可,window的话,可以直接双击exe文件执行,这个是go写的工具,作者已经编译好了多个平台的可执行文件。
resouces文件夹中有一个JsEnv_Dev.js文件,可用于构建通信环境。
打开JsEnv 复制粘贴到网站控制台(注意:可以在浏览器开启的时候就先注入环境,不要在调试断点时候注入!!!)
https://github.com/jxhczhl/JsRpc/tree/main/resouces
然后连接通信这里的group自定义,我这里就随便写了mt,注册的方法是si,也可以随意写。param是进行传参,也可以随便写和下面对应就行。r.default.bs就是我们找到的签名函数。
-
group :自定义,我这里就随便写了mt -
si :注册的方法是si,也可以随意写 -
resolve:一个回调函数,用于向服务端返回处理结果,默认 -
param :param是进行传参,也可以随便写和下面的对应就行 -
r.default.bs :是我们找到的签名函数
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=mt"); console.log("连接成功!"); demo.regAction("si", function (resolve,param) { resolve(r.default.bs(param)); })
进行一次请求后控制台会打印连接成功,同时我们的服务端也看到了新上线的客户端。
下面通过go接口尝试传入参数进行签名,成功拿到签名结果。
通过Mitmproxy可实时修改请求和响应内容,但我这次不用这个进行调用,这里使用yakit的热加载功能。
编写热加载代码,我们通过获取请求数据包的body内容进行签名之后,然后重新设置给请求头的si字段即可。具体代码如下:
jsrpcReq = func(origin /*string*/) { //JSrpc的group group = "mt"; //jsrpc的action action = "si";if (origin[0] == "{"){ rsp,rep = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group="+group+"&action="+action+"¶m="+json.dumps(origin), false),poc.appendHeader("content-type", "application/x-www-form-urlencoded"))~return json.loads(rsp.GetBody())["data"]; } else{ rsp,rep = poc.Post("http://127.0.0.1:12080/go",poc.replaceBody("group="+group+"&action="+action+"¶m="+codec.EncodeUrl(origin), false),poc.appendHeader("content-type", "application/x-www-form-urlencoded"))~return json.loads(rsp.GetBody())["data"]; }}// beforeRequest 允许在每次发送数据包前对请求做最后的处理beforeRequest = func(https, originReq, req) { // 获取请求体内容 body = poc.GetHTTPPacketBody(req) // 如果请求体不为空且长度大于0,则进行处理if body != nil && len(body) > 0 { // 将请求体内容传递给jsrpcReq函数 encryptedParam = jsrpcReq(string(body)) // 将结果添加到请求头中的"si"字段 req = poc.ReplaceHTTPPacketHeader(req, "si", encryptedParam) } // 返回修改后的请求return []byte(req)}
尝试调用热加载的代码进行请求,请求成功。
同时我们的数据包重放以及爆破功能也是可以用了
总结
-
在Web逆向工程(尤其是破解签名算法)的过程中,最耗时的环节往往是调试和定位关键签名函数,而非后期的代码复现或调用逻辑实现 -
至于JsRpc工具,多用几次就熟悉了,熟悉后也可以使用sekiro -
调试的时候直接通过浏览器开发者工具的 Network面板和 调用栈回溯快速定位签名触发点,比反混淆代码效率更高。使用 XHR/Fetch断点或全局 Hook拦截可大幅减少搜索范围
原文始发于微信公众号(pentest):JsRpc结合yakit热加载实现签名破解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论