前端加密对抗-mitmproxy

前端加密对抗-mitmproxy

分析前端加密的意义?

正常来说,我们在做测试的时候,一般都是明文数据,并且服务端不会对传入的参数进行检测。这个时候的测试就是一番风顺。

  1. 如下图,请求参数会带一个sign=xx,后端会对这个sign进行校验,此处的校验规则是:将请求参数进行拆分,并且按照ascii顺序排序之后进行md5(query + “字符常量”)的加密方式进行校验

image-20230208174716892

  1. 如下图,请求内容和响应内容都是加密的

image-20230208174827150

遇到这些复杂的情况时,我们一般很难用burp继续去测试,因为需要对参数重新进行加密算法的操作,人工操作的话肯定是不现实的。

基本操作

找断点——通过搜索敏感关键词

image-20230208164323959

通过请求参数发现关键词sign=

通过devtools进行sources的搜索
image-20230208164536660

找到sign加密的地方并且打断点,再次触发请求可以进行debug

image-20230208164721142

找断点——通过网络请求的调用栈

有时候关键词搜索出来的结果过于多,可以通过此方式查找会更精确

image-20230208170012837

找一个看起来更容易定位的函数,这里的函数其实可以随便找,因为在调用栈中,可以回溯/跟进每个函数。

image-20230208170058250

断点发现有一个关键词参数encryptedData

image-20230208170253688

跟踪加密的方法是s=Object(ht.b)(o, _dyn$.t(622)),可以确认加密函数是s=Object(ht.b), 所以可以在上图3245行进行断点跟踪,找到加密方法

image-20230208170502589

mitmdump的基本使用(addons编写)

  1. 简单的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()
]
  1. 给mitmdump运行, 加了一些参数,仅供参考

image-20230208182401082

  1. 设置burp代理
    image-20230208182450772

    如果勾选了Do DNS lookups over SOCKs Proxy, 脚本能获取的url信息是包含域名的,如果不勾选的话,获取到的url信息是IP的,因为经过了一层解析。

手写加密算法

如下图所示的加密校验(此处我修改了3c为2c,为了是将报错作为参考系)

image-20230208175313161

可以看出sign=xxx是整个数据包发送到后端之后提供给后端校验的数据

通过debug查看加密算法

image-20230208175720198

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从小到大排序

image-20230208175933622

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加密的值

image-20230208180501904

其中,_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):
# 每个参数url解码一次
params_sorted[i] = urllib.parse.unquote(v)
return "&".join(params_sorted)

def request(self, flow: http.HTTPFlow):
# 跳过drw的url, 已经带sign=的不在计算
if flow.request.url.endswith(".dwr") or '&sign=' in flow.request.url:
return

if flow.request.method.lower() == "get":
# 获取get链接的参数
query = urllib.parse.urlparse(flow.request.url).query
else:
# 获取 post 链接的参数
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已经自动完成参数的拼接了

image-20230208181646612

最后

本文是举了一个比较简单的算法例子,还有其他的如des、aes、sm4等算法都可以用此办法进行加密中转。

但是如果遇到复杂的算法,比如自带了一些自己写的算法,如果在代码篇幅不多的情况下,其实手动还原会好点,如果引用了数不清的莫名其妙的东西,可以采用其他方法进行加密算法的还原或者是调用。比如python的execjs来局部调用一些js的算法(不想还原js算法的情况下)。

Reference