点个关注,谨防走丢~
作者简介
许理想,ID:Drea1v1,具备丰富的甲乙方安全工作经验,主要研究领域:代码审计、漏洞挖掘、红蓝对抗、数据安全等。
前言
本系列文章旨在记录并分享我在前端加密绕过方面的实战经验,系列文章分上下两篇,分别以两个真实案例讲述前端加密安全防护的过程以及绕过方式,同时分别对所使用到的工具做了简单介绍。
在上一篇文章中,我们以某网站为例,介绍了如何绕过前端加签的安全防护,传送门:前端加密攻防实战:深度解析与绕过技巧(上)。本篇文章将介绍如何绕过加签及加密双重防护的网站。
安全逻辑分析
先看看报⽂的组成,query参数只有⼀个sign(看到这个参数是不是有点熟悉,⼼⾥已经在笃定有签名校验了),请求体和返回体都是⼀串看不懂的字符串。请求header中有⼀个key为Encrypt的头,暂时不知道有什么⽤。
好了,带上以上找到的信息,开始分析。现在要找⼀个切⼊点,可以试⼀下全局搜索sign,发现有多达⼏百个结果。
逐个分析,发现⼀个可疑的地⽅,有出现data关键字以及sign,还有调⽤3DES进⾏加密,尝试在这⾥下断点,看看是否触发。
断点进来了,可以看到e是调⽤的3DES算法对r进⾏加密后的值,右边看到r是⼀个对象,有⼀个packages字段,其中包裹着header和request两个对象,request⼤概率是每个接⼝要传输的参数,可以看到登录输⼊的123323在request中有⼀个userName的key也是该值,所以e就是加密后的请求体。
加密的密钥是固定的,为M,是⼀个WordArray数组。
继续往下看,这⾥先把e的值记录起来,后⾯看看是不是就是真正的请求体。
下⾯将请求参数加密后的内容e和⼀个固定的值globalConfig.transfer作为hex_hmac_md5函数参数,⽣成参数n,将n的值作为参数调⽤匿名函数w.encrypt(t,"base64") ,重新⽣成n,最终将n拼接到url中的?sign= 后⾯
加密后的数据e赋值给了t.data:
ok,已经⼤致知道逻辑了,直接放开,查看新发起的报⽂,看⼀下请求体、sign是不是上⾯记录的e的值。从下⾯可以看到,能对应上,没找错地⽅。
绕过思路
从上⾯分析可以知道,报⽂加密使⽤的是3DES算法,不排除有魔改的情况,先本地使⽤js通⽤的算法库试,如果没区别,就免去扣取加密代码的步骤。直接让AI给我们⽣成加密、解密代码:
这⾥有个地⽅要改⼀下,AI给的代码key传的字符串,然后通过CryptoJS.enc.Utf8.parse⽣成⼀个WordArray对象。上⾯我们调试时,获取的就是WordArray对象,这⾥⼿⼯写⼀下代码初始化WordArray就⾏了(也可以将数组转回字符串,实际就是globalConfig.transfer)。整理⼀下,最终如下:
const CryptoJS = require("crypto-js");
//对称加密秘钥
var words = [1414545744,1346981460,1163019841,1128608304,825762624,556087609];
var sigBytes = 24; // 例如,只使用前6个字节(即前1.5个32位整数)
var wordArray = CryptoJS.lib.WordArray.create(words, sigBytes);
function tripleDesEncrypt(plain) {
encryptContext = CryptoJS.TripleDES.encrypt(JSON.stringify(plain),wordArray,{
iv: CryptoJS.enc.Utf8.parse("12345678"),
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString()
return encryptContext
}
function tripleDesDecrypt(encText) {
decryptContent = CryptoJS.TripleDES.decrypt(encText,wordArray,{
iv: CryptoJS.enc.Utf8.parse("12345678"),
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return decryptContent.toString(CryptoJS.enc.Utf8)
}
先⽤解密算法看看前端⽣成的密⽂和后端返回的密⽂是否能正常解密。稳!!。
加密算法解决了,还剩下sign,重头戏来了!将加密后的内容e和globalConfig.transfer传⼊hex_hmac_md5函数,⽣成n。
AI解释可能为⼀个基于HMAC-MD5算法⽣成消息认证码(MAC)。
同样先试⼀下使⽤通⽤算法⽣成,我就不截图了,最后验证正确。
//hex_hmac_md5的key,已打码
key = 'xxxxxxxxxxxx'
//参数为加密完的内容
function hex_hmac_md5(message) {
var hmac = CryptoJS.HmacMD5(message,key);
// 将结果转换为十六进制字符串
var hex = hmac.toString(CryptoJS.enc.Hex);
return hex;
}
⽣成的md5值后,继续继续跟进w.encrypt函数,调⽤this.$$encryptKey
继续跟进this.$$encryptKey。this.$getDataForEncrypt(e, r)的e为上⾯⽣成的md5,r为空,会⽣成对应字符串的ascii码数组。
This.keyPair.encrypt有点复杂就不看了,也不需要知道。
现在只要调⽤this.$$encryptKey就可以⽣成最终的sign值,但是从上⾯可以看到这个函数是⽐较复杂的,如果要把代码扣下来放到本地执⾏,要分析很多,并且需要补环境(函数执⾏所需的上下⽂)。通过时停⼤法,发现是可以直接调⽤函数的,那有没有办法可以直接获取当前运⾏时,直接执⾏函数呢?答案是有的,可通过JSRPC实现。
自动化工具JSRPC
JSRPC利⽤RPC(远程过程调⽤)协议实现JavaScript函数的远程调⽤功能。其⼯作原理是在客户端(即浏览器端)中注⼊RPC环境,使客户端与JSRPC服务器之间建⽴起WebSocket连接,以实现双向通信。
此外,⽤户可在客户端注册所需的加解密函数。当RPC服务器向客户端发送请求时,客户端会接收并执⾏相应的函数⽅法,随后将处理结果反馈回RPC服务器。服务器在接收到结果后,会通过接⼝的⽅式返回给⽤户。
下载地址:https://github.com/jxhczhl/JsRpc
计划利用工具可以实现的效果:
1、在bp中抓到的请求报⽂、返回报⽂都⾃动解密。
2、可直接在bp上输⼊明⽂的报⽂进⾏重放,插件根据内容⾃动⽣成加密请求体、sign。
3、不能影响原有浏览器上功能的使⽤。
实现思路:原来浏览器请求是转发到bp的,如需要对报⽂内容进⾏解密需要在浏览器和bp之间加⼀层mitmproxydownstream代理,通过execjs模块调⽤本地js函数对报⽂先进⾏解密,解密后再给到bp。在bp修改明⽂报⽂重放后,如直接转发到应⽤服务器,因报⽂为明⽂服务器⽆法识别,所以需要在bp与服务器之间再加⼀层mitmproxyupstream代理,⽤于将明⽂的请求报⽂再进⾏加密后转发。当服务器收到请求后,返回加密报⽂会先给到mitmproxy-upstream,需要代理对其报⽂再进⾏解密后给到bp。bp拿到返回报⽂后,为保证前端功能正常,将内容返回给浏览器时,需要再通过mitmproxy-downstream对返回内容进⾏加密。
说干就干,通过以下7个步骤完成上面的整体流程。
步骤⼀:开启JSRPC服务,监听端⼝和地址可以通过同⽬录下的config.yaml
步骤⼆:注⼊JS,构建通信环境。
项⽬提供代码JsEnv_Dev.js,复制后在浏览器控制台回⻋运⾏。
步骤三:注册js⽅法
注册前,需要将想要调⽤的函数赋值给全局变量(window), window.sign1=this.keyPair和window.sign2 = this.$getDataForEncrypt ⽅便调⽤。
注:赋值给全局变量时,需进⼊到调试模式,不然⽆法获取函数执⾏的上下⽂。
步骤四:在控制台执⾏远程调⽤代码
//连接rpc服务器websocket,group名字可随便起
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=demo1");
// 注册一个方法sign,param为需要传入的参数,这里为hex_hmac_md5生成的md5值
// 第二个参数为函数,resolve里面的值是想要的值(发送到服务器的)
demo.regAction("sign", function (resolve,param) {
console.log(param)
var unitArray = window.sign2(param);
console.log(unitArray)
var unitArray1 = window.sign1.encrypt(unitArray,false);
console.log(unitArray1)
resolve(unitArray1.toString("base64"));
})
步骤五:注册完js函数后,通过访问接⼝http://localhost:12080/go?group=demo1&action=sign¶m=123456 调⽤函数,返回内容中data为函数最终⽣成结果。
步骤六:现在已经能⽣成sign和加解密了,那可以开始写插件
分为两部分,⼀部分是下游代理服务downstream,⼀个是上游代理服务upstream。下游服务器主要是将浏览器到bp之间的报⽂进⾏解密,将bp返回到浏览器的返回报⽂进⾏加密。上游服务器将bp到服务器的请求报⽂进⾏加密和加签,服务器返回报⽂进⾏解密。
下游代理服务downstream.py
from mitmproxy import ctx
from mitmproxy import http
import execjs
import json
#加载js脚本
jsCode = open('xxx.js','r',encoding='utf-8').read()
class Downstream():
#解密浏览器到bp的请求报文
def request(self,flow: http.HTTPFlow):
if 'xxx.xxx.com' in flow.request.pretty_url and flow.request.method == 'POST':
#获取请求体
ctx.log.info("浏览器请求加密数据 => "+flow.request.get_text())
#解密请求体
ctx.log.info("浏览器请求明文数据 => "+self.decrypt(flow.request.get_text()))
#将解密后的内容重新给到请求体
flow.request.set_text((self.decrypt(flow.request.get_text())))
#加密bp到浏览器的返回报文
def response(self,flow: http.HTTPFlow):
if 'xxx.xxx.com' in flow.request.pretty_url and flow.request.method == 'POST':
ctx.log.info("响应报文加密前数据 => "+flow.response.get_text())
#加密返回报文
resp = self.encrypt(json.loads(flow.response.get_text()))
ctx.log.info("响应报文加密数据 => "+resp)
#重新将加密报文赋值给返回体
flow.response.set_text(resp)
def encrypt(self,plain):
enc = execjs.compile(jsCode).call('tripleDesEncrypt',plain)
return enc
def decrypt(self,enc):
plain = execjs.compile(jsCode).call('tripleDesDecrypt',enc)
return plain
addons = [Downstream()]
上游代理服务器upstream.py:
import requests,execjs
from mitmproxy import ctx
from mitmproxy import http
import json
#加载js脚本
jsCode = open('xxx.js','r',encoding='utf-8').read()
#jsrpc接口地址
url = "http://127.0.0.1:12080/go?group=demo1&action=sign¶m="
class Upstream:
#加密bp到服务器的请求,并生成新的签名
def request(self,flow: http.HTTPFlow):
if 'xxx.xxx.com' in flow.request.pretty_url and flow.request.method == 'POST':
#req = json.loads(flow.request.get_text())
#加密请求报文
req = self.encrypt(json.loads(flow.request.get_text()))
ctx.log.info("浏览器请求加密数据 => "+ req)
#对新生成的报文进行签名
sign = self.getSign(req)
ctx.log.info("浏览器请求sign => "+ sign)
#将新生成的签名重新赋值给sign
flow.request.query['sign'] = sign
#将加密后的请求报文重新赋值给请求体
flow.request.set_text(req)
#将服务器到bp的返回报文解密
def response(self,flow: http.HTTPFlow):
if 'xxx.xxx.com' in flow.request.pretty_url and flow.request.method == 'POST':
ctx.log.info("响应报文加密数据 => "+flow.response.get_text())
ctx.log.info("响应报文解密数据 => "+self.decrypt(flow.response.get_text()))
#将解密后的返回报文重新赋值到返回
flow.response.set_text((self.decrypt(flow.response.get_text())))
def encrypt(self,plain):
enc = execjs.compile(jsCode).call('tripleDesEncrypt',plain)
return enc
def decrypt(self,enc):
plain = execjs.compile(jsCode).call('tripleDesDecrypt',enc)
return plain
def getSign(self,enc):
#生成md5
md5 = execjs.compile(jsCode).call('hex_hmac_md5',enc)
#调用jsrpc接口
resp = requests.get(url + md5)
#解析jsrpc返回的内容,获取data
jsonStr = json.loads(resp.text)
return jsonStr['data']
addons = [Upstream()]
步骤七、运⾏上下游代理
下游服务器启动时需要增加--mode 参数,表示将流量转发到http://127.0.0.1:8080,bp监听的地址根据实际情况配置。--ssl-insecure 忽略ssl证书验证。
mitmdump --listen-port 9999 -s downstream.py --mode upstream:http://127.0.0.1:8080 --
ssl-insecure
上游服务器启动
mitmdump --listen-port 6666 -s upstream.py
bp设置上游服务器:
浏览器代理配置,这⾥需要设置为下游代理的监听地址:127.0.0.1:9999
效果展示,请求和返回报⽂都是明⽂,直接修改内容⾃动加密解密,同时浏览器功能使⽤正常:
总结
本系列文章以两个案例讲解前端加密的过程以及绕过方式,同时分别对所使用到的工具做了简单介绍。mitmproxy功能⾮常强⼤,本⽂也只是介绍了其能⼒的冰⼭⼀⻆。除了可以与jsrpc联动外,还可以和frida联动,实现app的加解密。
⽂章写得⽐较匆忙,有错的地⽅欢迎联系交流。
声明:本文仅供学习参考使用,任何人不得将其用于非法用途,否则后果自行承担,与本公众号无关。
原文始发于微信公众号(安全有术):前端加密攻防实战:深度解析与绕过技巧(下)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论