0x01 前言
在渗透测试中,前端加密的请求数据常常增加分析难度,传统的手动逆向加解密效率低下且耗时。JsRpc通过WebSocket与客户端通信,结合Burp Suite的插件(如Galaxy)或代理工具(如mitmproxy),可实现请求与响应的自动加解密,极大提升测试效率。本文整合两篇教程,详细解析JsRpc与Burp Suite联动的实现原理与步骤,涵盖环境搭建、函数注册、脚本配置及实战效果展示,帮助安全从业者快速掌握自动化加解密技术。
练手地址:
https://github.com/0ctDay/encrypt-decrypt-vuls
JsRpc:https://github.com/jxhczhl/JsRpc
参考文章:https://xz.aliyun.com/news/16886
https://xz.aliyun.com/news/14689
现在只对常读和星标的公众号才展示大图推送,建议大家把渗透安全HackTwo“设为星标”,否则可能就看不到了啦!
末尾可领取挖洞资料文件 #渗透安全HackTwo
0x02 漏洞详情
JsRpc基本使用
首先观察一下原始的数据包
可以看到除了请求体需要解密,还有请求头中的timestamp、requestId、sign
需要实时更新,所以请求包中总共有四个地方需要处理,由于是直接只用JsRpc,那么就只需要找到对应的处理位置然后调用即可,不需要弄清楚具体的处理逻辑
可以看到就是图中的几个地方处理的,具体逻辑就不分析了,主要是学会使用jsrpc
首先把这几个函数先提升到全局,需要先断点让其加载到作用域
然后在控制台执行下面语句提升至全局作用域
window.requestId=p
//requestId
window.v1 = v
//函数v
window.sign=a.a.MD5
//签名sign
window.l=l
window.d=d
然后注入jsrpc中的js文件JsEnv_Dev.js
,注意注js的时候断点就得停止了
然后启动jsrpc服务端
再客户端连接
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz");
连接成功后服务端会提示新上线….然后就是将函数注册进去,即我们要怎么处理值
demo.regAction("encode",function (resolve,param) {
n=JSON.stringify(v1(param))
var request = l(n)
var time = Date.parse(new Date);
var id=requestId()
var sg = sign(n+id+time).toString()
var data={"time":"","id":"","sign":"","request":""}
data["time"]=time.toString()
data["id"]=id
data["sign"]=sg
data["request"] = request
resolve(data);
})
上面是加密,下面是解密
demo.regAction("decode", function (resolve,param) {
//这样添加了一个param参数,http接口带上它,这里就能获得
var response = d(param)
resolve(response);
})
此时就可以测试一下jsrpc处理是否能成功
http://127.0.0.1:12080/go?group=zzz&action=encode¶m={"password":"123123","username":"asd","validCode":"ycpa"}
可以看到加密是没有任何问题的,zzz
就是加入的组和你一开始连接的时候相同即可,action
即你注册的加密函数,param
就是你要处理的参数,看看解密
也是能够正常处理的
JsRpc联动burp
联动burp需要借助mitmproxy写脚本,或者借助其它一些burp插件也行,这里就用mitmproxy了,脚本借助gpt弄几下即可
import requests
import json
from mitmproxy import ctx
def get_rpc(param):
url = "http://127.0.0.1:12080/go"
params = {
"group": "zzz",
"action": "encode",
"param": param.decode('utf-8', errors='ignore')
}
try:
result = requests.get(url, params=params, timeout=5)
# ctx.log.info(f"RPC Response: {result.text}")
# 第一次解析 JSON,获取外层结构
outer_data = result.json()
# 第二次解析 data 字段(字符串 -> 字典)
inner_data = json.loads(outer_data['data'])
return inner_data # 直接返回解析后的内层数据
except Exception as e:
ctx.log.error(f"Error in get_rpc: {str(e)}")
return None
def get_responseRpc(param):
url = "http://127.0.0.1:12080/go"
params = {
"group": "zzz",
"action": "decode",
"param": param.decode('utf-8', errors='ignore')
}
try:
result = requests.get(url, params=params, timeout=5)
outer_data = result.json()
# 第二次解析 data 字段(字符串 -> 字典)
inner_data = json.loads(outer_data['data'])
return inner_data # 直接返回解析后的内层数据
except Exception as e:
ctx.log.error(f"Error in get_rpc: {str(e)}")
return None
def request(flow):
original_body = flow.request.content
ctx.log.info(f"原始请求体: {original_body}")
result = get_rpc(original_body)
if result is None:
ctx.log.error("RPC failed, skipping request modification")
return
try:
# 直接使用内层数据
flow.request.headers['timestamp'] = result['time']
flow.request.headers['requestId'] = result['id']
flow.request.headers['sign'] = result['sign']
flow.request.content = result['request'].encode('utf-8')
flow.request.headers['Content-Length'] = str(len(flow.request.content))
ctx.log.info(f"加密后的请求体: {flow.request.content}")
except Exception as e:
ctx.log.error(f"Error in request: {str(e)}")
def response(flow):
response_body = flow.response.content
ctx.log.info(f"响应体: {response_body}")
result = get_responseRpc(response_body)
if result is None:
ctx.log.error("Response RPC failed, keeping original response")
return
try:
# 将解码后的字典转为 JSON 字符串并编码为字节
decoded_json = json.dumps(result, ensure_ascii=False).encode('utf-8')
flow.response.content = decoded_json
flow.response.headers['Content-Length'] = str(len(flow.response.content))
# 可选:设置 Content-Type 为 JSON
flow.response.headers['Content-Type'] = 'application/json; charset=utf-8'
ctx.log.info(f"解码后的响应体: {flow.response.content}")
except Exception as e:
ctx.log.error(f"Error in response: {str(e)}")
然后设置burp的上游代理
运行脚本
mitmdump -p 8081 -s .JsPython.py
这样即可实现自动化加解密
当密码不正确时
当密码正确时,这里请求头中的三个值在burp虽然没变但实际上是已经改变了的,因为是在脚本中做的处理
还是非常方便滴,因为不需要去逆js了,接下来是
JsRpc+Galaxy工具实现网站HTTP报文自动加解密+自动更新签名
Galaxy工具
可以做到在请求/响应在客户端/Burp/服务端流转时加入自己的处理逻辑,这可以用来实现请求/响应自动解密。
详情见https://github.com/outlaws-bai/Galaxy
JsRpc工具
在网站的控制台新建一个WebScoket客户端链接到服务器通信,调用服务器的接口 > 服务器会发送信息给客户端 > 客户端接收到要执行的方法执行完js代码后把获得想要的内容发回给服务器 > 服务器接收到后再显示出来。
详情见https://github.com/jxhczhl/JsRpc
环境靶场:
http://39.98.108.20:8085/
项目地址:
https://github.com/0ctDay/encrypt-decrypt-vuls/
JS逆向分析
网站数据包如下,数据进行加密处理,请求包的请求头内三个参数requestId、timestamp、sign缺一不可,改动任何一个都失效,且无法使用repeater重放数据
浏览器控制台定位相关js代码,其中关于请求头三个参数如下:
加密js代码如下,采用AES CBC key和iv都写在js内
JS内打上断点
可以看到其中n为需要传输的数据、timestamp通过Date.parse(new Date)获取、requestId通过函数p()获取、sign为a.a.MD5函数对传输的数据n加上timestamp加上requestId做MD5处理。l(n)为加密函数对传输的数据n进行加密。
控制台调用函数打印相关数据
注入JsRpc客户端及获取签名方法
在这里使用jsrpc调用接口执行js获取timestamp、requestId、sign三个请求头数据,而加密采用Galaxy自带加密模板。
jsrpc连接成功后,在浏览器控制台对相关函数进行注册
window.requestId=p
//requestId
window.v1 = v
//函数v
window.sign=a.a.MD5
//签名sign
连接通信
demo.regAction("hello",function (resolve,param) {
n=JSON.stringify(v1(param))
var time = Date.parse(new Date);
var id=requestId()
var sg = sign(n+id+time).toString()
var data={"time":"","id":"","sign":""}
data["time"]=time.toString()
data["id"]=id
data["sign"]=sg
resolve(data);
})
这里可以直接通过浏览器访问查看对应结果
在Galaxy的hook脚本中调用JsRpc服务端
加解密
此处加密采用模板加解密python文件AES_CBC来实现
脚本内写入key、iv和对应算法
该网站内数据包采用GET和POST两种方式进行请求,且只有POST请求的数据进行加密处理,因此请求数据发送到burp解密和burp明文数据到服务器加密需要区分开
JsRpc引入
引入jsrpc需要jython引入http客户端
from org.m2sec.core.outer import HttpClient
按照格式构建引入jsrpc和替换请求头的代码
burpRequestBody=request.getBody() #获取传输数据,也就是上文中的n
url="http://127.0.0.1:12080/go?group=zzz&action=hello¶m="
jsrpcUrl=url+burpRequestBody #将n拼接url
jsrpcRequest=request.of(jsrpcUrl) #构建request请求
jsrpcRespone=HttpClient.send(jsrpcRequest) #发送数据包
jsrpcResponeJson=jsrpcRespone.getJson() #获取返回包内数据
headData=jsrpcResponeJson["data"] #提取data中内容
headData=json.loads(headData) #将JSON数据的字符串转换为Python中的数据类型
time=headData["time"] #提取时间
requestId=headData["id"] #提取requestId
sign=headData["sign"] #提取签名
head=request.getHeaders() #提取原始数据包请求头
head.put("sign",sign) #替换sign
head.put("requestId",requestId) #替换requestId
head.put("timestamp",time) #替换时间
request.setHeaders(head) #替换请求头
将构建好的代码写入到burp发送到服务器的函数zhong
效果图
自动加解密实现效果
重放数据实现效果
Galaxy完整的hook脚本
import json
import base64
from org.m2sec.core.utils import (
CodeUtil,
CryptoUtil,
HashUtil,
JsonUtil,
MacUtil,
FactorUtil,
)
from org.m2sec.core.models import Request, Response
from org.m2sec.core.outer import HttpClient
from java.lang import String
"""
内置示例,需要自定义代码文件时查看该文档:https://github.com/outlaws-bai/Galaxy/blob/main/docs/Custom.md
按 Ctrl(control) + ` 可查看内置函数
"""
ALGORITHM = "AES/CBC/PKCS5Padding"
secret = b"1234567891234567"
iv = b"1234567891234567"
paramMap = {"iv": iv}
jsonKey = "data"
log = None
def hook_request_to_burp(request):
"""HTTP请求从客户端到达Burp时被调用。在此处完成请求解密的代码就可以在Burp中看到明文的请求报文。
Args:
request (Request): 请求对象
Returns:
Request: 经过处理后的request对象,返回null代表从当前节点开始流量不再需要处理
"""
if(request.getMethod()=="GET"):
return request
else:
# 获取需要解密的数据
encryptedData = CodeUtil.b64decode(request.getBody())
# 调用内置函数解密
data = decrypt(encryptedData)
# 更新body为已加密的数据
request.setContent(data)
return request
def hook_request_to_server(request):
"""HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。
Args:
request (Request): 请求对象
Returns:
Request: 经过处理后的request对象,返回null代表从当前节点开始流量不再需要处理
"""
if(request.getMethod()=="GET"):
burpRequestBody=''
url="http://127.0.0.1:12080/go?group=zzz&action=hello¶m="
jsrpcUrl=url+burpRequestBody
jsrpcRequest=request.of(jsrpcUrl)
jsrpcRespone=HttpClient.send(jsrpcRequest)
jsrpcResponeJson=jsrpcRespone.getJson()
headData=jsrpcResponeJson["data"]
headData=json.loads(headData)
time=headData["time"]
requestId=headData["id"]
sign=headData["sign"]
head=request.getHeaders()
head.put("sign",sign)
head.put("requestId",requestId)
head.put("timestamp",time)
request.setHeaders(head)
return request
# 获取被解密的数据
else:
data = request.getContent()
burpRequestBody=request.getBody()
url="http://127.0.0.1:12080/go?group=zzz&action=hello¶m="
jsrpcUrl=url+burpRequestBody
jsrpcRequest=request.of(jsrpcUrl)
jsrpcRespone=HttpClient.send(jsrpcRequest)
jsrpcResponeJson=jsrpcRespone.getJson()
headData=jsrpcResponeJson["data"]
headData=json.loads(headData)
time=headData["time"]
requestId=headData["id"]
sign=headData["sign"]
head=request.getHeaders()
head.put("sign",sign)
head.put("requestId",requestId)
head.put("timestamp",time)
request.setHeaders(head)
# 调用内置函数加密回去
encryptedData = encrypt(data)
# 将已加密的数据转换为Server可识别的格式
body = CodeUtil.b64encode(encryptedData)
# 更新body
request.setContent(body)
log.info("header2: {}",request)
return request
def hook_response_to_burp(response):
"""HTTP请求从Server到达Burp时被调用。在此处完成响应解密的代码就可以在Burp中看到明文的响应报文。
Args:
response (Response): 响应对象
Returns:
Response: 经过处理后的response对象,返回null代表从当前节点开始流量不再需要处理
"""
# 获取需要解密的数据
encryptedData = CodeUtil.b64decode(response.getBody())
# 调用内置函数解密
data = decrypt(encryptedData)
# 更新body
response.setContent(data)
return response
def hook_response_to_client(response):
"""HTTP请求从Burp将要发送到Client时被调用。在此处完成响应加密的代码就可以将加密后的响应报文返回给Client。
Args:
response (Response): 响应对象
Returns:
Response: 经过处理后的response对象,返回null代表从当前节点开始流量不再需要处理
"""
# 获取被解密的数据
data = response.getContent()
# 调用内置函数加密回去
encryptedData = encrypt(data)
# 更新body
# 将已加密的数据转换为Server识别的格式
body = CodeUtil.b64encode(encryptedData)
# 更新body
response.setContent(body)
return response
def decrypt(content):
return CryptoUtil.aesDecrypt(ALGORITHM, content, secret, paramMap)
def encrypt(content):
return CryptoUtil.aesEncrypt(ALGORITHM, content, secret, paramMap)
def set_log(log1):
"""程序在最开始会自动调用该函数,在上方函数可以放心使用log对象"""
global log
log = log1
0x03 总结
原文始发于微信公众号(渗透安全HackTwo):JsRpc与Burp Suite联动 自动化加解密的完整实战指南(详细版)|挖洞技巧
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论