aHR0cHM6Ly90ZGx6LmNjYi5jb20vIy9sb2dpbg 国密混合
先看报文:请求和响应都是全加密,这种情况就不像参数加密可以方便全文搜索定位加密代码,但因为前端必须解密响应的密文,因此万能的方法就是搜索拦截器,从第一行下断点分析,以找到加密的位置。
通常vue前端会使用axios配置拦截器,如下图,在搜索到的api.js的134、187行下断点,然后任意请求即可。
实际操作的时候断点建议打在第一个箭头函数和第二个箭头函数的第一行,避免因参数差异越过需要分析的逻辑。
做一下简单审计和判断,由于参数保存在config里,所以重点关注对config操作的代码,步入173行的函数进一步分析。
84、93行和96行看到加密方法了,分别是在post和get情况下对参数加密处理的逻辑。调试时由于是post方法,故步入84行函数调用。
步入该方法才实际调用加密函数本身,对应的猜测29行是解密函数,顺便下个断点。
该金融公司用的名为microAppSafety的js文件是个加解密库,且做了如上形式的混淆(本例涉及国密,该公司其它站点下使用类似文件名的js加解密库或均采用了国密)
混淆严重影响了源码的分析和阅读,但由于目的不是了解其加密逻辑的具体实现,到此该节就结束了,刚才解密的断点待服务器响应后也会停住,同理分析即可。
后记:该站点实际于测试环境进行,与生产环境的代码略有不同(源码形式不同,非代码实现不同),起初分析的时候由于种种原因都没有找到加密的函数,但在生产环境下找到,反推测试环境:找到的加密函数的关键字"encryptData"在测试环境中同文件里搜索并下断点,才得以成功完成测试环境的分析。
前端加解密通常涉及两个开源库:CryptoJS和JSEncrypt,两个库的代码分别形如:
//CryptoJS
var wordArray = CryptoJS.enc.Utf8.parse('ð¤¢');
var utf8 = CryptoJS.enc.Utf8.stringify(wordArray);
//JSEncrypt
var crypt = new JSEncrypt();
crypt.setKey(__YOUR_OPENSSL_PRIVATE_OR_PUBLIC_KEY__);
var text = 'test';
var enc = crypt.encrypt(text);
全局搜索加密的参数名,并跟踪从取值到请求过程值的变化(处理)
加密位置常见于:
var pwd = $('#pwd').val();
var pwd = encrypt(pwd);
2.2. 对全报文加密的请求设置XHR断点,查看调用栈
全局搜索保存签名的字段(通常包含sign)
parm=1&sign=md5(parm=1)
pwd=a&name=b&sign=md5(a|b)
基于RPC技术的自动化去加密测试。
本地替换js修改加密调用
前端拦截器里最后通过方法g()实现对参数处理的逻辑,但不是加密方法本身,其中实现了request method的判断和超时参数的添加(方法p)。
所以改造p方法来去掉加密的调用,使burp接收明文。
代码如下:
function p(e) {
return JSON.stringify({
data: Object(u["a"])({}, e),
dataExpireTime: 1689999999999
//使用一个较大的固定时间戳而非实时生成,以绕过服务器的时间戳超时校验机制
})
}
请求(POST)
GET方法和POST方法经过处理后的流量在burp里已经是明文。
调试过程将加密对象设置为全局对象,第二个打印的变量是加密密钥(源码硬编码值)。
Q&A:
为什么是d?
因为调试的当前页面里指向加密对象的变量是d;
为什么不定义p方法或是d.encryptData方法?
因为如果全局变量指向p方法会导致其运行加密方法时找不到某些定义在加解密库js文件中的方法而报undefined异常(破坏了作用域链的顺序);直接指向这个加密对象便于加解密时RPC使用同一个全局对象。
将RPC客户端加载到浏览器环境里,注入方法如油猴插件hook,本地覆盖到页面js和运行代码段等,建议在代码段里运行(实测注入到页面js中ws的通讯不稳定)
使用sekiro框架提供的服务端与客户端demo即可,并按实际情况修改通讯代码
//省略未变动的代码,将在参考链接中给出
var client = new SekiroClient("ws://127.0.0.1:5612/business-demo/register?group=rpc-test&clientId="+guid());
client.registerAction("enc",function(request, resolve, reject){
resolve(secApp.encryptData(request["params"],'04337449135FE6BD62D0683CE30AEA1BD178B879A392162D9F87A2FF0EC819A…'));
})
编写用于处理加密调用和密文流量转发的脚本mitm.py:
#rpc加密
def encrypt(params):
# print("request params in enc func:::{}".format(params))
api = "http://127.0.0.1:5612/business-demo/invoke?group=rpc-test&action=enc&jsonData={}".format(params)
res = requests.get(api).json()
# print("res:::{}:::{}".format(str(res),str(res['data'])) ) json()转换响应为json对象便于访问data字段
return json.dumps({'data':res['data'],'responseCode':res['responseCode'],'responseDesc':res['responseDesc']})
#修改请求
def request(flow):
#获取请求方法,返回字符串:POST GET
method = flow.request.method
if method == "GET":
#获取查询参数
# :MultiDictView[('{"data":{},"dataExpireTime":1689999999999}', '')]:::type is:::
# params = flow.request.query 目标对整个查询字串加密,query.get("param_name")针对参数加密的情况使用
params = flow.request.url.split("?", 1)[1]
elif method == "POST":
#获取请求body
params = flow.request.text
else:
params = None
# print("request params:::{}:::type is:::{}".format(params,type(params)))
encryptedData = encrypt(params)
print("encData:::{}".format(encryptedData) )
if method == "GET":
flow.request.url = flow.request.url.split("?",1)[0]+ "?" + encryptedData
# print("request url:::{}".format(flow.request.url))
elif method == "POST":
flow.request.text = encryptedData
Tips:这里可以利用burp的repeater模块进行脚本的测试,不必在浏览器里操作站点功能。
如果请求未通过,服务器将响应200以外的代码,此时拦截器进入reject部分,该部分的处理是没有解密过程的,响应包也可看到是明文形式的,因此编写中间脚本时需要判断返回码以决定是否进入RPC解密的调用。
一般情况使用GET方法便于浏览器API调用,但受限于URL长度,当对响应解密时,响应过长会导致GET方法无法正常请求,因此API调用改换POST方式。
#RPC解密
def decrypt(params):
api = "http://127.0.0.1:5612/business-demo/invoke"
jsonObj = {"group": "rpc-test",
"action": "dec",
"param": str(params)}
res = requests.post(api, json=jsonObj).json()
print("res:::{}:::".format(str(res['data'])) )
return json.dumps(res['data'])
#修改响应
def response(flow):
body = flow.response.content.decode('utf-8')
print("response body:::{}".format(body))
#判断响应是否为密文
if flow.response.status_code == 200:
decryptedBody = decrypt(body)
flow.response.text = decryptedBody
function f(e) {
return e
}
RPC client加上解密的action:
client.registerAction("dec", function(request, resolve, reject){
resolve(secApp.decryptDataOneWay(request["param"], 'DA7668FAx7'));
});
(图示上窗运行RPC server,下窗运行mitmdump)
(运行RPC client)
原本加密的响应数据已呈明文。
红色箭头发起第一个请求,RPC Server和RPC Clinet之间通过websocket通讯,mitmdump和RPC Server间通过http通讯(API),其余箭头都是代理流量转发的过程。
注: 可以联动自动化工具,把流量用脚本加密代理出去。
mitmproxy
https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Request
sekiroAPI:
https://sekiro.iinti.cn/sekiro-doc/01_user_manual/3.restful_api.html#get%E5%92%8Cpost
sekiro客户端:
https://sekiro.virjar.com/sekiro-doc/assets/sekiro_web_client.js
完整通讯代码:
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:5612/business-demo/register?group=rpc-test&clientId=" + guid());
// var secApp = new microAppSafety;
client.registerAction("enc", function (request, resolve, reject) {
resolve(secApp.encryptData(request["param"],enckey));
})
client.registerAction("dec", function (request, resolve, reject) {
resolve(secApp.decryptDataOneWay(request["param"],deckey));
})
//站点请求一次后在控制台中执行window.secApp = new microAppSafety;注册全局对象
续:脚本优化
import json
import requests
import urllib.parse
java_server = 'http://127.0.0.1:5612/business-demo/invoke'
# RPC
def rpc(action, params, group='rpc-test'):
url = java_server
data = {
'group': group,
'action': action,
'param': params
}
count = 0 # 计数器
while 1:
count += 1
if count > 10:
# TODO:处理超时逻辑
break
res = requests.post(url, data=data).json()
if 'data' in res:
return res
return ''
# 解密
def decrypt(params: str):
res = rpc('dec', params)
print("dec:::res:::{}:::".format(str(res)))
return json.dumps({'data': res['data'], 'responseCode': res['responseCode'], 'responseDesc': res['responseDesc']})
# 加密
def encrypt(params: str):
res = rpc('enc', params)
return res['data']
# 修改请求
def request(flow):
method = flow.request.method
if method == "GET":
flow.request.url = flow.request.url.split("?", 1)[0] + "?" + encrypt(urllib.parse.unquote(flow.request.url.split("?", 1)[1]))
print('GET')
elif method == "POST":
flow.request.text = encrypt(flow.request.text)
# 修改响应
def response(flow):
body = flow.response.content.decode('utf-8')
if flow.response.status_code == 200:
flow.response.text = decrypt(body)
埋坑(框架bug):
count = 0
while 1:
count += 1
res = requests.post(api, json=jsonObj).json()
print(count)
if 'data' in res:
break
古早文档,代码部分已全量改进,先不放了,没时间改了,马上要去看电影了