前端加密对抗-mitmproxy
分析前端加密的意义?
正常来说,我们在做测试的时候,一般都是明文数据,并且服务端不会对传入的参数进行检测。这个时候的测试就是一番风顺。
- 如下图,请求参数会带一个sign=xx,后端会对这个sign进行校验,此处的校验规则是:将请求参数进行拆分,并且按照ascii顺序排序之后进行md5(query + “字符常量”)的加密方式进行校验
- 如下图,请求内容和响应内容都是加密的
遇到这些复杂的情况时,我们一般很难用burp继续去测试,因为需要对参数重新进行加密算法的操作,人工操作的话肯定是不现实的。
基本操作
找断点——通过搜索敏感关键词
通过请求参数发现关键词sign=
通过devtools进行sources
的搜索
找到sign
加密的地方并且打断点,再次触发请求可以进行debug
找断点——通过网络请求的调用栈
有时候关键词搜索出来的结果过于多,可以通过此方式查找会更精确
找一个看起来更容易定位的函数,这里的函数其实可以随便找,因为在调用栈中,可以回溯/跟进每个函数。
断点发现有一个关键词参数encryptedData
跟踪加密的方法是s=Object(ht.b)(o, _dyn$.t(622))
,可以确认加密函数是s=Object(ht.b)
, 所以可以在上图3245行进行断点跟踪,找到加密方法
mitmdump的基本使用(addons编写)
- 简单的demo
1 2 3 4 5 6 7 8 9 10 11
| class AutoDecoderClass(object):
def request(self, flow: http.HTTPFlow): pass
def response(self, flow: http.HTTPFlow): pass
addons = [ AutoDecoderClass() ]
|
- 给mitmdump运行, 加了一些参数,仅供参考
设置burp代理
如果勾选了Do DNS lookups over SOCKs Proxy
, 脚本能获取的url信息是包含域名的,如果不勾选的话,获取到的url信息是IP的,因为经过了一层解析。
手写加密算法
如下图所示的加密校验(此处我修改了3c为2c,为了是将报错作为参考系)
可以看出sign=xxx是整个数据包发送到后端之后提供给后端校验的数据
通过debug查看加密算法
signData
为请求GET请求参数ethod=edit&roleid=5985&operatorid=13223232324&_=1675850145395
getQuery
方法(这部分不用细看,想手动还原算法分也行,不过此处的getQuery运行一下就可以得到结果从而可以判断出算法是啥样的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function getQuery(_0x25e05e) { var _0x59335d = { 'tMUuu': function(_0x5c4842, _0x4a44ef) { return _0x5c4842(_0x4a44ef); }, 'FukXq': function(_0x68f762, _0x5c9bb) { return _0x68f762(_0x5c9bb); }, 'IAwqi': function(_0x4c0bd0, _0x4ae967, _0x16eeab) { return _0x4c0bd0(_0x4ae967, _0x16eeab); }, 'RYViz': _0xfa96('b9', 'aosW') }; var _0x4c50e2 = []; var _0x8ddc6f = _0x25e05e['split']('&'); for (var _0xe55c87 = 0x0; _0xe55c87 < _0x8ddc6f['length']; _0xe55c87++) { if (_0x8ddc6f[_0xe55c87]['split']('=')[0x0]) { _0x4c50e2[_0xfa96('ba', 'b#9z')]({ 'name': _0x8ddc6f[_0xe55c87]['split']('=')[0x0], 'value': _0x59335d[_0xfa96('bb', 'SKqK')](decodeURIComponentSafely, _0x59335d['FukXq'](decodeURIComponentSafely, _0x59335d[_0xfa96('bc', '4q@0')](getCaption, _0x8ddc6f[_0xe55c87], _0x8ddc6f[_0xe55c87][_0xfa96('bd', 'oc$h')]('=')[0x1]))) }); } } return _0x4c50e2[_0xfa96('be', '%866')](_0x59335d['FukXq'](createComprisonFunction, _0x59335d['RYViz'])); }
|
从运行结果看得出,是将所有的参数分隔之后将key和value都取出来以{name: X, value: Y}
的方式存入数组并返回,同时也可以看出此处的排序规则为ASCII从小到大排序
getSign
签名算法, 和上面的函数一样,可以手动还原 但是可能没啥必要。还原算法的时候可以配合debug的变量作用域来编写代码。比如_0xfa96('13d', '3nwA')
其实就是一个length。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function getSign(_0x2c8a1a) { var _0x388511 = { 'DWutz': function(_0x1bf78a, _0x23f0a5) { return _0x1bf78a + _0x23f0a5; }, 'ldLIf': function(_0x41a705, _0x25b696) { return _0x41a705 - _0x25b696; }, 'pwfjR': _0xfa96('13c', 'Yb@s') }; var _0x2f1bba = ''; for (var _0x4d9213 = 0x0; _0x4d9213 < _0x2c8a1a[_0xfa96('13d', '3nwA')]; _0x4d9213++) { if (_0x2c8a1a[_0x4d9213] && _0x2c8a1a[_0x4d9213][_0xfa96('13e', 'eoTh')]) { _0x2f1bba += _0x388511[_0xfa96('13f', 'eoTh')](_0x2c8a1a[_0x4d9213][_0xfa96('140', 'Q5pM')] + '=', _0x2c8a1a[_0x4d9213][_0xfa96('141', 'hlCR')]); if (_0x388511['ldLIf'](_0x2c8a1a['length'], 0x1) > _0x4d9213) { _0x2f1bba += '&'; } } } console[_0xfa96('142', 'o9yX')](_0x2f1bba); return md5(_0x388511[_0xfa96('143', 'SKqK')](_0x2f1bba, _0x388511['pwfjR'])); }
|
跟踪进入getSign
查看md5加密的值
其中,_0x2f1bba
是重新拼接之后的值, _0x388511['pwfjR'])
为一个常量salteBrkhGPrugSZqXEwB6YnX7m49VIQYObJ
。
最后组合成为 md5(排序参数组合+salt)
梳理加密流程
获取参数 –> 排序参数 -> 组合参数 -> 末尾拼接salt -> md5 -> 拼接sign= -> 发送
mitmproxy脚本实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class AutoDecode4Finance(AutoDecoderClass):
def new_string(self, params): params_sorted = sorted(params.split("&")) for i, v in enumerate(params_sorted): params_sorted[i] = urllib.parse.unquote(v) return "&".join(params_sorted)
def request(self, flow: http.HTTPFlow): if flow.request.url.endswith(".dwr") or '&sign=' in flow.request.url: return
if flow.request.method.lower() == "get": query = urllib.parse.urlparse(flow.request.url).query else: query = flow.request.content.decode("utf-8")
new_string = self.new_string(query) new_string += "eBrkhGPrugSZqXEwB6YnX7m49VIQYObJ" flow.request.query['sign'] = md5hash(new_string)
|
效果
去掉sign参数可以正常请求,因为mitmdump已经自动完成参数的拼接了
最后
本文是举了一个比较简单的算法例子,还有其他的如des、aes、sm4等算法都可以用此办法进行加密中转。
但是如果遇到复杂的算法,比如自带了一些自己写的算法,如果在代码篇幅不多的情况下,其实手动还原会好点,如果引用了数不清的莫名其妙的东西,可以采用其他方法进行加密算法的还原或者是调用。比如python的execjs来局部调用一些js的算法(不想还原js算法的情况下)。
Reference