记某次加解密测试
起因
测试时遇到了一个流量加密的系统,而加解密这块弄的比较少,所以记录下这次加解密的流程,该系统的加密流量如下:
JsRpc+Galaxy
之前逛github的时候就看到过Galaxy这个插件,但一直没用过,就想着用着试试,同时找到了下面这篇参考文章:
JsRpc+Galaxy 实现网站HTTP报文自动加解密+自动更新签名
https://xz.aliyun.com/t/15252?time__1311=GqjxnD0GitMDlhzG7DyDI2DfxuxRiQOioD
简单说就是Galaxy实现了4个hook,分别是请求从浏览器到burp时解密,burp再将请求发送给服务端时加密,服务端返回到burp时解密,burp再将返回发送到浏览器时加密,详细的使用方式见如下地址:
https://github.com/outlaws-bai/Galaxy/wiki/%E5%8A%9F%E8%83%BD%E8%AF%A6%E8%A7%A3
而这4个加解密需要我们自己去实现,这里我准备使用的是http的方式,可以使用任何语言实现这4个加解密http服务端,然后加解密时Galaxy会与这个http服务端进行通信,作者也有python实现好的一些加解密服务端可以直接使用,也可以根据实际情况修改再用:
https://github.com/outlaws-bai/GalaxyHttpHooker
JsRpc简单说就是将浏览器和本地服务端建立一个WebScoket连接,然后就可以直接在本地执行浏览器中加载的js函数,比如加解密函数,所以配合Galaxy可以达到一些意想不到的效果,详细使用方式见如下地址:
https://github.com/jxhczhl/JsRpc
加解密实现
首先需要定位到加解密的js,首先是加密流程,简单看下,采用了三段式加密SM3+SM4+SM2:
解密流程则是传入服务端返回的密文和key,对密文进行截取后进行sm4解密:
更详细的流程其实可以不用分析,因为这些函数都可以使用jsrpc去调用,然后就是尝试在浏览器控制台连接jsrpc本地启动的服务端,我在物理机上测试了一下是可以连接上的:
但由于目标系统需要使用ie加载一些控件,所以就用windows虚拟机测试了一下,发现连不上了,从报错信息来看是需要wss,后面配置了一下还是不太行就没深入研究了:
于是想了另外一个方法,把加解密的js复制下来,在本地浏览器控制台运行下,然后再连接jsrpc就行了,如下在本地控制台运行js后可直接调用加解密函数:
但这样还有一个问题,虽然可以调用加解密函数了,但key是由客户端随机生成的,这里无法和jsrpc建立连接就获取不到,然后就想到可以将传入key的那个js截断,然后修改成固定的key,如下每次客户端加密数据时的key都将会是111...,同时只要不关闭页面这个js都会一直生效:
现在加解密函数能调用,key固定,就可以开始实现Galaxy的4个加解密hook了,首先是客户端到burp的解密,返回继续看下客户端加密流程,三部分,第一部分传入明文data和key,然后将data转换成byte,M1将其位数填充到16的整数倍后赋值给m1,m2为M2随机生成一段16位的byte,然后M3将m1和m2进行异或后再将m2拼接在最后赋值给m,最后使用sm4加密m,第二部分为sm2加密key用于服务端解密,第三部分为sm3加密明文、key和盐用于验签,然后将三部分拼接起来发送给服务端:
很明显这里我们要获取到明文只需要用到第一部分就行了,对应的解密流程如下,在浏览器控制台注册对应解密函数如下:
demo.regAction("test1", function (resolve,param) {
//传入密文中sm4加密部分,进行sm4解密
var m = _Sm4.crypt(Code.hexStr2bytes(param),Code.str2bytes("11111111111111111111111111111111"),0);
//截取最后16位
var m2 = m.slice(Math.max(m.length - 16, 0));
//截取m最后16位前和m2进行异或得到明文byte
var m1 = M3(m.slice(0,m.length - 16),m2);
//这里由于编码问题,返回16进制字符串
resolve(Code.bytes2hexStr(m1));
})
可直接在浏览器访问url传入密文进行测试,获取16进制字符串转换成字符后即可获取明文:
galaxy对应的hook如下:
async def hookRequestToBurp(request: RequestModel):
"""HTTP请求从客户端到达Burp时被调用。在此处完成请求解密的代码就可以在Burp中看到明文的请求报文。"""
method = request.get_method()
#仅处理post请求
if method == 'POST':
#获取原始密文
cryptedData = request.get_content().decode('utf-8')
#仅处理#10开头请求
if cryptedData[:3] == '#10':
#截取原始密文中sm4密文部分
start = cryptedData.find('u001d') + len('u001d')
end = cryptedData.find('u001d', start)
body = cryptedData[start:end]
#传入sm4密文到jsrpc进行解密
jsrpcUrl = "http://127.0.0.1:12080/go?group=zzz&action=test1¶m=" + body
res = requests.get(jsrpcUrl)
#获取16进制明文
data = res.json()["data"]
#删除加密时M1填充的乱码
last_227d_index = data.rfind('227D')
#十六进制转换成byte传入请求体,在burp中显示明文
bytes_obj = bytes.fromhex(data[:last_227d_index + 4])
request.set_content(bytes_obj)
return request
else:
return request
else:
return request
实现的效果如下,当请求从浏览器到burp时调用上面的解密hook进行解密,然后会增加一个X-Galaxy-Http-Hook: HookedRequest请求头:
然后就是加密,当我们修改了明文数据时,需要进行重新加密再发送给服务端,加密就很简单了,直接调用加密函数传入明文和key即可,如下在浏览器控制台注册对应解密函数:
demo.regAction("test2", function (resolve,param) {
//加密
var enbody = me.encrypt(param, "11111111111111111111111111111111")
resolve(enbody);
})
同样的可直接通过url调用进行加密:
galaxy对应hook如下:
async def hookRequestToServer(request: RequestModel):
"""HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。"""
#获取明文
decryptedData = request.get_content().decode('utf-8')
#传入明文到jsrpc进行加密
jsrpcUrl = "http://127.0.0.1:12080/go?group=zzz&action=test2¶m=" + decryptedData
res = requests.get(jsrpcUrl)
#获取密文
data = res.json()["data"]
#设置密文为请求体发送给服务端
request.set_content(data.encode("utf-8"))
return request
async def hookRequestToServer(request: RequestModel):
"""HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。"""
#获取明文
decryptedData = request.get_content().decode('utf-8')
#传入明文到jsrpc进行加密
jsrpcUrl = "http://127.0.0.1:12080/go?group=zzz&action=test2¶m=" + decryptedData
res = requests.get(jsrpcUrl)
#获取密文
data = res.json()["data"]
#设置密文为请求体发送给服务端
request.set_content(data.encode("utf-8"))
return request
实现效果如下,可通过右键进行加密测试功能是否正常,正常情况下有X-Galaxy-Http-Hook: HookedRequest请求头的请求包会自动进行加密:
然后是服务端返回的信息进行解密,也很简单直接调用解密函数,如下在浏览器控制台注册对应解密函数:
demo.regAction("test3", function (resolve,param) {
//这样添加了一个param参数,http接口带上它,这里就能获得
//解密
var debody = me.decrypt(param, "11111111111111111111111111111111")
resolve(debody);
})
同样的可直接通过url调用进行解密:
galaxy对应hook如下:
async def hookResponseToBurp(response: ResponseModel):
"""HTTP响应从Server到达Burp时被调用。在此处完成响应解密的代码就可以在Burp中看到明文的响应报文。"""
#获取服务端返回密文
encryptedData = response.get_content()
#传入密文到jsrpc进行解密
url = "http://127.0.0.1:12080/go?group=zzz&action=test3¶m="
jsrpcUrl = url + encryptedData.decode('utf-8')
res = requests.get(jsrpcUrl)
#获取明文
data = res.json()["data"]
#将明文和密文同时传入返回体,在burp中显示明文和密文
response.set_content(data.encode("utf-8") + "nenbody=".encode("utf-8") + encryptedData)
return response
实现的效果如下:
可以发现上面返回包中除了明文还有原本的密文,是为了方便实现最后一个加密hook,由于目标系统基本没有需要修改返回包的情况,所以为了方便就不需要重新加密了,直接返回原本的密文即可,可根据实际情况确定是否需要实现加密:
async def hookResponseToClient(response: ResponseModel):
"""HTTP响应从Burp将要发送到Client时被调用。在此处完成响应加密的代码就可以将加密后的响应报文返回给Client。"""
#获取返回体
decryptedData = response.get_content().decode('utf-8')
#获取返回体中的密文
endata = re.search(r'(?<=enbody=).+', decryptedData).group(0)
#设置返回体为原本的密文,发送给客户端
response.set_content(endata.encode("utf-8"))
return response
实现的效果如下:
最后访问系统,burp中的明文流量如下,后面就可以开始进行测试了:
监制丨船长、铁子
策划丨Cupid
美工丨molin
原文始发于微信公众号(千寻安服):记某次加解密测试
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论